diff --git a/.claude/settings.json b/.claude/settings.json index 9e6afbe..6d5339c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,6 +10,10 @@ "Bash(python3 -m muriel.oklch:*)", "Bash(.venv/bin/python -m muriel.oklch:*)", "Bash(uv run python -m muriel.oklch:*)", + "Bash(python -m muriel.rasterize:*)", + "Bash(python3 -m muriel.rasterize:*)", + "Bash(.venv/bin/python -m muriel.rasterize:*)", + "Bash(uv run python -m muriel.rasterize:*)", "Bash(cairosvg:*)", "Bash(.venv/bin/cairosvg:*)", "Bash(python -m cairosvg:*)", diff --git a/CHANGELOG.md b/CHANGELOG.md index b3d4375..27b55a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ version numbers follow [Semantic Versioning](https://semver.org/). ### Added +- **`muriel rasterize` — SVG to PNG without Cairo.** The critique agent's + rasterize step called `cairosvg`, which on Windows installs and then fails + at import (no `libcairo-2.dll`). `muriel.rasterize` keeps cairosvg first + and falls back to ImageMagick 7 (`magick`), whose Windows build carries + librsvg and Cairo itself. Its librsvg reads only the first family of a + `font-family` stack and rounds weight 600 to Bold, so before the render + every stack is resolved against `magick -list font`. The CSS keywords muriel's + generators lead with (`ui-sans-serif`, `system-ui`, `ui-monospace`) + resolve to the faces a browser picks. On Windows, intermediate weights + become their named faces ("Segoe UI Semibold"), chosen by the CSS + weight-matching rule. Prints the backend, every substitution, leftover + font fallbacks, and a note when `` elements are present, since + ImageMagick's librsvg drops them. The critique agent and the repo's + permission allowlist now point at it. + - **`muriel.collection` accepts a clip's `video` for Wunderkammer's next release.** Wunderkammer's `feat/video-tiles` (4c86e79) adds an optional `video` to a clip: one http(s), `blob:` or relative URL, never `data:`, diff --git a/muriel/__main__.py b/muriel/__main__.py index 956ce41..22d1075 100644 --- a/muriel/__main__.py +++ b/muriel/__main__.py @@ -19,6 +19,7 @@ muriel diagram-check fig.svg # a11y + label + contrast gate muriel dimensions # print the dimensions registry muriel heroshot in.png out.png --tilt 12 + muriel rasterize fig.svg -o fig.png # SVG to PNG, cairosvg or ImageMagick muriel tilt-shift raw.png hero.png muriel venn spec.json out.png muriel styleguide examples/muriel-brand.toml --css @@ -60,6 +61,7 @@ "import-tokens": ("muriel.design_tokens_import", "Convert a DTCG / open-codesign tokens.json into a muriel brand.toml"), "import-corpus": ("muriel.tools.corpus_audit", "Audit a DESIGN.md corpus through design_md_import + 8:1 floor"), "palettes": ("muriel.palettes", "List / generate / validate categorical palettes (CVD + contrast)"), + "rasterize": ("muriel.rasterize", "SVG to PNG for a visual audit: cairosvg, else ImageMagick with fonts pinned to installed faces"), "smartcrop": ("muriel.tools.smartcrop", "Saliency-aware crop solver (edges + optional faces / text / saliency)"), "squint": ("muriel.squint", "Dimension-scaled blur ladder + thumbnail companions — does hierarchy survive degradation?"), "styleguide": ("muriel.styleguide", "Load a brand.toml; print / derive CSS / matplotlibrc / contrast audit"), diff --git a/muriel/rasterize.py b/muriel/rasterize.py new file mode 100644 index 0000000..ec57bb3 --- /dev/null +++ b/muriel/rasterize.py @@ -0,0 +1,624 @@ +""" +muriel.rasterize — SVG to PNG for a visual audit, on whatever renderer the box has. + +The critique agent judges a PNG, not SVG source, so every SVG audit starts +with a rasterize step. cairosvg (the ``[critique]`` extra) was the only +path, and on Windows it installs cleanly and then fails at import: it binds +the system Cairo library through cffi, and Windows ships no +``libcairo-2.dll``. ImageMagick's Windows build carries librsvg and Cairo +inside it, so ``magick`` renders SVG with nothing else installed. + +Backends, first that works wins: + +1. ``cairosvg`` — unchanged wherever it already works. An import that + raises OSError (installed, no Cairo library) counts as unavailable. +2. ``magick`` — ImageMagick 7's librsvg delegate, with fonts pinned first. + +Font pinning +------------ + +The librsvg that ImageMagick bundles (2.40) hands a whole ``font-family`` +value to Pango as one name. It does not walk a CSS stack: ``Inter, Arial, +sans-serif`` with Inter absent falls back to Pango's generic "Sans" with an +"expect ugly output" warning and never reaches Arial, and the CSS-only +keywords muriel's generators lead with (``ui-sans-serif``, ``system-ui``, +``ui-monospace``) match nothing. So before the render every +``font-family`` — attribute, inline style, ``" + 'A', root_attrs="")) + style = ET.fromstring(out).find(f"{{{SVG_NS}}}style") + self.assertIn("font-family: 'Segoe UI';", style.text) + self.assertEqual(_texts(out)[0].get("font-weight"), "600") + self.assertTrue(any("