SVG Icons in C++ with NanoSVG

By the Axialis Engineering team ·

SVG Icons in C++ with NanoSVG

C++ has no built-in way to draw an SVG icon, so toolkits like GDI, wxWidgets, and your own paint code expect raw pixels. NanoSVG fixes that with two single-header files and no build system to configure: this guide takes a clean SVG exported from Axialis IconVectors and rasterizes it to a Windows HBITMAP for AlphaBlend, to a cross-platform PNG, and to a wxBitmap you can draw in a wxWidgets paint handler.

NanoSVG is the lightest of the common options. If you need full SVG fidelity (filters, gradients-on-strokes, text), see the LunaSVG guide instead; for native toolkit renderers, see WinUI and Qt.

What is NanoSVG, and when to use it

Export a clean SVG from IconVectors

  1. Open or create your icon:
    • File → Open… (Ctrl+O) or New Icon (Ctrl+N).
    • Prefer a simple, single-color shape and a clean viewBox (e.g., 0 0 24 24).
    An SVG folder icon open on the IconVectors canvas, ready to export as a minified SVG
    Use File → Export → Export Minified (Shift+Ctrl+M) to get a lean SVG.

Windows: render an SVG to HBITMAP using the Windows API (NanoSVG)

The snippet below parses the SVG from memory, rasterizes it with NanoSVG to straight-alpha RGBA, then converts to a 32-bpp top-down DIB (HBITMAP) with premultiplied BGRA pixels for use with AlphaBlend.

#define NANOSVG_IMPLEMENTATION
#include "nanosvg.h"        // SVG parser
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvgrast.h"    // Rasterizer

#include <windows.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <iostream>

static const char kSVG[] = R"SVG(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" 
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" 
stroke-miterlimit="10" d="M14.8 3.4l1.9 2.3c.2 .2 .5 .4 .8 .4H20c1.1 0 2 .9 2 
2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V8c0-1.1 .9-2 2-2h2.5c.3 0 .6-.1 .8-.4l1.9-2.3
c.2-.2 .5-.4 .8-.4h4.1C14.3 3 14.6 3.1 14.8 3.4zM8.5 12.5c0 1.9 1.6 3.5 3.5 3.5
s3.5-1.6 3.5-3.5s-1.6-3.5-3.5-3.5S8.5 10.6 8.5 12.5z"/></svg>
)SVG";

// Create a top-down 32-bpp DIB HBITMAP from straight RGBA pixels.
// Converts RGBA (straight) → BGRA (premultiplied) for GDI AlphaBlend.
static HBITMAP CreateHBITMAPFromRGBA(const unsigned char* rgba,
                                     int width, int height, int srcStrideBytes)
{
  BITMAPINFO bi{};
  bi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
  bi.bmiHeader.biWidth       = width;
  bi.bmiHeader.biHeight      = -height;      // top-down
  bi.bmiHeader.biPlanes      = 1;
  bi.bmiHeader.biBitCount    = 32;
  bi.bmiHeader.biCompression = BI_RGB;

  void* dib = nullptr;
  HDC hdc = GetDC(nullptr);
  HBITMAP hbmp = CreateDIBSection(hdc, &bi, DIB_RGB_COLORS, &dib, nullptr, 0);
  ReleaseDC(nullptr, hdc);
  if (!hbmp || !dib) return nullptr;

  const int dstStride = width * 4;
  unsigned char* dst = static_cast(dib);

  for (int y = 0; y < height; ++y) {
    const unsigned char* s = rgba + y * srcStrideBytes;
    unsigned char* d       = dst  + y * dstStride;
    for (int x = 0; x < width; ++x) {
      const unsigned char R = s[4*x + 0];
      const unsigned char G = s[4*x + 1];
      const unsigned char B = s[4*x + 2];
      const unsigned char A = s[4*x + 3];

      // Premultiply then store as BGRA
      const unsigned char Rp = (unsigned)(R * A + 127) / 255;
      const unsigned char Gp = (unsigned)(G * A + 127) / 255;
      const unsigned char Bp = (unsigned)(B * A + 127) / 255;

      d[4*x + 0] = Bp;
      d[4*x + 1] = Gp;
      d[4*x + 2] = Rp;
      d[4*x + 3] = A;
    }
  }
  return hbmp;
}

