fix(assets): export-icon crops to the artwork; build refuses a page-sized icon viewBox (#205) #221

Merged
peterswimm merged 1 commit from fix/205-export-icon-artwork-bounds into main 2026-10-20 17:15:44 +00:00
Owner

For #205: ville's finding that export-icon exports the whole document.

Scope

Problem: export-icon.js exported the whole Affinity document. An icon drawn on A4 came out at 2481 × 3508, and assets.py preserves the source viewBox, so the icon shipped tiny inside a large transparent area. Nothing caught it.

Change:

  • export-icon.js exports the artwork's bounds.
    • It passes FileExportArea.createForSelection(…) to doc.export, built with Selection.create(doc, doc.layers, true) from every layer, so the designer's own selection is untouched.
    • A document with no artwork is refused.
    • The API is Affinity's own JSLib/document.js (Document.export(path, options, exportArea, size), FileExportArea.createForSelection), read from the installed Affinity.app.
  • The build refuses a loose icon.
    • New stdlib-only udg/generators/svg_bounds.py computes an over-approximated artwork box: control points, arc radii and composed transforms. Content under defs, clipPath and mask is skipped.
    • assets.py fails an icon whose artwork spans less than 75% of its viewBox in either dimension, with a message saying how to fix it.
    • Every committed icon spans 85% or more. Wireframes keep deliberate margins (down to 56%), so they're exempt.
    • Because the box only errs large, a tight icon can't be falsely refused.
  • README: the script row notes the crop and the check.

Testing

  • New generator/tests/unit/test_icon_viewbox.py:
    • every registered icon passes (19 cases);
    • the #205 regression: a real icon placed in a 2481 × 3508 page is refused;
    • transform composition, relative and implicit path commands, curve and arc extents, and non-painting content are ignored;
    • a static check that export-icon writes its file once and passes the export area. The scripts run only inside Affinity, so this follows test_affinity_scripts.py's precedent of checking the source.
  • Controls: with MIN_ICON_COVERAGE = 0.0, the A4 test fails; with the area argument removed from export-icon, the static test fails. Both restored afterwards.
  • python -m pytest generator/tests: 418 passed, 2 skipped.
  • assets.py check --all: ok.
  • Not tested: the script inside Affinity, which can't run here. Before merging, please run export-icon once on a small shape in an A4 document and confirm the SVG's viewBox fits the shape.

Impact Statement

  • Who is affected:
    • designers exporting icons, whose files come out cropped to the artwork;
    • anyone adding a loosely cropped icon, whose build now fails with a fix-it message.
  • Contract change: a new build rule for the icon category; no output changes for existing icons (check --all is clean).
  • Rollback: revert.
For #205: ville's finding that `export-icon` exports the whole document. ## Scope **Problem:** `export-icon.js` exported the whole Affinity document. An icon drawn on A4 came out at 2481 × 3508, and `assets.py` preserves the source viewBox, so the icon shipped tiny inside a large transparent area. Nothing caught it. **Change:** - **`export-icon.js` exports the artwork's bounds.** - It passes `FileExportArea.createForSelection(…)` to `doc.export`, built with `Selection.create(doc, doc.layers, true)` from every layer, so the designer's own selection is untouched. - A document with no artwork is refused. - The API is Affinity's own `JSLib/document.js` (`Document.export(path, options, exportArea, size)`, `FileExportArea.createForSelection`), read from the installed Affinity.app. - **The build refuses a loose icon.** - New stdlib-only `udg/generators/svg_bounds.py` computes an over-approximated artwork box: control points, arc radii and composed transforms. Content under `defs`, `clipPath` and `mask` is skipped. - `assets.py` fails an **icon** whose artwork spans less than 75% of its viewBox in either dimension, with a message saying how to fix it. - Every committed icon spans 85% or more. Wireframes keep deliberate margins (down to 56%), so they're exempt. - Because the box only errs large, a tight icon can't be falsely refused. - **README:** the script row notes the crop and the check. ## Testing - **New `generator/tests/unit/test_icon_viewbox.py`:** - every registered icon passes (19 cases); - **the #205 regression:** a real icon placed in a 2481 × 3508 page is refused; - transform composition, relative and implicit path commands, curve and arc extents, and non-painting content are ignored; - a static check that `export-icon` writes its file once and passes the export area. The scripts run only inside Affinity, so this follows `test_affinity_scripts.py`'s precedent of checking the source. - **Controls:** with `MIN_ICON_COVERAGE = 0.0`, the A4 test fails; with the area argument removed from `export-icon`, the static test fails. Both restored afterwards. - **`python -m pytest generator/tests`:** 418 passed, 2 skipped. - **`assets.py check --all`:** ok. - **Not tested:** the script inside Affinity, which can't run here. Before merging, please run `export-icon` once on a small shape in an A4 document and confirm the SVG's viewBox fits the shape. ## Impact Statement - **Who is affected:** - designers exporting icons, whose files come out cropped to the artwork; - anyone adding a loosely cropped icon, whose build now fails with a fix-it message. - **Contract change:** a new build rule for the `icon` category; no output changes for existing icons (`check --all` is clean). - **Rollback:** revert.
fix(assets): export-icon crops to the artwork, and the build refuses a page-sized icon viewBox (#205)
All checks were successful
PR policy check / policy-gate (pull_request) Successful in 0s
Security Scans / Trivy filesystem scan (pull_request) Successful in 59s
Block Validation / validate (pull_request) Successful in 2m3s
Security Scans / Gitleaks secret scan (pull_request) Successful in 1m3s
Test Generator / lint-workflows (pull_request) Successful in 36s
Test Generator / lint-prose (pull_request) Successful in 38s
Test Generator / test (pull_request) Successful in 1m30s
UDG dist drift gate / dist/ matches dtcg + udg sources (pull_request) Successful in 26s
Copy coverage / Design copy reaching rendered HTML (pull_request) Successful in 1m9s
1699d2b4b7
export-icon passes a FileExportArea built from every layer, so the SVG is the
artwork's bounds rather than the A4 page. assets.py fails an icon whose
artwork spans under 75% of its viewBox, measured by a stdlib over-approximated
bounding box (svg_bounds.py).
peterswimm deleted branch fix/205-export-icon-artwork-bounds 2026-10-20 17:15:45 +00:00
Sign in to join this conversation.
No description provided.