fix(og): surface and prevent the missing libraqm text shaping - #10813
Conversation
The live OG card at api.anyplot.ai/og/home.png is pixel-identical to a render forced onto Pillow's BASIC layout engine, i.e. the deployed container's Pillow has no libraqm. Two consequences: the `ss02` stylistic set that turns MonoLisa Italic into its script forms is dropped from the `— any library.` line, and no text is kerned at all — including on the spec and collage cards, which use no features. `_draw_text_with_features` caught every exception and redrew plain text, so the endpoint kept returning a valid 200 PNG and nothing reached the logs. The only symptom was an off-brand image. - Narrow the fallback to the exceptions Pillow actually raises and log it; the missing-shaping case warns once per process instead of per text run. - Expose `has_text_shaping()` and log the capability at API startup. - Fail the API image build when Pillow lands without libraqm, so a wheel that loses it cannot ship silently. - Cover both paths in tests, including a render comparison that proves the swashes land when the feature is applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLkhDnKHdaqxT5pMwC38GX
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLkhDnKHdaqxT5pMwC38GX
There was a problem hiding this comment.
🟡 Changes recommended
The new “swashes land” test currently compares PNG bytes rather than asserting a pixel-level diff, which can allow false positives and weakens the regression guard this PR is meant to provide.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR prevents and surfaces a production regression where OG cards were rendered without kerning and without MonoLisa Italic ss02 swashes due to Pillow missing libraqm, by adding capability detection/logging and a Docker build-time guard.
Changes:
- Add
core.images.has_text_shaping()plus a logged, once-per-process warning path in_draw_text_with_features()when shaping/features are unavailable. - Log text shaping capability at API startup and fail the API Docker build if Pillow lacks libraqm.
- Add unit tests and documentation/changelog entries describing the shaping requirement and safeguards.
File summaries
| File | Description |
|---|---|
core/images.py |
Adds libraqm capability detection and non-silent fallback behavior for OpenType feature rendering. |
api/main.py |
Logs shaping capability once during app startup for fast diagnosis in Cloud Run logs. |
api/Dockerfile |
Adds a build-time assertion to prevent shipping an image where Pillow lacks libraqm. |
tests/unit/core/test_images.py |
Adds tests covering shaping detection, warning behavior, and home OG rendering differences. |
docs/reference/seo.md |
Documents the libraqm dependency and how to validate it. |
CHANGELOG.md |
Records the production OG-card regression and the safeguards added in this PR. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Copilot review: comparing encoded PNG bytes would pass on an encoder change even if the swashes were gone. Decode both renders and require a non-empty ImageChops difference, anchored below the eyebrow row so the diff has to sit in the tagline block the feature actually touches. Verified: diff box (308, 341, 953, 430) with ss02 applied, and None when both renders drop the feature — so the assertion fails on the regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLkhDnKHdaqxT5pMwC38GX
There was a problem hiding this comment.
🔵 Needs a closer look
The updated SEO documentation slightly overstates the “warn once” behavior and should be aligned with the actual logging semantics implemented in core.images.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
docs/reference/seo.md:345
- The docs say
core.images“logs a warning the first time a feature run is dropped”, but the code only warns once-per-process when text shaping (libraqm) is missing; when Pillow rejects a requested feature for a specific font, it logs a warning on each occurrence. Tighten the wording so it matches the actual behavior (and keeps the focus on the missing-libraqm guard).
- `api/Dockerfile` fails the build when the installed Pillow lacks libraqm.
- The API logs the capability at startup (`Text shaping (libraqm): available`,
or an error line when it is missing), and `core.images` logs a warning the
first time a feature run is dropped.
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
Summary
api.anyplot.ai/og/home.pngis pixel-identical to a local render forced onto Pillow'sBASIClayout engine (0 differing pixels; against the normal RAQM render it differs on every text line). The deployed container's Pillow therefore has no libraqm: thess02stylistic set that turns MonoLisa Italic into its script forms is dropped from the— any library.line, and no text is kerned at all — that part also affects the spec and collage cards, which request no features. MonoLisa itself loads fine; both TTFs ings://anyplot-static/fonts/return 200 and the wordmark metrics match MonoLisa, not the DejaVu fallback._draw_text_with_featurescaughtExceptionand silently redrew plain text, so the endpoint kept returning a valid 200 PNG and nothing reached the logs — the only symptom was an off-brand image. The fallback now catches the exceptions Pillow actually raises, logs them, and warns once per process (not per text run) when shaping is missing entirely.api/Dockerfilefails the build when the installed Pillow lacks libraqm, and the API logs the capability at startup via the newcore.images.has_text_shaping().Note: this makes the regression visible and prevents a future build from reintroducing it — it does not by itself repaint the currently deployed revision. The same lock installs a Pillow 12.3.0 manylinux wheel that does bundle libraqm (verified locally:
features.check('raqm') → True), so the running image is most likely older than the current lock; the next API deploy should restore the swashes, and the new startup log line says whether it did.Test plan
/verify-core—pytest tests/unit/api tests/unit/core→ 1005 passed;ruff check .clean;mypy core apicleanTestTextShapingcovers: feature pass-through when shaping is available, single warning + plain fallback when it is not, fallback when the font rejects the feature, and a render comparison proving the home card differs with vs. withoutss02(skipped where MonoLisa is not cached locally, since the DejaVu fallback has noss02)api.anyplot.ai/og/home.pngand compared it againstcreate_home_og_image()under RAQM (differs) and underBASIC(identical), which is what identifies the root causeapi/Dockerfile(no deploy from this session)Checklist
CHANGELOG.mdupdated under[Unreleased](### Fixed, bold-titled bullet, PR ref)docs/reference/seo.mdgains a "Text shaping (libraqm)" section under Branded OG images, plus the italic font variant in the font listGenerated by Claude Code