Upstream lives at treeform/pixie; its README follows below and still describes the library accurately. This fork adds what FrameOS needs to draw pictures on hardware that does not have room for them.
A FrameOS scene renders on a Raspberry Pi Zero with 512MB of RAM, or on an ESP32-S3 with 8MB of PSRAM and about 100KB of internal heap. Upstream pixie decodes an image by allocating the finished image plus every intermediate the codec wants, which is the right trade on a desktop and the difference between rendering and rebooting on a frame. Everything below follows from that, plus a few features FrameOS wanted along the way.
Nothing here removes or renames upstream API: this is a superset, and it tracks
upstream. -d:frameosEmbedded only changes defaults (a conservative decode
budget), never behaviour you did not ask for.
A memory budget decoders actually respect (pixie/decodebudget.nim).
setDecodeBudgetBytes sets a per-decode ceiling covering intermediates and
output; decoders plan their allocations before making them and raise a catchable
PixieError when the plan does not fit, instead of taking the process down with
them. 0 means unlimited, which is the default on hosts. An application that
knows its live free memory can refresh the budget before every decode.
Decoding straight into the size you want. decodeImageScaled,
decodeImageScaledInto and readImageScaled take a target size and a fit mode
(fitStretch, fitCover, fitContain, see scaledFitRects), and the
downscale happens during decoding: a 4000×3000 JPEG headed for a 800×480 panel
never exists at full size. Sampling is box-filtered rather than nearest, in the
row-streamed decoders too (RowBoxSampler), and JPEG chroma is interpolated
rather than point-picked, so a heavy downscale does not come out crawling with
aliasing.
Streaming decoders that never hold the file. Every scaled decoder has a
pull-source form — decodePngStreamScaledInto, decodeJpegStreamScaledInto,
decodeBmpStreamScaledInto, decodePpmStreamScaledInto,
decodeWebpStreamScaledInto — driven by an ImageSourceProc callback that
hands over the next chunk of input. Feed one from a file and neither the
compressed bytes nor the full-size pixels are ever resident.
A self-contained streaming inflate (pixie/inflatestream.nim, vendored from
zippy 0.10.16). PNG scanlines leave a fixed ~64KB window as they are produced,
are unfiltered in place, and multi-IDAT streams are inflated as segments
rather than concatenated first. The fork depends on stock zippy again as a
result.
Images that can borrow pixels. view(image, x, y, w, h) is a window onto
another image's memory rather than a copy, with newImageFrom,
toContiguousSeq, the forEachSpan template and items/pairs iterators as
the seams that keep flat operations fast for owners and correct for views.
pixelsEqual compares contents. Image is {.acyclic.} — load-bearing, not an
optimisation: without it ORC treats every image as a cycle candidate, which
crashes a host that shares images with a dynamically loaded driver.
SVG that draws text, and draws into your buffer. <text> and <tspan>
become glyph outlines and then ordinary paths, so fill, stroke, gradients,
opacity and transforms apply to them exactly as to a <path>; font-family
resolution is the application's to answer through setSvgTypefaceResolver,
since pixie ships no fonts. parseSvgXml parses markup the way <text> needs
it. Svg.renderInto(target) rasterizes into an image the caller already owns,
which for a caller that has a correctly sized canvas is the difference between
one image and two.
Color emoji. COLR/CPAL layered glyphs and CBDT/CBLC and sbix bitmap glyphs
render through fillText, with hasColorGlyph to ask and Typeface.fallbacks
to supply an emoji face behind a text face.
Text as paths. Arrangement.computePath returns a whole arrangement's
outlines as one path and Font.baselineOffset gives the distance from the top
of a typeset block to its first baseline — the two pieces anything that
positions text by its baseline needs.
Fixes carried here. EXIF orientation was silently dropped for little-endian
(II) JPEGs, which is what most Sony and Canon bodies write, so those photos
decoded sideways. JPEG streaming resync tolerates a window slide.
👏 👏 👏 Check out video about the library: A full-featured 2D graphics library for Nim (NimConf 2021) 👏 👏 👏
Pixie is a 2D graphics library similar to Cairo and Skia written entirely in Nim.
This library is being actively developed and we'd be happy for you to use it.
nimby install pixie
Pixie includes CPU rasterization, image codecs, text layout, paths, paints, masking, blending, and SIMD-accelerated operations.
- Pixie 5.0 performance improvements
- The details of JPEG decoding in Nim
- A full-featured 2D graphics library for Nim (NimConf 2021)
Features:
- Typesetting and rasterizing text, including styled rich text via spans.
- Drawing paths, shapes and curves with even-odd and non-zero windings.
- Pixel-perfect AA quality.
- Supported file formats are PNG, BMP, JPG, SVG + more in development.
- Strokes with joins and caps.
- Shadows, glows and blurs.
- Complex masking: Subtract, Intersect, Exclude.
- Complex blends: Darken, Multiply, Color Dodge, Hue, Luminosity... etc.
- Many operations are SIMD accelerated.
| Format | Read | Write |
|---|---|---|
| PNG | ✅ | ✅ |
| JPEG | ✅ | |
| BMP | ✅ | ✅ |
| QOI | ✅ | ✅ |
| GIF | ✅ | |
| SVG | ✅ | |
| PPM | ✅ | ✅ |
| Format | Read |
|---|---|
| TTF | ✅ |
| OTF | ✅ |
| SVG | ✅ |
Supported Caps:
- Butt
- Round
- Square
Supported Joins:
- Miter (with miter angle limit)
- Bevel
- Round
Supported Blend Modes:
- Normal
- Darken
- Multiply
- ColorBurn
- Lighten
- Screen
- Color Dodge
- Overlay
- Soft Light
- Hard Light
- Difference
- Exclusion
- Hue
- Saturation
- Color
- Luminosity
Supported Mask Modes:
- Mask
- Overwrite
- Subtract Mask
- Intersect Mask
- Exclude Mask
| Format | Supported | Description |
|---|---|---|
| M m | ✅ | move to |
| L l | ✅ | line to |
| H h | ✅ | horizontal line to |
| V v | ✅ | vertical line to |
| C c S s | ✅ | cubic curve to |
| Q q T t | ✅ | quadratic curve to |
| A a | ✅ | arc to |
| z | ✅ | close path |
To learn how to use Pixie for realtime graphics with GPU, check out Boxy.
nim r tests/tests.nim
git clone https://github.com/treeform/pixie to run examples.
nim c -r examples/text.nim
var font = readFont("examples/data/Roboto-Regular_1.ttf")
font.size = 20
let text = "Typesetting is the arrangement and composition of text in graphic design and publishing in both digital and traditional medias."
image.fillText(font.typeset(text, vec2(180, 180)), translate(vec2(10, 10)))nim c -r examples/text_spans.nim
let typeface = readTypeface("examples/data/Ubuntu-Regular_1.ttf")
proc newFont(typeface: Typeface, size: float32, color: Color): Font =
result = newFont(typeface)
result.size = size
result.paint.color = color
let spans = @[
newSpan("verb [with object] ",
newFont(typeface, 12, color(0.78125, 0.78125, 0.78125, 1))),
newSpan("strallow\n", newFont(typeface, 36, color(0, 0, 0, 1))),
newSpan("\nstral·low\n", newFont(typeface, 13, color(0, 0.5, 0.953125, 1))),
newSpan("\n1. free (something) from restrictive restrictions \"the regulations are intended to strallow changes in public policy\" ",
newFont(typeface, 14, color(0.3125, 0.3125, 0.3125, 1)))
]
image.fillText(typeset(spans, vec2(180, 180)), translate(vec2(10, 10)))nim c -r examples/square.nim
let ctx = newContext(image)
ctx.fillStyle = rgba(255, 0, 0, 255)
let
pos = vec2(50, 50)
wh = vec2(100, 100)
ctx.fillRect(rect(pos, wh))nim c -r examples/line.nim
let ctx = newContext(image)
ctx.strokeStyle = "#FF5C00"
ctx.lineWidth = 10
let
start = vec2(25, 25)
stop = vec2(175, 175)
ctx.strokeSegment(segment(start, stop))nim c -r examples/rounded_rectangle.nim
let ctx = newContext(image)
ctx.fillStyle = rgba(0, 255, 0, 255)
let
pos = vec2(50, 50)
wh = vec2(100, 100)
r = 25.0
ctx.fillRoundedRect(rect(pos, wh), r)nim c -r examples/heart.nim
image.fillPath(
"""
M 20 60
A 40 40 90 0 1 100 60
A 40 40 90 0 1 180 60
Q 180 120 100 180
Q 20 120 20 60
z
""",
parseHtmlColor("#FC427B").rgba
)nim c -r examples/masking.nim
let ctx = newContext(lines)
ctx.strokeStyle = "#F8D1DD"
ctx.lineWidth = 30
ctx.strokeSegment(segment(vec2(25, 25), vec2(175, 175)))
ctx.strokeSegment(segment(vec2(25, 175), vec2(175, 25)))
mask.fillPath(
"""
M 20 60
A 40 40 90 0 1 100 60
A 40 40 90 0 1 180 60
Q 180 120 100 180
Q 20 120 20 60
z
""",
color(1, 1, 1, 1)
)
lines.draw(mask, blendMode = MaskBlend)
image.draw(lines)nim c -r examples/gradient.nim
let paint = newPaint(RadialGradientPaint)
paint.gradientHandlePositions = @[
vec2(100, 100),
vec2(200, 100),
vec2(100, 200)
]
paint.gradientStops = @[
ColorStop(color: color(1, 0, 0, 1), position: 0),
ColorStop(color: color(1, 0, 0, 0.15625), position: 1.0),
]
image.fillPath(
"""
M 20 60
A 40 40 90 0 1 100 60
A 40 40 90 0 1 180 60
Q 180 120 100 180
Q 20 120 20 60
z
""",
paint
)nim c -r examples/image_tiled.nim
let path = newPath()
path.polygon(
vec2(100, 100),
70,
sides = 8
)
let paint = newPaint(TiledImagePaint)
paint.image = readImage("examples/data/mandrill.png")
paint.imageMat = scale(vec2(0.08, 0.08))
image.fillPath(path, paint)nim c -r examples/shadow.nim
let path = newPath()
path.polygon(vec2(100, 100), 70, sides = 8)
let polygonImage = newImage(200, 200)
polygonImage.fillPath(path, rgba(255, 255, 255, 255))
let shadow = polygonImage.shadow(
offset = vec2(2, 2),
spread = 2,
blur = 10,
color = rgba(0, 0, 0, 200)
)
image.draw(shadow)
image.draw(polygonImage)nim c -r examples/blur.nim
let path = newPath()
path.polygon(vec2(100, 100), 70, sides = 6)
let mask = newImage(200, 200)
mask.fillPath(path, color(1, 1, 1, 1))
blur.blur(20)
blur.draw(mask, blendMode = MaskBlend)
image.draw(trees)
image.draw(blur)nim c -r examples/tiger.nim
let tiger = readImage("examples/data/tiger.svg")
image.draw(
tiger,
translate(vec2(100, 100)) *
scale(vec2(0.2, 0.2)) *
translate(vec2(-450, -450))
)











