Let a theme carry its own fonts, drawings and images (#343) #1070

Merged
tiagoagueda merged 1 commit from 343-theme-assets into main 2026-10-28 10:51:06 +00:00
Owner

A theme could not bring a file of its own: the renderer fetches nothing
(#163), and nothing turned a file in a plugin's package into the data:
address the rule asked for. Now {% load theme_assets %}{% theme_asset "path" %} writes one into the document before either renderer starts,
so WeasyPrint's data-only fetcher and Chromium's abort-all route stay
exactly as they were, and tests/security/test_pdf_fetching.py is
unchanged.

What the file is, read from its first bytes, decides what is written: an
SVG as markup, held to the allowlist core.pictures keeps Postulo's own
pictures to (no script, handler, foreignObject, image or outside
reference; to a fragment), with its

A theme could not bring a file of its own: the renderer fetches nothing (#163), and nothing turned a file in a plugin's package into the data: address the rule asked for. Now `{% load theme_assets %}{% theme_asset "path" %}` writes one into the document before either renderer starts, so WeasyPrint's data-only fetcher and Chromium's abort-all route stay exactly as they were, and tests/security/test_pdf_fetching.py is unchanged. What the file is, read from its first bytes, decides what is written: an SVG as markup, held to the allowlist core.pictures keeps Postulo's own pictures to (no script, handler, foreignObject, image or outside reference; <use href> to a fragment), with its <style> rules and style= attributes turned into presentation attributes by cssselect2 and its ids prefixed; a PNG, JPEG, WebP or GIF as a data: address; a WOFF2, WOFF, TTF or OTF font as an @font-face rule with a data: source. Attributes rather than a nonce on the drawing's <style>: an inline SVG's stylesheet is the whole document's, and could restyle the CV or carry @import and url(). The path is held to the plugin's own package through importlib.resources (a zipped package works): `..`, absolute paths, drives, backslashes, hidden files and symbolic links are refused, and registration sets Theme.package to the plugin's package whatever the plugin wrote. A file is limited to 1 MiB and a document's files to 3 MiB; one that is refused writes nothing and logs why. A theme declares the scripts each font draws (Theme.font_scripts), and the font's own character map is asked for the character check_fonts asks the server's fonts for (#74): a document in a script the font does not both declare and draw gets no @font-face, and is set in the server's fonts. check_fonts and Overview list each theme font and its scripts. The document preview's policy, and no other, adds `font-src data:`, so the preview draws the font the PDF has. The plugin surface gains load_theme_asset, ThemeAssetRefused and the two ceilings. Closes #343 ## Notes for review - **The maintainer's decisions (the issue's recommendations):** - Assets are inlined before drawing, and the fetchers stay closed. `tests/security/test_pdf_fetching.py` is unchanged. - `data:` fonts are allowed on the document preview's policy only: `FRAMEABLE_BY_OURSELVES` is renamed `PREVIEW_POLICY`, and only the two preview views use it. - There are ceilings per asset and per document: 1 MiB per file and 3 MiB per document, as constants on the plugin surface. - **Decisions the issue left open:** - An SVG's `<style>` and `style=` are turned into presentation attributes rather than given the nonce. CSS inside an inline SVG applies to the whole document and can carry `@import`/`url()`; attributes paint only their own element and look the same in the PDF. - A theme font problem is a warning in `check_fonts` and never changes its exit code. - **Dependencies:** `fonttools`, `tinycss2` and `cssselect2` are declared, because the new module imports them. All three already came with WeasyPrint. - **Wiki:** the *Writing a plugin* and *CVs* text is applied when this merges. `test_wiki_surface` skips in CI. - **Not run locally:** the WeasyPrint drawing test (no Pango) and the symlink test (Windows). CI runs both. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Let a theme carry its own fonts, drawings and images (#343)
Some checks failed
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m23s
CI / Unit tests and coverage (Python 3.14) (push) Failing after 14m41s
CI / Browser tests (Chromium) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
CI / Security audit (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Database tests on PostgreSQL (push) Has been cancelled
64430e17e6
A theme could not bring a file of its own: the renderer fetches nothing
(#163), and nothing turned a file in a plugin's package into the data:
address the rule asked for. Now `{% load theme_assets %}{% theme_asset
"path" %}` writes one into the document before either renderer starts,
so WeasyPrint's data-only fetcher and Chromium's abort-all route stay
exactly as they were, and tests/security/test_pdf_fetching.py is
unchanged.

What the file is, read from its first bytes, decides what is written: an
SVG as markup, held to the allowlist core.pictures keeps Postulo's own
pictures to (no script, handler, foreignObject, image or outside
reference; <use href> to a fragment), with its <style> rules and style=
attributes turned into presentation attributes by cssselect2 and its ids
prefixed; a PNG, JPEG, WebP or GIF as a data: address; a WOFF2, WOFF, TTF
or OTF font as an @font-face rule with a data: source. Attributes rather
than a nonce on the drawing's <style>: an inline SVG's stylesheet is the
whole document's, and could restyle the CV or carry @import and url().

The path is held to the plugin's own package through importlib.resources
(a zipped package works): `..`, absolute paths, drives, backslashes,
hidden files and symbolic links are refused, and registration sets
Theme.package to the plugin's package whatever the plugin wrote. A file
is limited to 1 MiB and a document's files to 3 MiB; one that is refused
writes nothing and logs why.

A theme declares the scripts each font draws (Theme.font_scripts), and
the font's own character map is asked for the character check_fonts asks
the server's fonts for (#74): a document in a script the font does not
both declare and draw gets no @font-face, and is set in the server's
fonts. check_fonts and Overview list each theme font and its scripts.

The document preview's policy, and no other, adds `font-src data:`, so
the preview draws the font the PDF has. The plugin surface gains
load_theme_asset, ThemeAssetRefused and the two ceilings.

Closes #343

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tiagoagueda force-pushed 343-theme-assets from 64430e17e6
Some checks failed
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m23s
CI / Unit tests and coverage (Python 3.14) (push) Failing after 14m41s
CI / Browser tests (Chromium) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
CI / Security audit (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
CI / Database tests on PostgreSQL (push) Has been cancelled
to 36c64afa88
Some checks failed
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m11s
CI / Unit tests and coverage (Python 3.14) (push) Successful in 13m19s
CI / Database tests on PostgreSQL (push) Successful in 4m7s
CI / Security audit (push) Successful in 1m2s
CI / Browser tests (Chromium) (push) Successful in 34m2s
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
2026-10-28 00:08:50 +00:00
Compare
tiagoagueda force-pushed 343-theme-assets from 36c64afa88
Some checks failed
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m11s
CI / Unit tests and coverage (Python 3.14) (push) Successful in 13m19s
CI / Database tests on PostgreSQL (push) Successful in 4m7s
CI / Security audit (push) Successful in 1m2s
CI / Browser tests (Chromium) (push) Successful in 34m2s
Every Python / Unit tests (Python 3.13) (push) Has been cancelled
Every Python / Unit tests (Python 3.12) (push) Has been cancelled
CI / Browser tests (Chromium) (pull_request) Has been cancelled
CI / Unit tests and coverage (Python 3.14) (pull_request) Has been cancelled
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Has been cancelled
CI / Database tests on PostgreSQL (pull_request) Has been cancelled
CI / Security audit (pull_request) Has been cancelled
to 562f86c917
All checks were successful
CI / Checks: lint, migrations, catalogues, settings, built files (push) Successful in 3m16s
CI / Database tests on PostgreSQL (push) Successful in 4m18s
CI / Security audit (push) Successful in 1m14s
Every Python / Unit tests (Python 3.12) (push) Successful in 13m57s
CI / Browser tests (Chromium) (push) Successful in 34m8s
CI / Checks: lint, migrations, catalogues, settings, built files (pull_request) Successful in 3m21s
CI / Security audit (pull_request) Successful in 1m4s
Every Python / Unit tests (Python 3.13) (push) Successful in 14m34s
CI / Unit tests and coverage (Python 3.14) (pull_request) Successful in 13m13s
CI / Database tests on PostgreSQL (pull_request) Successful in 4m13s
CI / Browser tests (Chromium) (pull_request) Successful in 32m36s
CI / Unit tests and coverage (Python 3.14) (push) Successful in 16m17s
2026-10-28 10:44:22 +00:00
Compare
tiagoagueda deleted branch 343-theme-assets 2026-10-28 10:51:08 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo!1070
No description provided.