int main()
{
  // 1) Parse SVG (units: px). NanoSVG expects a dpi; 96 is standard for icon work.
  NSVGimage* svg = nsvgParse((char*)kSVG, "px", 96.0f);
  if (!svg) { std::cerr << "Parse error\n"; return 1; }

  // 2) Decide output size (e.g., 24×24 px) and scale from the SVG viewBox size.
  const float outW = 24.0f, outH = 24.0f;
  const float scaleX = outW / svg->width;
  const float scaleY = outH / svg->height;
  const float scale  = (float)fmin(scaleX, scaleY);

  // 3) Rasterize to straight RGBA
  NSVGrasterizer* rast = nsvgCreateRasterizer();
  if (!rast) { nsvgDelete(svg); return 2; }

  const int W = (int)lround(outW);
  const int H = (int)lround(outH);
  const int stride = W * 4;

  unsigned char* rgba = (unsigned char*)std::malloc(H * stride);
  if (!rgba) { nsvgDeleteRasterizer(rast); nsvgDelete(svg); return 3; }

  nsvgRasterize(rast, svg, 0.0f, 0.0f, scale, rgba, W, H, stride);

  // 4) Convert to a premultiplied BGRA top‑down DIB for GDI
  HBITMAP hbmp = CreateHBITMAPFromRGBA(rgba, W, H, stride);

  // Cleanup NanoSVG buffers
  std::free(rgba);
  nsvgDeleteRasterizer(rast);
  nsvgDelete(svg);

  if (!hbmp) { return 4; }

  // TODO: Select hbmp into a memory DC and AlphaBlend onto your window DC.
  DeleteObject(hbmp);
  return 0;
}

Optional: blit with alpha

// After you get HBITMAP hbmp:
HDC hdcScreen = GetDC(hwnd);
HDC hdcMem    = CreateCompatibleDC(hdcScreen);
HBITMAP old   = (HBITMAP)SelectObject(hdcMem, hbmp);

BLENDFUNCTION bf{};
bf.BlendOp             = AC_SRC_OVER;
bf.BlendFlags          = 0;
bf.SourceConstantAlpha = 255;       // use per-pixel alpha
bf.AlphaFormat         = AC_SRC_ALPHA;

AlphaBlend(hdcScreen, dstX, dstY, W, H, hdcMem, 0, 0, W, H, bf);

SelectObject(hdcMem, old);
DeleteDC(hdcMem);
ReleaseDC(hwnd, hdcScreen);

Cross-platform: import SVG and convert to PNG

When you only need a PNG on disk (build-time icon baking, CI, or a headless tool), skip the platform bitmap types entirely: parse and rasterize with NanoSVG, then write the RGBA buffer with a single-header writer such as stb_image_write. The same code compiles on Windows, macOS, and Linux.

#define NANOSVG_IMPLEMENTATION
#include "nanosvg.h"
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvgrast.h"

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"   // Single-header PNG writer

#include <cstdlib>
#include <cstring>
#include <cmath>
#include <iostream>

static const char kSvg[] = R"SVG(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" 
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" 
stroke-miterlimit="10" d="M14.8 3.4l1.9 2.3c.2 .2 .5 .4 .8 .4H20c1.1 0 2 .9 2 
2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V8c0-1.1 .9-2 2-2h2.5c.3 0 .6-.1 .8-.4l1.9-2.3
c.2-.2 .5-.4 .8-.4h4.1C14.3 3 14.6 3.1 14.8 3.4zM8.5 12.5c0 1.9 1.6 3.5 3.5 3.5
s3.5-1.6 3.5-3.5s-1.6-3.5-3.5-3.5S8.5 10.6 8.5 12.5z"/></svg>
)SVG";

int main()
{
  NSVGimage* svg = nsvgParse((char*)kSvg, "px", 96.0f);
  if (!svg) { std::cerr << "Parse error\n"; return 1; }

  const int W = 64, H = 64;                 // desired PNG size
  const float sx = W / svg->width;
  const float sy = H / svg->height;
  const float scale = (float)fmin(sx, sy);

  NSVGrasterizer* rast = nsvgCreateRasterizer();
  if (!rast) { nsvgDelete(svg); return 2; }

  unsigned char* rgba = (unsigned char*)std::malloc(W * H * 4);
  if (!rgba) { nsvgDeleteRasterizer(rast); nsvgDelete(svg); return 3; }

  nsvgRasterize(rast, svg, 0, 0, scale, rgba, W, H, W*4);

  // Write PNG (straight RGBA)
  if (!stbi_write_png("output.png", W, H, 4, rgba, W*4)) {
    std::cerr << "PNG write error\n";
  }

  std::free(rgba);
  nsvgDeleteRasterizer(rast);
  nsvgDelete(svg);
  return 0;
}

