SVG Icons in C++ with LunaSVG

By the Axialis Engineering team ·

SVG Icons in C++ with LunaSVG

You want crisp SVG icons in a C++ desktop app, but most GUI toolkits only accept raster bitmaps, so you need to rasterize the vector yourself at the right pixel size. This guide rasterizes IconVectors-exported SVGs with LunaSVG — a small, dependency-free C++17 renderer — and hands the pixels to three real targets: a Windows HBITMAP for GDI, a PNG file, and a wxBitmap for wxWidgets.

LunaSVG parses an SVG into a lunasvg::Document via Document::loadFromData() (or loadFromFile()), then Document::renderToBitmap() returns a lunasvg::Bitmap of premultiplied ARGB pixels. The one detail that trips people up is that pixel layout: every target below needs a different swizzle or un-premultiply step, and each section shows the exact loop.

The SVG file is parsed once by LunaSVG and rasterized at the pixel size each display scale needs, then handed to the toolkit as HBITMAP or wxBitmap icon.svgone sourceLunaSVGfull SVG 1.1 rendererparses the vector oncepaths, fills, strokesrasterize at the target size16 pxat 100%24 pxat 150%32 pxat 200%HBITMAP or wxBitmapcopied from the LunaSVG bitmapcrisp at 1x, 2x, 3xwhole-pixel sizes onlypremultiply alpha for GDIRender on demand for the DPI the window actually has; never scale a bitmap that was drawn for another one.
Vector in, pixels out, at the right moment. LunaSVG parses the file once; you ask for a bitmap at the size the current display scale needs, 16 px at 100 percent, 24 at 150, 32 at 200, and hand the result to the toolkit as HBITMAP or wxBitmap. LunaSVG renders into its own 32-bit buffer; copy it into the toolkit bitmap with the alpha handling that toolkit expects.

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).
    A 24x24 camera icon open on the IconVectors canvas, with the layers and pixel/vector preview panels visible
    Use File → Export → Export Minified (Shift+Ctrl+M) to get a lean SVG.

Windows: render an SVG to HBITMAP using the Windows API

GDI's AlphaBlend expects a top-down 32-bpp DIB with BGRA premultiplied pixels, but LunaSVG hands you ARGB premultiplied. The snippet below renders the SVG, allocates a DIB section with a negative biHeight (top-down), then swizzles ARGB→BGRA per pixel so the result drops straight into AlphaBlend or BitBlt.

#include <lunasvg.h>
#include <windows.h>
#include <cstdint>
#include <cstring>
#include <iostream>

using namespace lunasvg;

static HBITMAP CreateHBITMAPFromLunaBitmap(const Bitmap& bmp)
{
    if (!bmp.valid()) return nullptr;

    const int w = bmp.width(), h = bmp.height();
    const int srcStride = bmp.stride();      // ARGB premultiplied (A,R,G,B)
    const int dstStride = w * 4;             // BGRA premultiplied for 32bpp DIB

    BITMAPINFO bi{};
    bi.bmiHeader.biSize        = sizeof(BITMAPINFOHEADER);
    bi.bmiHeader.biWidth       = w;
    bi.bmiHeader.biHeight      = -h;         // 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 uint8_t* src = bmp.data();
    uint8_t* dst = static_cast<uint8_t*>(dib);

    // Swizzle ARGB (A,R,G,B) → BGRA (B,G,R,A); premultiplication preserved
    for (int y = 0; y < h; ++y) {
        const uint8_t* srow = src + y * srcStride;
        uint8_t* drow = dst + y * dstStride;
        for (int x = 0; x < w; ++x) {
            const uint8_t A = srow[x*4 + 0];
            const uint8_t R = srow[x*4 + 1];
            const uint8_t G = srow[x*4 + 2];
            const uint8_t B = srow[x*4 + 3];
            drow[x*4 + 0] = B;
            drow[x*4 + 1] = G;
            drow[x*4 + 2] = R;
            drow[x*4 + 3] = A;
        }
    }
    return hbmp;
}

