Skip to content

feat(rasterize): SVG to PNG without Cairo, fonts pinned for ImageMagick - #7

Open
andyed wants to merge 4 commits into
mainfrom
feat/rasterize-magick-fallback
Open

andyed wants to merge 4 commits into
mainfrom
feat/rasterize-magick-fallback

Conversation

@andyed

@andyed andyed commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Why

The critique agent rasterized SVGs with cairosvg. On Windows, cairosvg installs and then fails at import because Windows has no libcairo-2.dll, so SVG audits there lose their visual pass.

What

python -m muriel.rasterize in.svg -o out.png (also muriel rasterize):

  1. cairosvg first, where it imports. An OSError at import (installed, no Cairo) counts as unavailable, not as a crash.
  2. ImageMagick 7 (magick) otherwise. Its Windows build carries librsvg and Cairo itself, so it needs nothing else installed.

The ImageMagick rung runs a font-pinning pass first, because its librsvg 2.40 has two problems with muriel's output:

  • It tries only the first family of a font-family stack. muriel's generators lead with ui-sans-serif, -apple-system, system-ui, …, which never match, so every label fell back to a generic "Sans" with an expect ugly output warning. Stacks now resolve against magick -list font (attributes, inline styles, <style> sheets), and the CSS keywords map to the faces a browser picks (system-ui → Segoe UI, monospace → Consolas).
  • It rounds intermediate weights to the nearest of 400 and 700, so the 600 on muriel's titles and axis labels rendered Bold. On Windows, each element that declares a font is rewritten to name the face directly ("Segoe UI Semibold" at weight 400), chosen by the CSS weight-matching rule. Weight pinning stands down when a <style> sheet sets fonts, or when the family lists only one face (a variable font).

The output reports the backend, each substitution with counts, any font fallbacks left in the render, and a note when the SVG has <image> elements. ImageMagick's librsvg drops or discolors those, so the note points to muriel capture (Chromium).

On boxes where cairosvg works, output is unchanged: pinning applies only on the ImageMagick rung.

Also updated: the critique agent now calls python -m muriel.rasterize instead of cairosvg, with matching changes to the svg channel doc, the squint docstring, the repo's .claude/settings.json allowlist, and CHANGELOG.

Test plan

  • tests/test_rasterize.py: 40 tests. Pinning runs against a fixture font list, so it works on any OS. The ImageMagick render tests skip without magick.
  • Full suite on Windows: 873 passed, 4 skipped.
  • Rendered comparison-pair-serp.svg and comparison-pair-trace.svg on Windows. Unpinned, the headings fell back to a wide bold Sans (5 fallback warnings). Pinned, they render in Segoe UI Semibold and the mono cells in Consolas, with no warnings.
  • Not exercised: the cairosvg rung on macOS or Linux (this box has no Cairo). That path only adds the OSError catch around the existing call.

🤖 Generated with Claude Code

andyed and others added 4 commits October 15, 2026 21:01
The critique agent rasterized SVGs with cairosvg, which on Windows
installs and then fails at import because there is no libcairo-2.dll.
muriel.rasterize keeps cairosvg first and falls back to ImageMagick 7,
whose Windows build carries librsvg and Cairo itself.

ImageMagick's librsvg 2.40 tries only the first family of a font stack
and rounds weight 600 to Bold. So before the render each stack is
resolved against `magick -list font`, with ui-sans-serif / system-ui /
ui-monospace mapped to the faces a browser picks. On Windows,
intermediate weights become their named faces ("Segoe UI Semibold"),
chosen by the CSS weight-matching rule. The CLI reports the backend,
each substitution, leftover font fallbacks, and a note when <image>
elements are present, since this librsvg drops them.

The critique agent, svg channel, and repo permission allowlist now
point at `python -m muriel.rasterize`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in CHANGELOG.md only: both sides added entries at the top of
Unreleased/Added. Kept all of them, the rasterize entry first.
muriel/__main__.py merged cleanly (rasterize and collection both
registered).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in CHANGELOG.md only: main added the sparklines entry at the
top of Unreleased/Added. Kept both, the rasterize entry first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in CHANGELOG.md only: main added the human-capabilities,
semantic-zoom and node-CI entries at the top of Unreleased/Added. Kept
all of them, the rasterize entry first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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