Spice version 0.1.0 -- plutovg 1.3.3 Audience: Turmeric users who want off-screen 2D rendering (PNG/JPEG output, SVG-shape rasterisation, text composition). If you need real-time drawing prefer
tur-raylib;tur-plutovgis built for static images.
This guide walks through the five things you'll do 95% of the time:
tur-png for fine-grained editsEach section is a self-contained snippet you can drop into a defn main.
In your project's build.tur:
:spices #{
"plutovg" #{:url "https://github.com/rjungemann/turmeric-spices"
:ref "plutovg-v0.1.0"
:subdir "spices/plutovg"}
}
Then:
tur fetch # downloads plutovg, runs cmake, builds libplutovg.a
The first fetch takes 15-30 seconds (cmake + 11 plutovg .c files); subsequent ones reuse the cached build.
Every render targets a surface (a pixel buffer) and goes through a canvas (the drawing context).
(import plutovg/surface :refer [surface-create surface-destroy])
(import plutovg/canvas :refer [canvas-create canvas-destroy])
(let [sr (surface-create 800 600)]
(when (ok? sr)
(let [s (ok-val sr)
cr (canvas-create s)]
(when (ok? cr)
(let [c (ok-val cr)]
;; ... draw here ...
(canvas-destroy c)))
(surface-destroy s))))
Surfaces hold premultiplied ARGB pixels (32 bits per pixel; on a little-endian host the in-memory byte order is B G R A). The canvas inherits the surface's dimensions; it never owns the surface, so always destroy in the reverse order of creation.
ok?andok-valare auto-loaded stdlib helpers fromstdlib/result.tur; no explicit(import result ...)is needed.
(import plutovg/canvas :refer [canvas-set-source-color
canvas-circle canvas-fill
canvas-rect canvas-stroke
canvas-set-line-width])
(canvas-set-source-color c 0.13 0.55 0.99 1.0) ; sky blue
(canvas-circle c 400.0 300.0 120.0)
(canvas-fill c)
(canvas-set-source-color c 0.0 0.0 0.0 1.0) ; black outline
(canvas-set-line-width c 3.0)
(canvas-circle c 400.0 300.0 120.0)
(canvas-stroke c)
Two things to note:
canvas-fill and canvas-stroke consume the recorded path. If you want to fill and stroke the same path, build it in a standalone plutovg/path and use canvas-fill-path / canvas-stroke-path, or copy the path with path-clone.[0, 1]. There's also paint-create-color-hex if you prefer CSS-style strings -- see below.Gradients need their stops, spread method, and matrix at construction time (plutovg's API has no post-hoc setters), so we build a small stops buffer first.
(import plutovg/paint :refer [gradient-stops-create gradient-stops-add
gradient-stops-destroy
paint-create-linear-gradient
paint-destroy canvas-set-source])
(let [sbr (gradient-stops-create)
stops (ok-val sbr)]
(gradient-stops-add stops 0.0 0.13 0.55 0.99 1.0) ; sky blue
(gradient-stops-add stops 1.0 0.99 0.40 0.13 1.0) ; coral
(let [pr (paint-create-linear-gradient
0.0 0.0 800.0 600.0 ":pad" stops)
paint (ok-val pr)]
(canvas-set-source c paint)
(canvas-rect c 0.0 0.0 800.0 600.0)
(canvas-fill c)
(paint-destroy paint))
(gradient-stops-destroy stops))
For a radial gradient swap in paint-create-radial-gradient (cx cy cr fx fy fr spread stops). The same stops buffer works for both flavours.
The :pad spread keyword keeps the gradient's edge colors at the extents; :reflect and :repeat are the other options.
(let [pr (paint-create-color-hex "#22aaff")]
(when (ok? pr)
(canvas-set-source c (ok-val pr))
(canvas-rect c 100.0 100.0 200.0 200.0)
(canvas-fill c)
(paint-destroy (ok-val pr))))
paint-create-color-hex understands the syntaxes plutovg's CSS color parser accepts: #rgb, #rrggbb, #rrggbbaa, rgb(...), rgba(...), named colors like "red", etc.
Text needs a font face. You either load one off disk or set up a face cache keyed by family + style.
(import plutovg/font :refer [font-face-load-from-file font-face-destroy
canvas-set-font canvas-fill-text
canvas-text-extents])
(import plutovg/path :refer [rect-w rect-h rect-destroy])
(let [fr (font-face-load-from-file
"/System/Library/Fonts/Supplemental/Arial.ttf" 0)]
(when (ok? fr)
(let [ff (ok-val fr)]
(canvas-set-font c ff 36.0)
(canvas-set-source-color c 0.1 0.1 0.1 1.0)
(canvas-fill-text c "Hello, world!" ":utf8" 50.0 100.0)
;; Measure for layout
(let [er (canvas-text-extents c "Hello, world!" ":utf8")
box (ok-val er)]
(println (rect-w box) "x" (rect-h box))
(rect-destroy box))
(font-face-destroy ff))))
Coordinates for canvas-fill-text are the baseline origin, not the top-left of the glyphs. Use font-face-ascent / font-face-descent if you need to convert from a top-left layout.
For larger applications it's cleaner to register all your fonts up front:
(import plutovg/font :refer [font-cache-create font-cache-destroy
font-cache-add-file font-cache-get
canvas-set-font])
(let [fc (ok-val (font-cache-create))]
(font-cache-add-file fc "/fonts/inter/Inter-Regular.ttf")
(font-cache-add-file fc "/fonts/inter/Inter-Bold.ttf")
;; ... draw with whichever style you need
(let [ff (ok-val (font-cache-get fc "Inter" ":bold" 24.0))]
(canvas-set-font c ff 24.0)
(canvas-fill-text c "Headline" ":utf8" 50.0 80.0))
(font-cache-destroy fc)) ; releases all registered faces
font-cache-get returns a borrowed face -- do not font-face-destroy it. Let font-cache-destroy clean everything up.
If you want plutovg to discover the platform's standard font directories, call font-cache-load-system fc instead of (or in addition to) add-file. That works on macOS and Linux; behaviour on other platforms depends on plutovg's build.
PNG and JPEG are one-liners:
(import plutovg/surface :refer [surface-write-png surface-write-jpeg])
(surface-write-png s "render.png")
(surface-write-jpeg s "render.jpg" 90) ; quality in [0, 100]
Both return (ok 0) / (err 0) results, so check before claiming success:
(let [r (surface-write-png s "render.png")]
(if (ok? r)
(println "wrote render.png")
(println "PNG write failed")))
tur-plutovg rasterises a scene into a tightly packed pixel buffer; tur-png lets you read and edit those pixels by hand. The bridge is surface-data and tur-png's png-write-raw:
(import plutovg/surface :refer [surface-data surface-width surface-height])
(import png/writer :refer [png-write-raw])
;; ... draw with plutovg into surface `s` ...
(png-write-raw "out.png"
(surface-data s)
(surface-width s)
(surface-height s)
4 ; channels: RGBA
8) ; bit depth
Two byte-order caveats:
(r, g, b, a) is stored as (r*a/255, g*a/255, b*a/255, a). If you want unmultiplied output (the usual PNG convention), divide each colour channel by a/255 before handing the buffer to tur-png.B G R A. tur-png's png-write-raw accepts that layout when the channel count is 4 -- it stores rows verbatim. If you need RGBA-ordered bytes (for an external API), shuffle in place after rendering.For most workflows -- "draw with plutovg, save with plutovg" -- skip tur-png entirely and call surface-write-png. Reach for tur-png when you need to read an existing PNG into plutovg or post-process individual pixels in Turmeric.
In reverse construction order:
paint-destroy every paint you created.path-destroy every standalone plutovg/path.font-face-destroy only the faces you loaded directly with font-face-load-from-file / font-face-load-from-data. Faces obtained from a font cache are owned by the cache.font-cache-destroy to release the cache (and every cached face).gradient-stops-destroy / dash-array-destroy for builder buffers.rect-destroy for any rect handles returned by path-extents / font-face-text-extents / canvas-text-extents.canvas-destroy the canvas.surface-destroy the surface last.Forgetting any one of these is a memory leak, not a crash -- plutovg uses reference-counted internals and a failed cleanup is recoverable, but valgrind / ASAN will complain.
tur-raylib -- if you need real-time / windowed rendering instead of static images.tur-png -- pixel-level edits on plutovg's output.plutovg/path -- the standalone path object covers SVG path-data parsing (path-parse), flattening (path-clone-flatten), dashing (path-clone-dashed), and bounding-box queries (path-extents). Useful when you want to compute geometry once and stamp it many times.plutovg/canvas -- there's a full state stack (canvas-save / canvas-restore), Porter-Duff blend operators (canvas-set-operator), and global opacity (canvas-set-opacity) for compositing effects.