Cross-platform: NanoSVG + wxWidgets (draw a wxBitmap)

This example converts a NanoSVG raster (straight RGBA) into a wxBitmap and draws it in a wxWidgets paint handler. wxImage stores color and transparency separately, so split the rasterizer output into an RGB buffer and an alpha buffer with SetAlpha() rather than premultiplying.

#define NANOSVG_IMPLEMENTATION
#include "nanosvg.h"
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvgrast.h"

#include <wx/wx.h>
#include <cstdlib>
#include <cmath>

static const char kSvg[] = R"SVG(
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" 
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" 
stroke-miterlimit="10" d="M14.8 3.4l1.9 2.3c.2 .2 .5 .4 .8 .4H20c1.1 0 2 .9 2 
2v10c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V8c0-1.1 .9-2 2-2h2.5c.3 0 .6-.1 .8-.4l1.9-2.3
c.2-.2 .5-.4 .8-.4h4.1C14.3 3 14.6 3.1 14.8 3.4zM8.5 12.5c0 1.9 1.6 3.5 3.5 3.5
s3.5-1.6 3.5-3.5s-1.6-3.5-3.5-3.5S8.5 10.6 8.5 12.5z"/></svg>
)SVG";

static wxBitmap MakeWxBitmapFromRGBA(const unsigned char* rgba, int w, int h)
{
  // wxImage expects non‑premultiplied RGB + a separate alpha buffer.
  unsigned char* rgb   = new unsigned char[w*h*3];
  unsigned char* alpha = new unsigned char[w*h];

  for (int y = 0; y < h; ++y) {
    const unsigned char* srow = rgba + y * (w*4);
    for (int x = 0; x < w; ++x) {
      const int idx = y*w + x;
      rgb[idx*3 + 0] = srow[x*4 + 0]; // R
      rgb[idx*3 + 1] = srow[x*4 + 1]; // G
      rgb[idx*3 + 2] = srow[x*4 + 2]; // B
      alpha[idx]     = srow[x*4 + 3]; // A
    }
  }
  wxImage img(w, h, rgb, /*static_data=*/false);
  img.SetAlpha(alpha, /*static_data=*/false);
  return wxBitmap(img);
}

class MyApp : public wxApp {
public:
  bool OnInit() override {
    NSVGimage* svg = nsvgParse((char*)kSvg, "px", 96.0f);
    if (!svg) return false;

    const int W=64, H=64;
    const float scale = std::min(W/svg->width, H/svg->height);

    NSVGrasterizer* rast = nsvgCreateRasterizer();
    if (!rast) { nsvgDelete(svg); return false; }

    unsigned char* rgba = (unsigned char*)std::malloc(W*H*4);
    if (!rgba) { nsvgDeleteRasterizer(rast); nsvgDelete(svg); return false; }

    nsvgRasterize(rast, svg, 0, 0, scale, rgba, W, H, W*4);
    wxBitmap wxbmp = MakeWxBitmapFromRGBA(rgba, W, H);

    std::free(rgba);
    nsvgDeleteRasterizer(rast);
    nsvgDelete(svg);

    auto* frame = new wxFrame(nullptr, wxID_ANY, "NanoSVG + wxWidgets", wxDefaultPosition, wxSize(240,160));
    frame->Bind(wxEVT_PAINT, [wxbmp](wxPaintEvent&){
      wxPaintDC dc(wxDynamicCast(wxTheApp->GetTopWindow(), wxFrame));
      dc.DrawBitmap(wxbmp, 40, 40, /*useMask=*/true);
    });
    frame->Show();
    return true;
  }
};
wxIMPLEMENT_APP(MyApp);

Notes & troubleshooting

Wrapping up

With two headers and four function calls, NanoSVG turns the SVG icons you author in IconVectors into the pixel buffers GDI, stb_image_write, and wxWidgets need. The work that decides image quality happens upstream: a minified, single-color SVG with a clean viewBox and no unsupported elements rasterizes predictably at every size. Author and validate those icons in IconVectors, then drop in the renderer that fits your toolkit.

Related guides

Start Making SVG Icons Today with IconVectors

Download the fully-functional 30‑Day Free Trial and unlock your icon design workflow.

Version 1.70 for Windows and macOS