- 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.
The Window menu item from #11669 and the Azerbaijani catalog landed
together, so azerbaijani_covers_every_menu_label_and_path fails on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
#11005's new head adds path.toShape to the vector-path list next to
path.stroke; the batch's 95c7a5d17e had added it next to path.set. Both
merged cleanly into a duplicate or-pattern; keep the contributor's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
The bar draws inside the canvas pass, whose write-back of its own camera
dropped the fit that commit_crop asks for, so Done kept the old zoom. The
stored view now wins when the bar changed it, as a view menu row's does.
done_fits_the_cropped_document starts at 50 % and fails without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(export): preserve full JPEG chroma in Export As
* style(ui): simplify blend preview condition for Clippy
* test(export): verify full JPEG chroma at every quality
Exercise real JPEG export and decode at five qualities across U8, U16 and F32. Read all sampling factors from the main frame while skipping metadata segments and embedded thumbnails. Keep the colour-error limit at quality 100 and verify that the layered source is unchanged.
* Keep JPEG regression compatible with current Clippy
Restore the upstream blend-preview and IME predicates, removing unrelated lint-only changes. Use array chunks in the JPEG colour-error assertion for current stable Clippy.
* Use quality-dependent JPEG sampling in Export As
Keep 4:2:0 through quality 50 and full 4:4:4 above it, using the existing percentage-scale cutoff from Save for Web. Photoshop 27.11 modern Export As measurements show 4:2:0 at levels 1-4 and 4:4:4 at 5-7; no exact numerical mapping of the two scales is claimed.
Exercise both sampling regimes through 15 real JPEG exports across U8/U16/F32, check all percentage qualities in settings, and retain the Save As default. Boundary mutations fail at 50 and 51 as expected.
The automation registry check takes a `path` parameter for a host file
unless the command is listed as taking a document vector path, as the
other path commands are. path.toShape's `path` is the inline path being
drawn, so it belongs in that list; integration batch #11643 failed on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZbgszHyuEuWAzxaamteuU
Unit tests no longer read PHOTOCRAFT_UI_DIRECTION (cached per process, so a
developer's shell could flip the Hebrew/Urdu and shell assertions). The
Hebrew/Urdu test checks glyphs only when craft-fonts carries a face for the
script (the eager provider draws it in the first frame, as in CI with
CRAFT_FONTS_REQUIRED); without it the harness allows tofu and only the
mirroring is checked, instead of panicking on a host without the fonts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
- Draw the Crop task bar only on the active document's tile: a click into
another tile activates its document mid-pass, and the tile drawn after it
put the new frame's bar on itself for that frame.
- ui.inspect reports the bar hidden (and no rect) as soon as it can't show,
not only after the next draw.
- ui.set merges panels.task_bar field by field: {"at": [x, y]} keeps a pin,
{"pinned": true} keeps a dragged place; unknown nested fields are refused.
- Tests for each (each fails with its fix reverted), plus the task bar's
preset dropdown picking 1 : 1 in sync with the options bar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The automation registry check reads a `path` param as a host filesystem
path unless the command is listed as taking a document vector path, as
path.set is. #11005's new path.toShape takes an inline vector path, so it
joins that list; CI failed registry_filesystem_path_params_are_classified.
Co-Authored-By: DingusMcRinkyDinky <DingusMcRinkyDinky@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
- zoom field: a menu's I key drops İ/ı text whatever the UI language (the keyboard layout is
not the interface language).
- dropdown type-ahead: one entry per list tagged with its language, cleared on any frame in
another language, so a prefix never carries over a quick switch and back.
- Hebrew and Urdu: live shell test selects each catalog, checks mirroring and glyphs.
- Azerbaijani restart test resets the drawing language before the restarted shell loads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pinned central font manifest has no Tamil face (storytold/craft-fonts#63 adds one) and the
Linux runner has none installed, so the strict missing-glyph policy panicked on the Tamil
labels. Tamil keeps every label and state check; a guard fails once the manifest ships Taml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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.
file.export.layersToFiles gains groups: layers (walk every visible leaf at any depth, not only top-level layers) and naming: layer (bare <layer>.png names; duplicates get -2). This is the recursive folder export from the local Program Files build, brought in as the second fix on this branch.
Tested: layers_to_files_exports_group_children_as_loose_files (nested subgroup + sibling).
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.
The reference application's New Guide dialog has Orientation, Position
(a text field that takes typed units such as "2 cm") and Color, but no
unit dropdown; measured on 2026-11-06. Typed units already work on main,
so the PR only adds something the reference lacks.
This reverts merge 22e03d0bcb.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Linux runner has no GPU, so the GPU tests ran on Mesa llvmpipe over
GL, which can't render Rgba32Float: 57 GPU checks (50 GPU parity, 3
canvas, 2 adjustment preview parity, 2 effect-map recovery) returned
early and passed without comparing anything. A new step after Test
installs mesa-vulkan-drivers and runs the nextest `gpu` group on
lavapipe (Vulkan), reusing Test's build, and fails unless the parity
tests report the Vulkan backend and no test printed a skip.
No code change: photocraft's tests take whatever adapter wgpu returns,
software adapters included. Follows the CI half of lightcraft#1148.
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add an opt-in WinTab backend for pen pressure on Windows
Windows Ink stays the default. Preferences > Tools > Pen Tablet has a Tablet
input dropdown (Windows Ink (default) / WinTab). WinTab opens a WinTab 1.4
system context on PhotoCraft's window and polls pressure and the eraser end
(tilt on devices that report orientation axes; rotation only with a pen and
driver that report twist) into the stylus feed. The row says to turn Windows
Ink off in the tablet's software utility; the choice applies at next launch.
- crates/tablet: Windows-only wintab/wintab-sys backend. Devices with
orientation axes use the orientation packet; pressure-only devices open a
packet without it and report neutral tilt. Leaving is the driver's hardware
proximity message (WT_PROXIMITY) and the packet's out-of-context status, with
a long fallback for drivers that never report either. Overlap priority
follows window focus, and the message hook requests a frame for each WinTab
message so pressure-only strokes update without cursor movement.
- apps/photocraft: install the backend when the preference is on; poll it at
the start of each frame; the installed wake repaints the UI.
- ui-egui: TabletPollFn service; the Windows-only Pen Tablet row with the
Tablet input dropdown and the note.
- engine: tools.tabletInput (windowsInk/winTab), default windowsInk.
- docs: hardware-parity WinTab gap entry, qualifying tilt to orientation axes
and rotation to twist-reporting pens.
Tested locally on a Wacom tablet with Windows Ink off: pressure, tilt and the
eraser end reach the brush (rotation where the pen reports twist); the default
path is unchanged.
* WinTab: a message that finds the state borrowed is skipped, not a panic in the hook
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* WinTab: rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Scorecard: count the Tablet input setting
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>