- Render each shape cache clipped to its own bounds instead of the full
page clip.
- Copy embedded font bytes before locking the shared text engine and take
the lock per font, not for the whole registration loop.
- Track seen font content keys in a HashSet instead of scanning the font
list for every glyph; hash glyph text for fake-bold pairing instead of
cloning strings.
- Update the module and `pdf-editable` feature docs to describe text and
accepted no-op clips.
- Poll the cancellation watcher every 20 ms; Device callbacks only cover
paints, so a single long path still needs the watcher.
- Accept explicitly closed page-sized clip rectangles (m l l l l h).
- Pair fake-bold strokes through a text/font/order index instead of a
full scan, keeping the comparison cap and cancellation checks.
- Tests: solid zero-length stroke stays editable, explicit clip
rectangle.
- Count actual fake-bold comparisons toward the cap instead of the
product of glyph and stroke counts.
- Fall back for any dashed zero-length subpath (open or closed); test
covers a stroked line followed by a closed point.
- Handle invisible glyphs before the soft-mask check so hidden text
does not rasterise the page.
- Bound fake-bold glyph/stroke pairing by comparison count and check
cancellation during the scan.
- Keep both knots when a closing curve returns to its start; fall back
for a dashed single-point subpath.
- Skip inkless runs (zero-width/format characters) instead of treating
an empty cache as a render failure.
- Track soft masks and reject the page only when paint happens while one
is active.
- Tests: fake-bold mismatch across a path, closing curve loop.
- A text cache that renders empty now falls back the page instead of
leaving blank text.
- Fake-bold strokes pair only with fill glyphs at the same paint order.
- Retain single-point closed subpaths (round-cap dots) without emitting
duplicate phantom points; test added.
- Device methods documented; paint-order test covers text on both sides
of a path.
- Colours: AlphaColor components are straight, not premultiplied; use
them directly instead of dividing by alpha (the opacity test now
checks RGB, not just alpha).
- Text: key families by font content key, not PostScript name, so two
faces with one name cannot share a family. Keep text layers at the
path index where their glyphs were painted instead of appending them
after all paths.
- Correctness: reject bow-tie clip paths, start a new subpath after
close, fall back on unparseable/unsupported glyph mappings, truncated
content, failed font registration, and shape-cache budget exhaustion.
- Cancellation: check between text-cache renders as well as between
objects.
- Include Bezier control points in the cache-size estimate; cache the
content hash per Hayro font identity.
- Docs: ImportOptions/CLI help now describe editable text and the
embedded-font caveat. Tests: text cache is not blank, paint order,
bow-tie clip, line after close, cancellation between objects.
- Always suffix an embedded face's alias with a hash of its bytes, so the
same PostScript name from different documents cannot collide in the
process-wide shared engine; a face imported twice reuses its entry.
Registration is skipped when the alias or an installed full family is
already present.
- Release the shared text engine lock between layers so a text-heavy
import does not block the UI's layout for the whole cache loop.
- Test: two documents with one font name but different bytes get
distinct aliases and both families are present in the shared engine.
- Register imported fonts in photocraft_text::shared() so the app can
re-lay text after edits or DPI changes. Prefer an installed full
family, and register an embedded face under its PostScript name
(key-suffixed when two faces share it) so a subset cannot shadow it.
- Warn that text using PDF-embedded fonts may change font when edited or
reopened; the document format does not carry the bytes yet.
- Reject mirrored text and b == -c matrices; scale the tolerance by the
em size so 90-degree text stays editable. Tests for rotated and
mirrored text.
- Use pdfcer's 0.30 em line tolerance.
- Text: build one TextLayer per line/style run from the Device's glyph
draws. Derive the family from the PostScript name (subset tag, style
suffix, MT/PS, camel spacing); italic from the font flags/attributes
or name; bold from the weight or name; pair fill+stroke glyphs as fake
bold (Tr 2/6) instead of a second run.
- Require an embedded font with a PostScript name; standard-14/Type 1
substitutes and Type 3 text fall back to the raster page.
- Register embedded font bytes in the text engine, use the family the
font reports, and render text caches on import. Tests compare the
text composite with the raster page.
- Review fixes: accept a page-sized no-op clip; draw zero-width strokes
as 1 px hairlines and use 1 px as the dash base; reject non-uniform
stroke transforms; repeat odd dash arrays and keep zero dashes;
ExtGState ca/CA already reach the colour alpha (tested); note that
catch_unwind only guards unwinding profiles.
Add an experimental, opt-in PDF path importer on craft-pdf's interpret
Device. Fills and strokes become ShapeLayers in document pixels with
their real colours, widths and dash patterns; text, images, clipping
paths, transparency groups, patterns, soft masks and annotations keep
the raster page with a warning, so a page is never half imported.
- ImportOptions::pdf_editable (off by default) and --pdf-editable.
- pdf-editable Cargo feature in photocraft-io; the CLI enables it.
- Shape caches are rendered within a 256 MiB page budget.
- Cancellation is forwarded through a 5 ms watcher; hostile pages are
caught and fall back instead of panicking.
Tests cover fill/stroke colours and dashes, composite parity with the
raster page, text fallback and cancellation.
Raise the synthetic page to a million strokes so the 45 ms watcher
threshold keeps a wide margin on an optimised test build, and record why
the importer keeps one RenderCache per page.
- render_page: forward Interrupt to craft-pdf through an owned flag and a
scoped 5 ms watcher, so dense vector pages stop mid-render.
- Warn when craft-pdf reports a substituted font (the backend does not emit
the flag yet).
- /PageLabels: emit an entry for every page, start at 0, and write no tree
when every artboard name is empty.
- Export budget: charge retained page data plus the largest page's two f32
buffers instead of the whole document canvas, saturating on overflow.
- Render each artboard from a document holding only its group, so
overlapping boards no longer paint on each other's pages. One document
shell is cloned and reused.
- Tests: dense-page cancellation, labels with an empty middle name and all
empty, overlapping artboard pixels, sparse 9000x9000 canvas, seven US
Letter pages.
Conflicts: psd_import.rs (main's vector_preview field kept, the SmartObject
still built as a binding so pin_puppet_meshes runs), smart_cmds/rendering.rs
(main's JobCtx argument kept next to the PR's placed_puppet_params).
Co-Authored-By: Tungtono <Tungtono@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- --pdf-dpi now rejects zero, negative, NaN and infinite values at parse time (unit-tested) instead of silently falling back to 300 DPI.
- The RGBA8 raster is converted and written in 256-row bands with a cancellation check per band, so the f32 buffer no longer duplicates a whole page; the pixel-memory estimate charges the retained raster instead of that buffer.
- docs/file-format-parity.md: the 2026-11-07 revision row is inside the revision table.
- NOTICE now reproduces the Hayro MIT notice, the Foxit/PDFium BSD-3-Clause text, Adobe's CMaps BSD-3-Clause text and the CC0 statement for the ICC profile; package.ps1 ships NOTICE and ATTRIBUTION.md in both installers.
The module compiles for wasm (only its import entry point is stubbed), so the renderer imports and the native document types are gated; the web build no longer sees a dependency it does not have.
- Iterate the page sizes with their index instead of indexing the vector (clippy::needless_range_loop).
- Fix the .ai dispatch test's module path.
- Regenerate docs/scorecard.md after the merge (10 of 197 settings).
- craft-pdf (the shared Rust/Hayro reader in craft-libs, rev c5a1cbf) renders each page; no shared library is loaded or shipped, and every desktop platform works.
- Raster pages only: editable text/vector import and the pdf_text/pdf_vectors options are removed (they can return later through craft-pdf's interpret device). Page geometry, artboards, page range, budgets, cancellation and warnings are kept; the effective DPI now respects craft-pdf's 8192 px side and 64M px page limits.
- Drops pdfium-render, the PDFium download in CI, the build-script copy and the packaging/licence staging; records the Hayro/fonts/CMaps/ICC assets in ATTRIBUTION.md and NOTICE.
- \.ai\ files starting with %PDF- now take the PDF importer, matching the comment.
- Attribution, architecture and file-format-parity docs updated.
- A complete import makes a fixed number of cancellation checks; cancelling on the last per-object check and on the public boundary check must both return Cancelled and never a document.
- Six page variants (plain, CropBox offset, rotations 90/180/270, oversized CropBox) compare each stored page raster with a direct PDFium render at 72, 150 and 300 DPI: 18/18 byte-identical, and the sizes prove the crop intersection (190x80) and oversized CropBox (200x100) cases.
- Both tests skip without PDFium and fail under PHOTOCRAFT_REQUIRE_PDFIUM, like the existing end-to-end test.
The PDF export codec goes through photocraft-codecs; nothing in the crate used the image crate, so the dependency and its lockfile edge are removed. pdfium-render stays pinned exactly.
Every function this PR adds now has a doc comment, including the test functions in the importer, PDF export and layers_to_files suites. No behaviour change.
Module docs now state that pdfium-render 0.9.5 exposes path fill/stroke presence but not paint, width or dash (black paints, 1 pt stroke); that text colour is sampled and fonts are matched by family/name; and that only package.ps1 provisions PDFium, not the shared release workflow. The text and path extraction comments say the same where the code relies on it.
Raise docstring coverage on the functions this PR adds: the fixture/assertion helpers in the end-to-end test and the importer's test frame builders now have doc comments. No behaviour change.
- Map editable objects through the page's effective bounding box (media box clipped to the crop box) rather than the raw CropBox: an oversized CropBox no longer offsets text or paths from the rendered page. Covered by a fourth fixture page with CropBox (-50, -50, 250, 150) around a 200 x 100 MediaBox.
- Add a document-wide editable-object budget (20,000 layers, 1,000,000 path knots). A page that exceeds it keeps its raster, warns that its editable objects were omitted, and the unit test covers the accounting.
- Map editable text and paths through the page's crop box and /Rotate, matching the rendered raster: box offsets, 90/180/270 rotation, rotated text transforms and rotation-aware colour sampling.
- Check cancellation while traversing page objects and before returning the import; cap text colour samples at 4096.
- The cached PDFium failure now says to restart the application after fixing the library path.
- package.ps1 stages PDFium's LICENSE and bundled licence files flat for the MSI and the portable ZIP, tags the cache with the pinned release, and verifies freshly extracted DLLs.
- ci.yml provisions the pinned PDFium for the Windows integration leg and requires it there; local runs still skip explicitly.
Same reasoning as the wintab-sys pin in #11331: PDFium is FFI-loaded at run time, so a lockfile refresh must not move the wrapper under the pinned DLL release. Bump deliberately and re-check both the wrapper and the DLL release pin in packaging/windows/package.ps1.
- Process a closing segment's own geometry before marking the subpath closed, so a closing Bezier endpoint cannot leak control points into the next curve.
- Estimate retained memory from tile-aligned page bounds (including the page's X offset) instead of raw pixels: a 1 x 10,000 px page admits its 40 tiles, not 10,000 pixels.
- Sample text colour from the glyph ink (the pixel farthest from the box's dominant colour) instead of the bounds' top-left corner, which was usually background or antialiasing.
- Cache the PDFium bind: pdfium-render binds once per process, so a second PDF import in one session failed with PdfiumLibraryBindingsAlreadyInitialized.
- Add a PDFium-backed two-page end-to-end test with a transformed path on page 2 and non-origin red/green text at 72 and 300 dpi; it prints an explicit SKIPPED line when PDFium is unavailable.
- package.ps1: pin pdfium-binaries chromium/8138, verify the archive and any cached DLL against SHA-256, drop a stale bin/pdfium.dll before extraction and check tar's exit status.
- Paths on page 2+ were placed without the page's document X offset; text and paths now share PageFrame::to_doc, which applies the DPI scale, the Y flip and the page offset in one place.
- Flip Y in pixels and round once, so text placement, text colour sampling and path points all stay in the same coordinate system.
- Add unit tests for 72/300 dpi, the page-2 offset and the sampled colour; they need no PDFium.
- Apply PdfPageObject::matrix() to path segments: PDFium returns raw object-space points, so scaled or rotated paths landed in the wrong place.
- Flip path y with the page height in pixels, not points; paths were off-canvas at any DPI other than 72.
- Take fill and stroke presence from the path's draw mode: stroke-only paths no longer get a black fill, and paths that are neither filled nor stroked are skipped.
- Estimate import pixel memory as retained tiles (4 B/px over all pages) plus the converting page's peak (24 B/px), defaulting to a 2 GiB budget when the caller sets none. Reuse the RGBA copy and reserve the f32 buffer fallibly.
- Stage pdfium.dll for the MSI fixtures (install-policy payload) and for the real package
stage/portable zip; the package script downloads it before cargo.
- Regenerate docs/scorecard.md for the new Preferences fields.
- Text y positions convert the page height to pixels; invisible PDF text stays hidden.
- Page ranges that start past the PDF are refused; zero first/last bounds are independent.
- A PDF named .svg now keeps the PDF importer; other signatures beat the .svg suffix fallback (regression test).
- Text layers sample their fill colour from the rendered page and use the PDF font family/name.
- File Handling preferences expose pdf_dpi, pdf_first_page and pdf_last_page; the page range maps to ImportOptions.pdf_pages.
- ImportOptions gains pdf_pages (1-based inclusive), pdf_text and pdf_vectors;
the importer maps PDF text objects to editable text layers and vector paths to
shape layers through the effective DPI.
- photocraft-cli convert gains --pdf-dpi and --pdf-pages.
- Preferences gain pdf_dpi/pdf_pages, wired through jobs::import_options.
- apps/photocraft/build.rs copies PDFIUM_DLL beside the exe; the MSI installs it.
- Cancellation: the importer and the PDF writer take the Interrupt and check between pages.
- Physical size: the effective DPI keeps each page print size when budgets cap the raster.
- Oversized pages: final dimensions are checked before rendering.
- Memory: pages go straight into layer surfaces; the aggregate pixel estimate is admitted.
- Labels: /PageLabels (UTF-16BE) carry artboard names; no label is drawn outside the media box.
- Metadata: the artboard export warns when free-form text metadata is dropped.
- Export: hidden groups report their children; non-string modes are rejected; case-insensitive
filename collisions are suffixed.
- Dispatch: a recognised raster renamed .pdf goes to the flat importer; .ai keeps its path.
- Platform: the PDFium recovery message names the actual library.
The importer budgets the per-page raster (100 M px) and the artboard strip (200 M px): an
oversized page is scaled down, and pages that would exceed the strip budget are skipped with
a warning, instead of allocating a multi-gigabyte union canvas. The page-count cap stays 16.
Also applies cargo fmt to the new PDF code and the export-recursion test.
The PDF importer renders each page with PDFium at the import DPI (default 300), lays the
pages out side by side and wraps each in an artboard, so a 2-page PDF opens as 2 artboards
and saves back as a 2-page PDF (one page per artboard, page-sized, labelled). The import
caps at 16 pages for now.
The PDF writer now takes a list of pages: shared font/ICC/selection objects, one image per
page, and the existing one-page export is the single-page case. Artboards with vector/text
layers are rasterised in this path.
Tested: artboards_export_as_one_page_each; convert sample.pdf app-multi.pdf (2 pages),
rendered back with the probe.
File > Open, drag-and-drop and photocraft-cli now accept PDF files. The first page is
rendered with PDFium at 300 dpi (ImportOptions.pdf_dpi), built through the flat
importer, and the document resolution keeps the page's print size. The page count is
reported; multi-page import is the next step (#2541 / FILE-215-10).
- crates/io: pdf_import module, native-only pdfium-render + image deps, import and
preflight dispatch (.ai keeps its existing path).
- PDFium loads from the executable's folder or PDFIUM_DIR.