github.com/OrelSokolov/nanosvg.cr
1.1.0 / published Oct 13, 2026 / repository
Pure Crystal port of NanoSVG: a simple SVG parser and rasterizer. Parses SVG files into cubic bezier shapes and rasterizes them to RGBA, byte-identical to the C library on the test artwork. No C extensions, no external dependencies.
nanosvg.cr
Pure Crystal SVG parser and rasterizer, based on NanoSVG by Mikko Mononen (zlib license).
Parses SVG files into a list of cubic bezier shapes and rasterizes them to
an RGBA buffer. It started as a faithful port of the C library — on the
test artwork it produces byte-identical output — and has since been
extended beyond the C original: <use> with <defs> path templates,
nested <svg> viewports, MathJax-compatible rendering (see
"Differences from C"), and <text>/<tspan> rendering with real
font outlines, advances and GPOS kerning (see "Text rendering").
Installation
- Add the dependency to your
shard.yml:
dependencies:
nanosvg:
github: OrelSokolov/nanosvg.cr
- Run
shards install
Usage
require "nanosvg"
# Parse (units: "px", "pt", "pc", "mm", "cm", "in"; DPI controls conversion)
image = NanoSVG.parse_from_file("test.svg", "px", 96.0f32)
puts "size: #{image.width} x #{image.height} (#{image.shapes.size} shapes)"
# Walk the shapes: each path is a flat Float32 array of
# x0,y0, cpx1,cpx1,cpx2,cpy2,x1,y1, ... (npts-1 divisible by 3)
image.shapes.each do |shape|
shape.paths.each do |path|
i = 0
while i < path.npts - 1
p = i*2
draw_cubic_bez(path.pts[p], path.pts[p+1], path.pts[p+2], path.pts[p+3],
path.pts[p+4], path.pts[p+5], path.pts[p+6], path.pts[p+7])
i += 3
end
end
end
# Rasterize to a non-premultiplied RGBA buffer
scale = 2.0f32
w = (image.width * scale).round.to_i32
h = (image.height * scale).round.to_i32
pixels = NanoSVG::Rasterizer.rasterize(image, 0.0f32, 0.0f32, scale, w, h)
A persistent NanoSVG::Rasterizer instance can be reused for multiple
images. See examples/render.cr for a complete parse → rasterize → PNG
pipeline (built on the stdlib, no external shards):
crystal run examples/render.cr -- example_data/drawing.svg out.png 512
Supported SVG features
- Elements:
svg(root and nested — a nested<svg>acts as a viewport transform, used by MathJax for stretchy delimiters),g,path,rect,circle,ellipse,line,polyline,polygon,defs,linearGradient,radialGradient,stop,style(simple.classselectors),use(references a<path id>fromdefsviahref/xlink:href, withx/yandtransform; forward references are resolved after parsing — this is what MathJax SVG output is built on) - Path commands: all of
MmLlHhVvCcSsQqTtAaZz(arcs are converted to cubic beziers) - Styles: presentation attributes, inline
style="...", CSS classes,display:none - Paint: solid colors (
#rgb,#rrggbb,rgb(), 147 named colors), linear/radial gradients (objectBoundingBox/userSpaceOnUse, gradientTransform, stops,xlink:hrefstop inheritance) - Stroke: width, opacity, caps (butt/round/square), joins
(miter/round/bevel), miter-limit, dashes + offset,
paint-order transform(matrix/translate/scale/rotate/skewX/skewY) with nestingviewBox,preserveAspectRatio, units (px pt pc mm cm in em ex %), DPI- Fill rules: nonzero and evenodd; anti-aliased scanline rasterization (5 sub-scanlines, 1/1024 fixed-point)
text/tspan: real glyph outlines from system TrueType fonts (font-family/size/weight/style, fill/gradient/opacity/transform,text-anchor,dominant-baseline,letter-spacing,x/y/dx/dy), with hmtx advances and GPOS PairPos kerning (formats 1/2, incl. extension lookups; legacykerntable as a fallback) — see "Text rendering"
Not supported: use referencing non-path elements, symbol,
clip paths, masks, filters, patterns, markers, CSS cascade beyond
simple classes, OpenType features beyond kerning (ligatures, marks),
CFF (.otf) and variable-font outlines, bidi/complex scripts.
Known limitations of the <use>/nested-<svg> support:
- A
transformattribute on the<path>template insidedefsis ignored when the template is instantiated by<use>(MathJax output does not use it). <use>with a forward reference (thedefsentry appears after the<use>in the document) is resolved after parsing and appended to the end of the shape list, so it paints on top rather than in document order. Invisible with opaque glyphs; may matter for overlapping semi-transparent shapes.- A nested
<svg>with aviewBoxbut nowidth/heightfalls back to scale 1 instead of the spec default (100% of the parent viewport), and the implicit clipping of nested viewports is not applied.
Text rendering
<text>/<tspan> elements become glyph-outline shapes placed along
the baseline with hmtx advances and GPOS kerning, laid out the way
Pango/HarfBuzz would lay the same string:
- A minimal TrueType reader (
src/nanosvg/font.cr, no dependencies):cmap(formats 4/12),hmtx,loca/glyf(simple + composite glyphs, quadratics elevated to cubics),hhea/OS/2metrics. - Kerning (
font_kern.cr): the GPOSkernfeature — PairPos formats 1 and 2, class-based and pair-set, extension (type 9) lookups — with the legacykerntable as a fallback. Values verified against HarfBuzz (Liberation Sans kern(A,V) = -152, kern(space,A) = -113; DejaVu Sans kern(A,V) = -131; Lato kern(A,V) = -108 — all exact). - Families resolve through fontconfig (
fc-match, the same library Pango uses), so both renderers pick the same font file; explicit registration is available viaNanoSVG::FontStore.register.
Validated against librsvg on a 100-case dataset
(dataset/text/gen.py, references rendered with hinting disabled so
both sides rasterize raw outlines): mean |Δα| averages 0.15
(0.35 on plain text, worst case 1.72 on dominant-baseline="middle"
where rsvg's metrics differ from ours by ~1px at 32px); 0.33% of
pixels differ by |Δα|>16 — the antialiasing floor between the NanoSVG
scanline rasterizer and Cairo/FreeType coverage. Byte identity is not
expected across different rasterizer algorithms; the residual is edge
coverage only, not geometry. See bench/text_pixel_diff.cr.
When no font can be resolved (no fc-match, e.g. headless Windows),
text elements are skipped silently — the pre-text behavior.
Differences from C
- Extended beyond the C original:
<use href="#id">referencing a<path id>from<defs>(withx/y,transform, and forward references), nested<svg>elements treated as viewport transforms — this is what MathJax SVG output is built on — and<text>/<tspan>rendering (see "Text rendering"). - Colors use the same bit layout as C:
0xAABBGGRR(R in the low byte). - The
NOTE(port)comments in the sources mark deliberate deviations (mostly: C read uninitialized memory in degenerate cases; the port uses deterministic values). - The C memory pool/freelist in the rasterizer is left to the GC.
- The full
NANOSVG_ALL_COLOR_KEYWORDSlist is always enabled.
Development
crystal spec # run the test suite
crystal run examples/render.cr -- example_data/23.svg out.png 512
License
zlib — same as the original NanoSVG (see LICENSE).
The polygon rasterization is based on the stb_truetype rasterizer by
Sean Barrett.
API
- NanoSVG
Big-endian readers shared by the font and its kerning tables.