// Example SVG (export your own minified XML from IconVectors)
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() {
  auto doc = Document::loadFromData(kSVG);
  if (!doc) { std::cerr << "Parse error\n"; return 1; }

  // Render at document size or pick explicit pixels: renderToBitmap(w, h)
  auto bmp = doc->renderToBitmap(24, 24);
  if (!bmp.valid()) { std::cerr << "Render error\n"; return 2; }

  HBITMAP hbmp = CreateHBITMAPFromLunaBitmap(bmp);
  if (!hbmp) { std::cerr << "HBITMAP error\n"; return 3; }

  // 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

If you only need a raster file — for a build step, an icon cache, or a platform that loads PNGs directly — LunaSVG writes one for you. Bitmap::writeToPng() serializes the rendered buffer to disk, so the whole pipeline is parse, render, write, with no GUI toolkit involved. The same three calls compile unchanged on Windows, macOS, and Linux.

#include <lunasvg.h>
using namespace lunasvg;

// Your SVG payload (exported & minified in IconVectors)
static const char kSvg[] = R"SVG(
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <path d="M4 12 9 17 20 6" fill="none" stroke="black" stroke-width="2"/>
</svg>
)SVG";

int main()
{
    // 1. Parse the SVG straight from the std::string
    auto doc = Document::loadFromData(kSvg);              // in-memory API
    if (!doc)                                             // nullptr -> parse error
        return -1;

    // 2. Rasterize to an RGBA bitmap (defaults to the SVG’s own width/height)
    auto bmp = doc->renderToBitmap();
    if (!bmp.valid())                                     // empty -> render error
        return -2;

    // 3. Persist the bitmap as a PNG file
    // (writeToPng saves the current bitmap buffer to disk)
    bmp.writeToPng("output.png");

    return 0;
}

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

wxImage stores straight (non-premultiplied) RGB in one buffer and alpha in a separate buffer, so the conversion here un-premultiplies each channel (R = Rm * 255 / A, guarding against A == 0) before handing both buffers to wxImage. The resulting wxBitmap draws in any paint handler with DrawBitmap.

#include <lunasvg.h>
#include <wx/wx.h>

using namespace lunasvg;

static const char kSvg[] = R"SVG(
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
  <circle cx="12" cy="12" r="8" fill="black"/>
</svg>
)SVG";

static wxBitmap MakeWxBitmapFromLuna(const Bitmap& bmp) {
  if (!bmp.valid()) return wxBitmap();

  const int w = bmp.width(), h = bmp.height();
  const int stride = bmp.stride();
  const unsigned char* src = bmp.data(); // ARGB premultiplied

  // Allocate RGB buffer (wxImage takes ownership) and separate alpha
  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 = src + y*stride;
    for (int x = 0; x < w; ++x) {
      const unsigned char A = srow[x*4 + 0];
      const unsigned char Rm = srow[x*4 + 1];
      const unsigned char Gm = srow[x*4 + 2];
      const unsigned char Bm = srow[x*4 + 3];

      // Un‑premultiply: R = Rm*255/A (guard A=0)
      unsigned char R = (A ? (unsigned)(Rm*255 + (A/2)) / A : 0);
      unsigned char G = (A ? (unsigned)(Gm*255 + (A/2)) / A : 0);
      unsigned char B = (A ? (unsigned)(Bm*255 + (A/2)) / A : 0);

      const int idx = (y*w + x);
      rgb[idx*3 + 0] = R;
      rgb[idx*3 + 1] = G;
      rgb[idx*3 + 2] = B;
      alpha[idx] = 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 {
    auto doc = Document::loadFromData(kSvg);
    if (!doc) return false;
    auto bmp = doc->renderToBitmap(64, 64);
    if (!bmp.valid()) return false;

    wxBitmap wxbmp = MakeWxBitmapFromLuna(bmp);

    auto* frame = new wxFrame(nullptr, wxID_ANY, "LunaSVG + 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

Related guides

Start Making SVG Icons Today with IconVectors

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

Version 2.10 for Windows - October 6, 2026. macOS and Linux: version 2.00 available now, 2.10 coming soon.