Skip to content

raster: add pure-Go PNG, GIF, PDF, and PPTX rendering - #2868

Merged
alixander merged 3 commits into
masterfrom
agent/native-raster-renderer
Sep 3, 2026
Merged

raster: add pure-Go PNG, GIF, PDF, and PPTX rendering#2868
alixander merged 3 commits into
masterfrom
agent/native-raster-renderer

Conversation

@alixander

@alixander alixander commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Human

  • get rid of playwright dependency for rendering png (and derivatives e.g. gifs/pdfs)
  • previously this was done by using a headless browser to capture what the svg looks like on your machine
  • playwright needs chromium, and has been a huge PITA in the installation path of D2 on various envs
  • different chromium versions, different OS's, all these would make png's inconsistent
  • instead we embed a small emoji set, and rasterize the svg shapes/fonts directly
  • i expect there to be corner cases to handle

AI

Adds native Go rendering for PNG, GIF, PDF, and PPTX exports.

The implementation has four main layers:

  • d2scene is the typed drawing model shared by renderers.
  • d2scenebuild converts compiled D2 diagrams into scenes, including text, links, effects, assets, and animation.
  • d2raster paints scenes with an analytic scanline rasterizer.
  • d2svgimport parses the bounded SVG subset used by image assets and MathJax output.

PNG renders one frame. GIF preserves animation timing. PDF and PPTX preserve boards and links. Missing images render a deterministic placeholder in their declared box; JPEG assets honor EXIF orientations 1–8.

The renderer bounds canvas size, path work, filters, decoded assets, fonts, animation, multi-board exports, and encoded output before writing a result. Rendering and encoding honor cancellation throughout.

Text and assets

  • Text uses pure-Go shaping with bidi, script, ligature, mark, and ordered fallback support.
  • Emoji fallback uses the pinned Noto Color Emoji 2.051 COLRv1 font. Its compressed and decoded payloads are authenticated and loaded lazily only when needed.
  • MathJax's U+00B5 fallback is a small D2-authored vector glyph.
  • Paper, grain, and sketch-streak textures have one canonical source shared by SVG and scene rendering.
  • Canonical W3C SVG 1.0 and SVG 1.1 external doctypes are accepted without fetching or applying DTD content. Other declarations and entity expansion remain rejected.

CLI and packaging

The existing CLI output-format behavior directly selects the appropriate renderer; there is no backend flag or additional command. The release container no longer installs Node.js or Playwright because those packages are not runtime dependencies of these export formats.

The stripped CGO_ENABLED=0 binary is 36,849,058 bytes on darwin/arm64: 3,223,824 bytes (9.59%) larger than current master and 679,776 bytes smaller than the previous PR revision.

Performance

Operation-scoped workspaces reuse frame, scanline, offscreen, shaping, PNG, and GIF storage without retaining caller-owned inputs or outputs. Representative Apple M4 measurements include:

  • repeated 512x384 rendering: about 0.78 ms and 14 KB allocated with workspace reuse, versus 0.98 ms and 2.26 MB for owned frames
  • 512x512 PNG encoding: about 0.75 ms for flat images, 2.05 ms for gradients, and 7.95 ms for noisy images, versus 1.15 ms, 3.1 ms, and 19.1 ms with image/png
  • 30-frame GIF encoding: about 8.8 ms and 1.05 MB allocated, versus 9.2 ms and 3.26 MB with image/gif

Fast paths retain exact byte-output tests against the standard encoders and scalar raster oracles.

Validation

  • complete CGO_ENABLED=0 go test ./...
  • complete CGO_ENABLED=1 go test -race ./..., followed by focused exact-head race tests after the final optimizations
  • go vet ./..., go mod verify, and a clean go mod tidy -diff
  • WebAssembly, linux/386, and linux/s390x compilation
  • repeated exact-output, cancellation, ownership, limit, font, PNG, and GIF tests
  • all pull-request CI, release-archive, and Linux/arm64 Docker smoke jobs
  • Playwright comparison of 624 E2E cases and 670 frames; 666 native renders, with the other four rejected by the documented 67,108,864-pixel safety limit
  • four large animated examples totaling 660 native GIF frames and 16 PDF pages; page structure matched and regenerated pixels were unchanged after the performance work

@alixander
alixander force-pushed the agent/native-raster-renderer branch 2 times, most recently from 175cc21 to 705d465 Compare August 29, 2026 23:23
@alixander alixander changed the title raster: replace browser exports with a pure-Go renderer raster: add pure-Go PNG, GIF, PDF, and PPTX rendering Aug 29, 2026
@alixander
alixander force-pushed the agent/native-raster-renderer branch 8 times, most recently from d935139 to 6f14deb Compare September 2, 2026 00:34
@alixander
alixander force-pushed the agent/native-raster-renderer branch from 6f14deb to 235c34a Compare September 2, 2026 00:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant