mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 18:33:22 +00:00
refactor/shared-pdf-reader
122 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60a82e858f | M2: route PDF reading through shared craft-pdf | ||
|
|
3f044b6263 |
fix(print): adopt native Windows spooler
Adopt the canonical craft-print revision from craft-libs #28 so Windows printing uses the in-process Windows.Data.Pdf/XPS backend instead of PowerShell. Keep exact paper, orientation and scaling validation and the existing PDFCraft composition and permission checks. Pin the reviewed winprint fork, scope its compile-time licence/advisory exceptions, and distinguish the earlier WDAC Sandbox exercise from the final pinned revision's Windows CI coverage. Physical printer output remains unverified. Add worker-barrier regressions for Home-selected tool handoff and startup document ordering. Fixes #1107 Co-authored-by: durtyrobbin <76844303+durtyrobbin@users.noreply.github.com> Co-authored-by: Dimitar Klaturov <7221384+bulgariamitko@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: skylar taylor-barrick <hi@skylar.cc> |
||
|
|
890b83c4c4 |
Add reviewable text-only PDF auto-tagging (#1440)
Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
a0bb1f08a4 | Export all images: CMYK JPEGs keep their colours instead of coming out as negatives (fixes #1640) (#1642) | ||
|
|
c0c40acf22 |
Save: fall back to a plain fsync where a volume refuses the full flush (fixes #981) (#1626)
On macOS sync_all is F_FULLFSYNC, which SMB and FUSE volumes such as a mounted Cryptomator vault refuse with ENOTSUP or ENOTTY, so every save there failed. Those two errors now fall back to fsync; any other flush error still fails the save. |
||
|
|
7a0f846a28 |
M13: Run conversion, OCR and optimization over folder trees (#1605)
Add a shared CLI/MCP batch runner with bounded discovery, retained relative paths and per-file processing in isolated engine sessions. Publish complete outputs without overwriting existing names, and return structured errors with an optional stop-on-error policy. Cover recursive jobs, overlapping inputs, concurrent output collisions, root confinement and partial-failure reporting in automation and CLI tests. Co-authored-by: LabDennis <341377186+LabDennis@users.noreply.github.com> |
||
|
|
13fbb2f6fb |
M0: label local builds in --version, About and the package release (#1529)
A build compiled with PDFCRAFT_BUILD_LABEL=<label> shows "0.7.1 (<label>)"
in `pdfcraft --version`, `pdfcraft-cli --version`, Help > About and the
first log line (pdfcraft_engine::build_info). The version itself is
unchanged, so the update check, the MCP server info and the User-Agent
still carry the release version.
packaging/linux/package.sh puts the label in the file names
(pdfcraft-0.7.1-<label>-linux-x86_64.rpm) and makes the .rpm/.deb release
`1.<label>` (nfpm `release: ${RELEASE}`, 1 when unlabelled). rpm sorts
0.7.1-1.<label> after 0.7.1-1 and before 0.7.2-1, so a locally built
package upgrades over the installed release and the next release replaces
it. packaging/env.sh accepts only letters, digits and inner dots, which
are valid in both rpm and deb release fields.
Unlabelled builds are unchanged.
Made with Claude Code (Claude Opus 5.5, medium effort). It passes the
workspace quality gates (fmt, clippy, cargo test --workspace, xtask assets)
and was checked by hand on Fedora 44, but it has not been exhaustively
regression tested.
Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
7163ba3dee |
Release 0.7.1: rebuild with the fixed shared release workflow
The previous release build failed in craft-common-actions, not in this repo: riscv64 packaging got --formats deb,rpm,tar (fixed in craft-common-actions#3), Linux jobs lacked the ALSA headers (craft-common-actions#5), and Windows signing needed the AZURE_* secrets listed in the caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
78f00e45fe |
Release prep: 0.7.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9d6552291b |
Issue #1296: hayro-syntax applies SASLprep to AES-256 passwords (PdfCraft patch 16) (#1345)
Revisions 5-6 of the standard security handler key the file on the
password processed with SASLprep (ISO 32000-2 7.6.4.3.3, Algorithm 2.A
step a). pdfcraft-crypt writes and reads it so, but the vendored
hayro-syntax skipped the step ("TODO: Do the above.") and used the UTF-8
bytes as typed. A password that SASLprep changes (a decomposed "cafe" +
U+0301, a no-break space, fullwidth letters typed by an IME in full-width
mode) was wrong for the renderer: Protect with such an open password was
rolled back ("the password is incorrect"), and an AES-256 file with such a
password did not open in a session with it, although pdfcraft-cos
accepted it.
- hayro-syntax (vendored patch 16) tries an R5/R6 password in its SASLprep
form (as pdfcraft-crypt computes it: as given if SASLprep refuses it,
UTF-8, at most 127 bytes), then, when that differs, as given.
stringprep, already in the tree through pdfcraft-crypt, is an optional
dependency enabled by the std feature; without std nothing changes.
- Tests: hayro-syntax crypto::pdfcraft_password_tests (candidate list and
R5/R6 dictionaries built from prepared passwords) and two engine tests
(Protect and reopen, and pdfcraft-cos-encrypted files) for the three
cases, with precomposed "caf\u{E9}" as the control. The dictionary test
and both engine tests fail without the fix.
Fixes #1296.
|
||
|
|
4541974ef9 |
Reading colours: replace document colours for reading comfort (#897) (#1270)
* test: give faux-redaction audit its own workdir (parallel-test flake) * hayro: ColorReplacement type and luminance mapping (reading colours, #897) * fmt: engine dates (upstream Greek localization miss) * hayro: apply ColorReplacement to solid text and line-art paints (#897) * render: ReadingColors view setting flows into RenderSettings (#897) * engine: per-document reading colours state (#897) * automation: view_reading_colors tool; page_render honours it (#897) * ui: Reading colours preference (Replace Document Colors, #897) * ui: seed reading colours on revision and signed-version opens (#897) * parity: a11y.replace-page-colors shipped (#897) * render: map stencil-mask paints through reading colours (#897) An ImageMask stencil's solid paint was baked into the raster bytes raw, bypassing the reading-colours hook every other solid fill goes through: under white-on-black, CCITT scans, JBIG2 text and stencil logos drew black ink on the black mapped background and vanished. Route it through replaced(paint, false) — stencils are line art in disguise — and cover it with an ImageMask fixture test sampling mapped ink on a mapped background. * render: print previews render with reading colours off (#897) Print previews went through the document's render pool, whose config now carries the reading colours: a white-on-black user saw black sheets in the Print dialog while the actual print stayed uncoloured. The request tag is the per-request channel that already crosses the render boundary, so PRINT_TAG now lives in pdfcraft_render and the renderer honours it by rendering previews with reading colours off — a preview's job is fidelity to the printout. The main view stays recoloured. Covered on both render paths (inline PageRenderer and pool worker). * ui: seed reading colours on created-document opens (#897) open_created (comment summaries) and open_created_bytes (images, text, blank pages, clipboard) pushed new DocViews without seeding the reading colours, unlike every other open path. A created document now starts with the app-wide preference too; the seed-on-open test drives both paths. * docs: reading-colours flag echo and display-fidelity parity notes (#897) The view_reading_colors description now says a flag that differs from the mode's preset default makes the echoed mode read custom, and the a11y.replace-page-colors parity notes record that print previews and view snapshots render with the document's true colours: reading colours apply to the reading view only. * Merge follow-ups: reading colours beside preview renders and the new catalogs - Renderer::new carries both color_replacement and the new preview flag - our raster tests construct RenderRequest with preview: false - the preference dialog grew past the window: scroll the combo into view and give the fixture a valid xref so no repair banner covers the dialog - checkboxes use the crate's square-box widget (clippy disallows egui's) - Kazakh and Turkish catalogs gain the ten reading-colours strings * Reading colours: record the hayro patch in vendor/README.md; add missing translations vendor/README.md gains hayro patch (13): `RenderSettings::color_replacement` / `ColorReplacement`, the `render_pdf` signature change and the removed `[dev-dependencies.log]`, with its tests. Adds the ten reading-colours labels to el, es, pl, te and zh-hant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8cacb44728 |
M3: follow Windows' Animation effects setting; add an Animation effects preference (#1235)
* M3: follow Windows' Animation effects setting; Smooth scrolling preference With Settings > Accessibility > Visual effects > Animation effects turned off, the wheel still eased: egui spreads a coarse wheel step over about a quarter of a second, for scrolling and for Ctrl-wheel zooming, and has no switch for it. - crates/ui-egui/src/motion.rs reads each frame's wheel input the way egui does, minus the easing, and hands it to the scroll areas and the views (zoom_delta). A test sums what egui eases and compares it with what is read at once, and fails if egui stops easing. - With the system asking for no animations, egui's own animations take no time and the progress card's bar steps without its sweep. - The desktop app asks WinRT's UISettings.AnimationsEnabled at launch and every two seconds of use, so a change applies without a restart. - Preferences has Smooth scrolling: Use system setting (default), On, Off. On or Off decides the wheel's scrolling and zooming, and scrolling to a place, whatever the system says. Saved, settable as smooth_scrolling on the control channel, translated in all 20 catalogs. How far a notch scrolls or zooms is unchanged. macOS, Linux and the browser aren't asked for their setting yet; Off works everywhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Credit Jeremy Yao by name in About (contributors/people.toml) My own entry: real name and display name "Jeremy Yao". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M3: rename Smooth scrolling to Animation effects; it decides every animation The preference also governs Ctrl-wheel zooming, so "Smooth scrolling" described only part of it. As "Animation effects" (Use system setting, On, Off) it now decides every animation in the window, the way the Windows setting of the same name does: - the wheel's scrolling and zooming, and scrolling to a place (as before); - egui's own animations, the progress card's sweep, and the chrome's hover and press washes (new on main, with durations of their own), which used to follow the system alone or not at all. Saved and set on the control channel as animation_effects (system, on, off); the unreleased smooth_scrolling name is gone. The three labels are retranslated in all 20 catalogs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: the app's two real-GPU tests take turns (FreeBSD SIGSEGV) FreeBSD CI died with SIGSEGV in the pdfcraft binary's tests on this branch ( |
||
|
|
26a68d9765 |
Units: read the Windows measurement system from the registry, not reg.exe (#1227)
The desktop app started `reg.exe query "HKCU\Control Panel\International" /v iMeasure` at every launch to pick the default Units (#463). On managed PCs reg.exe is a process EDR and WDAC/AppLocker setups watch, and a Sandbox process-creation audit showed it as the one child process left at startup. Read the value in-process with winsafe's safe RegGetValue (advapi feature) instead. Behaviour is unchanged: "1" is inches, "0" is millimetres, anything missing or unreadable falls through to the locale check. The value-to-unit mapping is a pure function with a unit test, plus a Windows test that the registry read yields a known value. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7caaa35a81 |
Fix Word invoice layout and pagination (#773) (#1126)
* M10: keep invoice overlays out of Word body flow (#773) Export inferred tables without visible borders. Keep text and images positioned on pages with artwork, rotated or large text, and retain each source page's size, crop rotation and units. Cover invoice conversion through the automation tools, and handle unavailable Windows symlink setup in the confinement test. Co-authored-by: Codex <noreply@openai.com> * M10: add @lollinoo's chosen contributor credit (#773) The contributor requested their public GitHub profile name while preparing the #773 PR. Co-authored-by: Codex <noreply@openai.com> * Word export: float a page only when text overlaps an image or watermark positioned() turned every page with any image, or with text twice the body size, into absolutely positioned VML text boxes, so a page with a logo or a big title lost its flowing paragraphs, headings and tables. Now a page floats only when it has rotated text, or when an image or a watermark-sized text block actually overlaps other text (bounded pairwise test). Other pages flow with images inline. Adds a regression test for a page whose image and title sit clear of the text, and updates the docs and parity notes. --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
6d843a8e8b |
Preserve source fonts in text editing and resize clipped artwork (#1116)
* M0: remove upstream trademark artwork from contributor fork
Use the existing document icon and a plain project label. Remove trademark
asset entries and regenerate attribution as required for modified forks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: group compatible PDF words and hide source text during editing
Join compatible words across text objects and subset-font spaces, recognize
component colours, and reuse subset fonts with numeric space positioning.
Render a read-only background with the selected glyphs removed while the
inline editor is open. Keep one frame and source line breaks, and reset
the layout width when switching paragraphs. Expose the same preview through page_render.
Add synthetic extraction, save/reopen, preview and UI regressions. Record
the remaining mixed-style paragraph and installed-font limitations.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: preserve rich PDF regions and preview text edits in place
Group mixed source runs into complete visual regions, retain run styles and source encodings, and use validated embedded source faces in the inline editor. Preserve source baselines and show a single live frame as text grows or shrinks, backed by a read-only draft geometry tool.
Validation: strict workspace Clippy, workspace tests, all 30 WASM crates, asset and parity gates, licensed synthetic edit/save/reopen and desktop checks, and an opt-in four-page private-document regression.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: keep cropped artwork from blocking text editing
Track page/path clipping separately from full image placement, use visible selection bounds, and prioritize text hit testing over overlapping artwork. Preserve fixed clips during image transformations and expose visible bounds through the headless tools.
Validation: strict workspace Clippy, workspace tests, 30 WASM crates, asset/parity gates, synthetic engine/tool/UI and native desktop checks, both private-document UI regressions, and all-page local edit/save/reopen/undo on a four-page cropped-artwork PDF.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: resize cropped images with their clipping paths
Keep the visible frame and image pixels under the same transform. Preserve the
opposite corner and respond to diagonal, horizontal-only and vertical-only
corner drags without jumping from an off-centre grip. Refuse shared clipping
scopes that would move unrelated content.
Add synthetic engine/tool/UI regressions and opt-in private-file coverage
for 80 corner gestures and 10 grow/shrink save/reopen/undo render checks.
Co-authored-by: OpenAI Codex <codex@openai.com>
* M7: complete source font subsets and preserve clipped editing
Add explicit document fonts, CFF embedding, range formatting and bounded contextual shaping; isolate shared image clips and verify private Chinese edits on all pages.
Co-authored-by: OpenAI Codex <codex@openai.com>
* Revert "M0: remove upstream trademark artwork from contributor fork"
This reverts commit
|
||
|
|
f3571c591f |
Merge pull request #1015 from storytold/codex/craft-print
M10.5: Adopt craft-print spooler with Windows printing fixes |
||
|
|
4a1e205f81 |
Accessibility: export before-and-after review record (#1204)
* Add accessibility before-and-after review record tool * Parity: leave out the review record (the parity list tracks Acrobat features; this is a PdfCraft extension) --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
00184a1006 | M10.5: Preserve main duplex-driver warnings through craft-print | ||
|
|
c5f1cb2d3f | M10.5: Reconcile hosted CI and keep the Arabic golden in both font builds | ||
|
|
8ee3673bd4 |
M7: support shaped Thai text in Add Text and UI fallback fonts (#767)
* M7: support shaped Thai text in Add Text and UI fallback fonts Share the bounded shaping and Type 3 writer with Thai, preserving vowel and tone clusters and original Unicode through save/reopen. Keep the Thai face optional and update UI notices and their translations. Constraint: Thai font assets remain in the companion craft-fonts repository. Confidence: high Scope-risk: moderate Co-authored-by: Codex <noreply@openai.com> * M7: show Thai Add Text before and after the fix Document the rejected input on the base commit and the saved Thai text on the fixed build using genuine UI captures. Constraint: Synthetic content and licensed fonts only; system font fallback disabled. Co-authored-by: Codex <noreply@openai.com> * M7: demonstrate Thai stress cases in the comparison screenshot Show nine lines of stacked marks, both Sara Am encodings, Thai digits and mixed Latin text. Verify all 198 Unicode code points survive saving and extraction with both PdfCraft and Poppler. Co-authored-by: Codex <noreply@openai.com> * CI and releases: craft-fonts pin to 3d9ab3a (adds Noto Sans Thai) ci.yml, freebsd.yml and the craft-fonts-ref release.yml passes to craft-common-actions move from 8dcdacd to storytold/craft-fonts 3d9ab3a, the revision with Noto Sans Thai (#18), so Thai Add Text is built in and tested. * ATTRIBUTION: drop the screenshot entry again (the image isn't committed) --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
981a2e57c8 | M10.5: Integrate main and adopt the validated craft-print spooler | ||
|
|
95434e9115 | M10.5: Preserve composed sheets with the shared scaling and duplex API | ||
|
|
49217aec6c |
fix(theme): complete Linux System appearance tracking (#1017)
* fix(theme): complete Linux System appearance tracking Follow up on #881 for #446 with the standard numeric appearance portal setting and event-driven updates that wake idle windows. Keep manual modes independent, cache their desktop updates without repainting, and cancel the worker on close. Retain inherited native window appearance and populate both egui style branches with the chosen palette. Preserve saved System, the Light default and unknown source fallback, and avoid desktop session access in headless UI contexts. Assisted-by: Codex (gpt-6.1-sol) * Desktop theme: keep the GNOME colour-scheme fallback XFCE needs The portal tracker read only org.freedesktop.appearance, which XFCE's portal doesn't answer (it proxies the GSettings schemas). It now also reads and subscribes to org.gnome.desktop.interface color-scheme and uses it when the freedesktop setting states no preference, as main's reader did. KDE answers only the freedesktop one, which wins when it states a preference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
21d5323132 |
Add faux-redaction audit (paint-order and colour-aware, fail-closed) (#110)
* Rework faux-redaction audit per maintainer review - Paint-order aware: a rectangle counts as a cover only when painted strictly after the text (positions from the joined content stream). - Colour-aware: WCAG contrast between text and cover must be below 3.0, so page backgrounds, table shading and highlights are not flagged. - Fail closed: audit_redactions returns Err on undecodable content streams (decoded_strict) and malformed /Annots; the operator bound is an error, not a silent truncation. - Graphics state carries across a page's content streams (one joined walk); form XObjects still isolate their state at Do. - Audit-only: the sanitize-for-sharing half is dropped (upstream #162). - Parity entry marked partial, not shipped. * Audit: register its layer, add a README, fail closed past 32 nested forms, document what it doesn't check --------- Co-authored-by: Erick Barraza <ejbarraza1994@gmail.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
b1e958cf63 |
M10: Enhance camera image finds the document's edges; rotate, crop or organize after Enhance page (part 3 of #1008) (#1027)
* M10: enhance camera images: straighten a page from four corners (ocr.camera-perspective) Four corners on a page (any order, a convex quadrilateral) are warped to an upright rectangle that replaces what the page shows, read at the page picture's own resolution (at most 600 dpi) and kept at that physical size. One undoable step through Replace Pages, so comments, links and bookmarks stay. - geom: a pure 3x3 homography solve and corner ordering (no dependencies); collinear, concave and non-finite input gives None. - engine: camera::warp (bilinear, over white, capped at 40 MP) and Session::enhance_camera_page. - automation: page_enhance_camera (corners in top-left-origin points). - parity: ocr.camera-perspective is partial; the UI handles, automatic edge detection and contrast cleanup are still missing. Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: Enhance camera image: drag four corners on the page, then Enhance page (ocr.camera-perspective) Scan & OCR ▸ Enhance camera image (ocr.enhance_camera) shows four corner handles on the current page, inset 5% from its edges, with what will be cut away dimmed. Enhance page straightens what they enclose through Session::enhance_camera_page (one undoable step); Cancel and Esc leave the page alone. The buttons float at the top right, clear of a centred page's corners, and the instructions are a notice, as for Crop. A command of its own, beside Enhance scanned file (ocr.enhance), which stays for scan correction (#1012). - ui-egui: camera_ui (handles, dimming, bar), QuickTool::EnhanceCamera, quick_tool "enhance-camera" for ui.state and the shot example. - Catalogs: the three new labels in every language with UI coverage (plus es, pl, zh-hant); ATTRIBUTION.toml hashes updated. - automation: page_enhance_camera is linked to the command. - parity: ocr.camera-perspective lists the command and the UI test. Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: Enhance camera image finds the document's edges (ocr.camera-perspective) The corners now start on the photographed document's edges when they can be found, else inset 5% as before. camera_edges::detect_document, pure Rust and deterministic: shrink to 400 px, blur, Sobel edges thinned and thresholded against the 98th percentile; a Hough transform where each edge votes within 6 degrees of its own normal; pairs of near-horizontal and near-vertical lines make convex quadrilaterals (at least 12% of the picture), kept when every side lies on a matching edge along at least 55% of its length and scored by area times the square of the mean support; each side is then refitted through its edge points (total least squares). None when nothing convinces. - Session::detect_camera_corners (user space); page_enhance_camera finds the corners itself when `corners` is omitted and reports the corners it used and whether they were detected; the UI command starts from them. - Tests on synthetic photos: plain table, text on the sheet, wood grain with a shadow, strong perspective, a 30 degree turn, a sheet running off the picture, no document, white on white, bad input; end to end through the engine and the tool. - Parity notes: what is still missing (turning the result upright, rounded corners, clean-up). Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M10: after Enhance page, rotate, crop or organize the page (ocr.camera-perspective) Enhance camera image no longer closes on Enhance page: the bar then offers rotate counterclockwise and clockwise for the enhanced page (one undo step each), Crop pages and Organize pages (which leave the tool for those tools), and Done; Esc leaves too. A photo of a card often comes out a quarter turn off until the page is turned upright automatically. The two icon buttons are named for screen readers. Every label already exists in the catalogs. Refs #1008. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: magdale76 <16279778+magdale76@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
86e74ec121 |
Linux: draw on the GPU the Wayland compositor draws with (#1088)
On a hybrid laptop whose Wayland compositor runs on the discrete GPU (Hyprland with AQ_DRM_DEVICES naming the NVIDIA card first), the built-in panel still hangs off the integrated GPU, so the panel rule (#8) picked it, and the window stayed empty: the compositor took the frames and showed nothing, with no error anywhere. Which GPU drives a display stands in for which GPU the compositor draws with. Under Wayland the compositor says so itself: the main_device of its linux-dmabuf feedback (version 4), the device it imports client buffers on, which Mesa's Vulkan device-select layer reads for the same purpose. compositor_gpu.rs asks for it on a short-lived connection, maps the dev_t to PCI ids through /sys/dev/char, and pick_adapter ranks that adapter first. No answer (X11, an older compositor, a GPU no adapter matches) leaves the existing rules in charge, and WGPU_POWER_PREF still overrides. wayland-client and wayland-protocols are already in the tree through winit; they become direct Linux-only dependencies. |
||
|
|
c20f3aff60 | M10.5: Adopt shared craft-print system spooler | ||
|
|
ae4f5d44f3 |
Merge pull request #966 from skylartaylor/codex/ui-suite-integration-20261010
Adopt shared panel docking with document-aware visibility |
||
|
|
ecd9d158ea | UI: preserve shared document workspace on current main | ||
|
|
53a5880386 |
Bump to 0.6.0; add a Linux riscv64 release build (#945)
- Cargo.toml: [workspace.package] and the internal path-dependency requirements move to 0.6.0 (they pinned 0.5.0, which `cargo update -w` can't match); Cargo.lock refreshed. - release.yml: new `linux-riscv64` job, ported from GridCraft. Cross-compiles on ubuntu-24.04 against ports.ubuntu.com riscv64 libs (the linux job's -dev packages, :riscv64), builds the tar.gz and the CLI-only tar.gz, smoke-tests pdfcraft-cli under QEMU, and is in the draft release job's needs, so SHA256SUMS covers it. - packaging/linux/package.sh: CROSS_ARCH / CROSS_TARGET / CROSS_COMPILE / EMULATOR support. - README and docs/releasing.md list the riscv64 tarballs (glibc 2.39+). - contributors.json refreshed. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8ff27dc86e | UI.9: preserve PDFCraft panels and document lifecycle | ||
|
|
9035ae3c82 |
fix(create): read UTF-16, byte-order-marked and Windows code page text files (#903)
Text files were decoded with String::from_utf8_lossy wherever they are
read: Create PDF and Combine from a .txt file, the automation `create`
tool's text `path`, and Search & Redact's word list file. So
- a UTF-16 file (Windows Notepad's "Unicode") became NULs and replacement
characters ("??G?r?????e?" for "Grüße"),
- a UTF-8 byte order mark became a stray character at the start, and
- a Windows-1252 or 1254 file lost every accented letter ("caf�", "K��").
For a word list that means the words were never found and stayed
unredacted.
pdfcraft_create::decode_text picks UTF-8 or UTF-16 (LE/BE) by the byte
order mark and drops it, keeps valid UTF-8, and otherwise decodes the
Windows code page: 1254 when the bytes 1254 uses for Ğ İ Ş ğ ı ş appear
without á í ó ú (which Turkish never writes but Icelandic and Faroese,
the only Western languages using those bytes, always do), else 1252.
encoding_rs is already in the build through lopdf.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
|
||
|
|
c933782039 |
Linux: "Use system setting" asks the desktop, not winit (#881)
winit reports no system theme on Linux — `ActiveEventLoop::system_theme` is `None` there on every desktop — so `ThemePreference::System` never saw the desktop's choice and silently fell back to whatever was on screen: PdfCraft came up light on a dark desktop, and picking "Use system setting" after Dark kept it dark. The default setting did not follow the system. `pdfcraft-platform` now reads the XDG Desktop Portal's Settings interface off the UI thread and the app follows the answer, including a change made while it runs. The colour scheme is asked for under its GNOME schema name first, which GNOME's and XFCE's portals both answer, then under the older freedesktop one, which KDE answers. A desktop that answers neither leaves the user's own choice alone, exactly as before. Co-authored-by: GyuriX <gyurix@users.noreply.github.com> |
||
|
|
aa6242d0c6 |
M4: bookmarks from tagged headings (organize.bookmarks-from-structure) (#754)
* Parity: merge the duplicate form.field-list evidence and notes #649 and #650 each added `evidence` and `notes` to the same entry, so the TOML has duplicate keys and `cargo xtask parity` fails on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M4: bookmarks from tagged headings (organize.bookmarks-from-structure) Bookmark options > New bookmarks from structure (command bookmark.from_structure, tool bookmark_from_structure) turns a tagged document's headings into bookmarks, as Acrobat's New Bookmarks From Structure does for headings: H1-H6 through the role map, a plain H by its Sect/Part/Art nesting, nested by level under a new first "Untitled" bookmark, each linked to its structure element (/SE, ISO 32000-2 §12.3.3). Titles come from /ActualText, the marked text drawn on the page (decoded through the font's ToUnicode or encoding), /Alt or /T. The structure walk is bounded (depth, visits, heading and title caps, seen-set) and survives self-referencing elements; an untagged document gets an actionable error; the edit is one undo step under the assemble permission. Not yet: choosing other element types, and destinations at the heading's position rather than its page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: size the shared raster allowance from thumbnail demand The canvas test planted a 132×171 thumbnail whose tag did not match the request finish_render_frame computes. On FreeBSD that real render finished in the same frame, so the retained raster exceeded 132×171×4. Plant a thumbnail that already satisfies demand and compare against that size. * M4: a heading's text stops gathering once it is far past the title length A heading whose marked-content list repeats one mark up to the visit cap could otherwise collect about a gigabyte before the title is cut to 256 characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
e742e29771 |
Windows: single instance, launches started together share one window (#282, #317) (#715)
* Windows: open files in the running PdfCraft instead of a new window each time (#282, #317) Outlook attachments, Explorer double-clicks and Open With start pdfcraft.exe with the file, so every file got its own process and window. Now the first PdfCraft listens on a Unix domain socket in %LOCALAPPDATA%\PdfCraft (or a portable copy's data folder), one per Windows session. A later launch that only names files sends their absolute paths, waits for "ok" and exits; the running app opens them as tabs through the existing OsEvent::Open path (the one macOS Apple events use) and comes to the front. Anything else keeps today's behaviour: --new-window, any other option, a launch without files, or any failure to hand over within 5 seconds opens a window of its own. Other platforms are unchanged. The socket lives in the user's profile, so other accounts can't create, replace or reach it. uds_windows (already in the lockfile through zbus) provides it: std has no Unix sockets on Windows and the workspace forbids unsafe. A socket left by a crash is replaced on the next start. * Windows: launches started together share one window (#282, #317) Several files selected in Explorer and opened at once start one process per file at the same moment. A launch could then find the socket bound by another launch that was not listening yet: connecting failed, clearing the socket failed with "Access is denied", and the launch opened a window of its own. Launches now take turns through a lock on a file beside the socket, held only while a launch finds out whether it is the first. A socket that cannot be cleared because another launch holds it is tried again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016es5tszobbb1yZ7JeozJXL * Windows: a portable copy doesn't hand files to another instance Its data folder may be writable by other accounts, so a socket there could leak or inject file paths. Builds on art804's single-instance work in #602. Co-authored-by: art804 <7828385+art804@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: art804 <7828385+art804@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
fb46909e3f |
Bump version to 0.5.0 (from 0.4.0) for release; refresh contributors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dc4b3eee84 |
Fix #378: draw on the GPU that drives the primary display on Windows (#631)
* Fix #378: draw on the GPU that drives the primary display on Windows The low-power preference picked the AMD Ryzen integrated GPU on a desktop whose monitors all hang off an NVIDIA card. Presenting from the headless iGPU to a window on another GPU took the AMD display driver down, blacking out every monitor for two minutes before PdfCraft exited. Windows now gets the display-aware adapter choice Linux already had: EnumDisplayDevices (through the safe `winsafe` crate, MIT) says which adapter drives each desktop display and which is primary; `pick_adapter` matches that to the wgpu adapters by PCI ids. The GPU driving the primary display wins, as the built-in panel does on Linux (#8). The log names the GPU chosen, the first question when a window stays black. Synthetic tests cover the Windows device-id parser and the primary-display rule; a live test checks the enumeration on the running machine. Verified on a Ryzen 7000 + RTX 3090 desktop: the app now starts on the 3090. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBMGuLLMhs1h8orSxcJXER * 631: the winsafe feature comment says user (and kernel) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
994b42c15c |
CLI: pdfcraft-cli.exe carries a Windows version resource (#598)
pdfcraft-cli.exe had no VERSIONINFO, so Explorer's Details tab, (Get-Item pdfcraft-cli.exe).VersionInfo and inventory tools showed no version or publisher for it. A build.rs like the app's embeds one with winresource (already a build dependency of pdfcraft): the same product name and copyright, its own description and file names, and the package version. PDFCRAFT_REQUIRE_WINRES=1, which the Windows packaging already sets, makes a missing resource compiler an error; otherwise it's a warning, and other targets are unaffected. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
74e30e5803 |
CLI: pdfcraft-cli ui refuses a control file another user could plant (#597)
* Security: pdfcraft-cli ui refuses a control file another user could plant `pdfcraft-cli ui --control FILE` sent the token and every command, typed text included, to whatever port FILE named, without checking who owned it. The README suggested `/tmp/pc.json`; in a shared /tmp another local user can create that file first, so the CLI talked to their listener. The CLI now refuses a symbolic link and anything but a regular file, a file over 64 KiB, and on Unix a file owned by another user or with any group or other permission bits (the app writes 600). The owner and mode are read from the opened file, and its device and inode must match the path's, so a swap after the check is refused too. std has no geteuid and unsafe is forbidden, so the effective uid is the owner of a file the CLI creates in the temp folder and removes. The README now uses a per-user path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Security: stop the app when --control can't be published A control file that couldn't be written was only logged, and the app ran on with a listener nobody could reach. The failure can mean another user already created a file at a shared path to receive the commands, and a script started with --control would otherwise drive nothing without being told. The app now exits with the error, before any document opens, so no work is lost. The README and the app's usage comment say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CLI: pdfcraft-cli ui refuses a control-file port above 65535 `ui` truncated the control file's port with `as u16`, so 65536 + N reached port N, and the token and command went to whatever listened there. A port that doesn't fit is now refused, before connecting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CLI: learn the effective uid from rustix, not a temp-file probe Review on #597: the probe made `pdfcraft-cli ui` depend on a writable temp folder to judge a control file somewhere else, created and removed a file on every call, and could leave one behind. rustix's `geteuid` is safe and can't fail, and rustix is already in Cargo.lock; it is a Unix-only dependency of the CLI, so Windows builds don't compile it. A control file of your own now works with TMPDIR pointing at a folder that doesn't exist; the probe refused it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
36468f0ad4 |
Windows: a GPU that can't set up the window falls back to OpenGL (#519) (#617)
* Windows: vendor egui-wgpu 0.36.2 as published (#577) The crate as released on crates.io, plus egui's LICENSE-APACHE and LICENSE-MIT from the 0.36.2 tag (the published crate leaves them out). Not used yet: the next commit patches it and adds it to [patch.crates-io]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Windows: a window larger than the GPU's texture limit can't close PdfCraft (#577) egui-wgpu 0.36.2 configures a window's surface at the window's own size. Beyond the device's max_texture_dimension_2d, Surface::configure fails validation and wgpu's default error handler panics. eframe configures the surface before the app is created, so no app code can intercept it (emilk/egui#8361). #578 raised the device's limit to the adapter's; this removes the crash for windows larger than even that. vendor/egui-wgpu: `winit::surface_fit` shrinks both sides by one factor so neither exceeds the limit (the longer side lands exactly on it), and egui draws at that factor into the surface. On DX12, PdfCraft's Windows default, the swapchain is stretched over the window (DXGI_SCALING_STRETCH), so the window stays complete and lined up with the pointer, only softer; wgpu's GL backend copies it unscaled into a corner instead. It applies wherever a surface is sized: install, resize (start-up restore, maximise, DPI change, fullscreen) and a recreated surface, which starts again from the window's own size. Depth and MSAA textures follow. A surface logs a warning when it becomes fitted. Why only an Outlook launch hit it (read from the code, not observed): winit creates the window at CW_USEDEFAULT, so Windows places it, and a launcher that hands ShellExecute its own window gets new windows on its monitor. winit then converts the restored logical size at that monitor's scale: 3440 points became 8600 pixels on a 250% panel. eframe's clamp compares points against the largest monitor, so it let 3440 through. Tests: surface_fit with the issue's 8600 x 3423 and edge sizes (zero, u32::MAX, a zero limit, the longer side exactly on the limit); egui-wgpu resolves to vendor/egui-wgpu and the fit stays wired into the paths that size a surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Windows: a GPU that can't set up the window falls back to OpenGL (#519) On a 2015 Intel GPU with no D3D12 driver, wgpu picked its GL adapter over WARP and Surface::configure failed (GpuWaitTimeout). wgpu's default error handler panics, and eframe configures the root surface before the app is created, so the OpenGL retry from #530 never ran. Vendored egui-wgpu (PdfCraft patch #519): winit::catch_errors runs a closure inside wgpu error scopes (validation, out of memory, internal) and returns the error. Painter::add_surface configures each new window's surface inside it and returns a failure as WgpuError::ConfigureSurface, so eframe returns Error::Wgpu and main retries with glow. Later reconfigures are unchanged. Tests: a real device returns a wgpu validation error from catch_errors instead of panicking (red with the scopes removed), and add_surface keeps the wiring (red with it removed). Docs: vendor/README.md patch 2, docs/development.md "Renderer fallback" and PDFCRAFT_RENDERER, ROADMAP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * vendor/egui-wgpu: drop the stray .cargo-ok marker * Tests: the GPU error-scope test skips on a machine without a GPU adapter --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
f7d5ca4a07 |
Windows: a window larger than the GPU's texture limit can't close PdfCraft (#577) (#609)
* Windows: vendor egui-wgpu 0.36.2 as published (#577) The crate as released on crates.io, plus egui's LICENSE-APACHE and LICENSE-MIT from the 0.36.2 tag (the published crate leaves them out). Not used yet: the next commit patches it and adds it to [patch.crates-io]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Windows: a window larger than the GPU's texture limit can't close PdfCraft (#577) egui-wgpu 0.36.2 configures a window's surface at the window's own size. Beyond the device's max_texture_dimension_2d, Surface::configure fails validation and wgpu's default error handler panics. eframe configures the surface before the app is created, so no app code can intercept it (emilk/egui#8361). #578 raised the device's limit to the adapter's; this removes the crash for windows larger than even that. vendor/egui-wgpu: `winit::surface_fit` shrinks both sides by one factor so neither exceeds the limit (the longer side lands exactly on it), and egui draws at that factor into the surface. On DX12, PdfCraft's Windows default, the swapchain is stretched over the window (DXGI_SCALING_STRETCH), so the window stays complete and lined up with the pointer, only softer; wgpu's GL backend copies it unscaled into a corner instead. It applies wherever a surface is sized: install, resize (start-up restore, maximise, DPI change, fullscreen) and a recreated surface, which starts again from the window's own size. Depth and MSAA textures follow. A surface logs a warning when it becomes fitted. Why only an Outlook launch hit it (read from the code, not observed): winit creates the window at CW_USEDEFAULT, so Windows places it, and a launcher that hands ShellExecute its own window gets new windows on its monitor. winit then converts the restored logical size at that monitor's scale: 3440 points became 8600 pixels on a 250% panel. eframe's clamp compares points against the largest monitor, so it let 3440 through. Tests: surface_fit with the issue's 8600 x 3423 and edge sizes (zero, u32::MAX, a zero limit, the longer side exactly on the limit); egui-wgpu resolves to vendor/egui-wgpu and the fit stays wired into the paths that size a surface. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * vendor/egui-wgpu: drop the stray .cargo-ok marker --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1f0466fcae |
M2: bound image resampling buffers before allocation (#592)
Check source, destination, convolution and filter-plan sizes before allocation. Preserve original image geometry when a resize exceeds the policy, while allowing valid wide sources to shrink. Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
56f85f76df |
M9: more signature validation algorithms (SHA-3, RIPEMD-160, Ed25519, P-521, brainpool, PSS, rsa_sha1) (#386)
* M9: more signature validation algorithms Validation now reads what real signers write, and says when it is weak: - Digests SHA-224, SHA-512/224, SHA-512/256, SHA3-224/256/384/512 and RIPEMD-160, beside SHA-1 and SHA-2. - RSA PKCS#1 v1.5 for every digest, in `rsa_pad`: the one expected encoding is built and compared byte for byte, with or without the DigestInfo's NULL (RFC 8017 App. B.1). A signature over the bare digest, which doesn't say which hash it uses, is refused. The signature value must be below the modulus (RFC 8017 §5.2.2); s and s + n both verified before. - RSA-PSS with the hash, MGF1 hash and salt length the signature declares. - ECDSA as DER, non-minimal DER or raw r||s on P-256, P-384, P-521 and brainpoolP256r1/P384r1/P512r1 (BSI "plain" ECDSA OIDs too); brainpoolP512r1 is hand-written on crypto-bigint because no RustCrypto crate has it. - Ed25519 (RFC 8419), and the legacy adbe.x509.rsa_sha1 format. - An algorithm PdfCraft can't check is Unknown, never Invalid. - SHA-1 and RIPEMD-160 signatures validate, but the signature's details say the algorithm is weak (wording pinned by tests). - Encoding irregularities that were tolerated are listed in the details (`verify_noting`; `verify` keeps its signature). Found while testing, each with a regression test that failed before the fix: a brainpoolP512r1 public key with a coordinate >= p panicked inside crypto-bigint's `sub_mod` (the certificate comes from the PDF); it is refused now, and the point at infinity is an invalid signature, not an error. Vectors were made with OpenSSL (tests/data/README.md has the commands). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Update comment to clarify MAC digest restrictions Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * M9: a legacy x509.rsa_sha1 signature whose key the verifier rejects is Invalid; only unsupported keys are "can't check yet" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
1ba8b194b2 |
XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures (#414)
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures The xfa crate lays out the field kinds that were left blank or shown as text fields: - Choice lists become combo boxes (editable with textEntry) or list boxes (open="always"; multi-select with open="multiSelect"), with the shown items and the saved values of <items save="1"> as /Opt pairs, /V, /I and /DV as saved values, several on separate lines in the data. Field::saved_value maps shown text to saved values for the layout and the script tree alike, so scripts read what is saved. - Password fields are password text fields; signature fields are /Sig fields the app can sign. - Image fields show the picture the data (base64) or the template holds; barcode fields show their value as text, with a warning that no bars are drawn. - PNG and GIF pictures (fields and draws) are decoded to Flate image XObjects with a soft mask for transparency; JPEGs stay as they were. Sizes come from the headers, so layout never decodes; decoding is capped at 40 million pixels and failures are warnings, not errors. Engine: multi-select list values from scripts and data are one per line. README and parity notes updated (form.xfa-dynamic). Hardened after an adversarial review pass: picture sizes refused from the header past 20 million pixels or 65,535 a side, the GIF decoder given limits (a frame far larger than its screen is refused before allocation), template pictures read whole up to 32 MB, pictures decoded once and remembered in the catalog by content hash so re-layouts reuse them, data pictures that aren't images reported by field; choice values matched against saved values first, several values deduplicated and capped, /I ascending and unique, /DV through the same mapping, <value> children read, the item cap reported; the reopen path takes another viewer's choice values (several per line); CMYK JPEGs inverted only with Adobe APP14. * D5: README for cos and engine, and an honest cos status line Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets. Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes. Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Preserve alternate appearances and indirect push-button values Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test. Co-authored-by: Codex <codex@openai.com> * M7: select and edit grouped Form XObject figures Co-Authored-By: Codex <noreply@openai.com> * M14: let neutral Windows locales fall through to display language Co-authored-by: Codex <codex@openai.com> * pdfcraft-cli --help lists `mcp` only when the build has it Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed `pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command". USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead of calling it an unknown command. - tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and mcp_without_the_feature_says_it_was_left_out (without the feature) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * engine README: the execution plan is local-only, so don't link a path that doesn't exist * Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher (not stable across Rust releases) and read back from the file. Each image XObject now records the SHA-256 of its picture's bytes, and a re-layout finds the XObjects on the pages it replaces by that hash; nothing goes in the catalog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Tanish Manku <tmanku.dev@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: lloyd <lloyd@stackworx.io> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
37076662a7 |
M7: select and edit grouped PDF figures (Form XObjects) (#450)
* D5: README for cos and engine, and an honest cos status line Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets. Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes. Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Preserve alternate appearances and indirect push-button values Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test. Co-authored-by: Codex <codex@openai.com> * M7: select and edit grouped Form XObject figures Co-Authored-By: Codex <noreply@openai.com> * M14: let neutral Windows locales fall through to display language Co-authored-by: Codex <codex@openai.com> * pdfcraft-cli --help lists `mcp` only when the build has it Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed `pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command". USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead of calling it an unknown command. - tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and mcp_without_the_feature_says_it_was_left_out (without the feature) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * engine README: the execution plan is local-only, so don't link a path that doesn't exist --------- Co-authored-by: Tanish Manku <tmanku.dev@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: lloyd <lloyd@stackworx.io> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
86ec14291c |
Fix main CI: move the rotated-field render test out of pdfcraft-forms (#591)
#516 added pdfcraft-render as a dev-dependency of pdfcraft-forms for one rendering test. Both crates are layer 3, so `cargo xtask layers` (run in CI) rejects the sideways edge and main went red. The test now lives in crates/engine/tests/form_rotation.rs, where both crates are already dependencies, and forms drops the dev-dependency. The parity evidence points at the new location. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bd4736d39c |
M2: bound page-label traversal and output (#480)
* hayro-syntax: fix float mantissa overflow on high-precision coordinates (#199) jsPDF outputs double-precision floating-point coordinates with 16+ decimal places in /MediaBox. In hayro-syntax, read_inner accumulated float mantissa digits into u64 using wrapping arithmetic. Integer mantissas exceeding u64::MAX wrapped modulo 2^64, severely corrupting page dimensions (e.g., 3874.96 pt became 185.61 pt, rendering as a warped ribbon). Accumulation now uses checked arithmetic: trailing fractional digits exceeding u64 capacity are safely ignored without incrementing decimal_shift. This preserves full IEEE-754 f64 precision while preventing overflow for numbers with arbitrary numbers of decimal places. Includes: - Unit test in hayro-syntax with reported values and a 50-digit fraction. - Synthetic PDF regression test in pdfcraft-render verifying inspect(). - Vendored patch (6) documentation in vendor/README.md. Fixes #199 * M2: bound page-label work and output before applying labels Share bounded PDF Roman and repeated-letter formatters between inspection and editing. Bound tree traversal, raw and decoded prefixes, individual labels and aggregate label output, with checked numbering arithmetic. Stage inspection labels before applying them and retain physical page numbers with a warning when optional labels cannot be processed. Reject unsupported label edits before changing the document tree. Add synthetic limit, cycle, Unicode, numbering and atomic-edit regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: mesh shadings can't exhaust memory or run for minutes `cargo xtask fuzz` (with the dash, JBIG2 and decompression fixes in place) found a page whose Coons patch shading (type 6), a 147-byte stream with 1-bit coordinates decoded onto +-40000, took the renderer past 3 GB. hayro-interpret samples mesh shadings (types 4-7) pixel by pixel over each triangle's device-space bounding box into a map, with nothing but the u16 cast bounding it: up to 65536 x 65536 entries for one triangle. A synthetic single patch passed 6 GB in six seconds; ten thousand triangles that each cover half a 1000 x 1000 page ran for minutes. Two rounds of review of earlier versions found more: every patch was cut into 722 triangles (about 150 KB) before anything limited them; a lattice mesh with `/VerticesPerRow 0` pushed empty rows forever, and one with 4294967295 read the whole stream into one row; a mesh painted a thousand times was triangulated a thousand times; and the first caps cut content without a word. Vendored hayro-interpret patch (18): - `ShadingPattern::encode_within(area)`: the vendored hayro renderer passes the area it will actually draw (the path's bounding box on the canvas, which it already computes) plus a pixel for texels at a fractional edge, and triangles are sampled only inside it, so the map holds at most that area's pixels. - `TriangleSampler`: at most 8 visits per pixel of that area plus 4 per triangle in it. A triangle's bounding box is about twice its area and a sub-pixel triangle's is at most 2 x 2 pixels, so tiling meshes and small dense ones (a patch in a few pixels is 722 triangles) still shade completely; overlapping ones stop. - Coons and tensor-product patches are triangulated one at a time. - Meshes hold at most `MAX_MESH_PATCHES` (2^16; a 200 x 200 gradient mesh is 40 thousand) patches and `MAX_MESH_TRIANGLES` (2^20) triangles, and a lattice needs two (ISO 32000-2 8.7.4.5.6) to 2^19 vertices per row. - A page samples at most `MAX_PAGE_MESH_TRIANGLES` (2^25; the 200 x 200 mesh is 29 million) mesh triangles in all, reset per page like the existing paint budget. - Each of these cuts logs a warning. `encode()` keeps its signature; with no area it samples within a fixed 2^24 visits. Synthetic regression tests in render: - `mesh_shadings_far_off_the_page_render`: a Coons patch and a triangle mesh decoded onto +-40000 render and the rest of the page draws; a patch on the page, the part of the huge one that covers it, and a patch filling a 4 x 4 pt rectangle as a pattern are all shaded. - `many_overlapping_mesh_triangles_finish`: 10,000 triangles each covering half of a 1000 x 1000 page finish, and are drawn. - `mesh_shadings_with_many_patches_or_no_rows_finish`: 40,000 patches beside the page, and a lattice with `/VerticesPerRow 0`. Red on main (out of memory, killed at 3.9 GB). Each part is pinned by a mutation: no visit budget stalls past 60 s; no clip leaves the covering patch unshaded; no per-triangle allowance leaves the small patch unshaded; triangulating all patches first runs out of memory (killed at 5.8 GB); no rows check runs out of memory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: philkwek <philkwek03@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: James Locke <336218001+jcl-bit@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
e88def3465 |
Never crash: decompression bombs can't exhaust the renderer's memory (#411)
* Never crash: decompression bombs can't exhaust the renderer's memory The renderer's parser (vendored hayro-syntax) decoded Flate, LZW and RunLength streams with no output limit. A 2 MB page whose content stream inflates to 2 GB, or whose 8 x 8 image does, took `pdfcraft-cli render` and `text` past 3 GB in under a second, and a few more MB of file scale it further. On the desktop that is the first-page render of an emailed PDF, and the allocation-failure abort takes every open document with it. Our own layers already bound single streams (lopdf's load-time decoding at 256 MiB, cos at 1 GiB), so `info` stays at a few MB; the renderer path didn't. Random mutation almost never makes a high-ratio stream, so `cargo xtask fuzz` doesn't find this; found by hand while triaging the fuzz run. Vendored hayro-syntax patch (6): - `decode_limit` gives each filter an output limit, and Flate (both flate2 and the fallback inflater), LZW and RunLength never write past it (the buffer can still grow to under twice it): `MAX_DECODED_STREAM`, 256 MiB, lopdf's figure; or, for the last filter of an image whose size, bits per component and components are known, twice its pixel bytes (a PNG predictor tag byte per byte at most), capped at 1 GiB. Earlier filters in a chain produce compressed data and get the general limit. - `Stream::decoded_within(max)` for callers that budget. - A page's `/Contents` array shares one `MAX_DECODED_STREAM` (naming one bomb twenty times decoded 5 GB), reserving exactly what it appends. Vendored hayro-interpret patch (18): - The content a page holds at once (its contents, and the forms, tiling patterns and Type 3 glyph procedures being painted) stays within `MAX_PAGE_CONTENT`, 512 MiB, reset per page like the existing paint budget. Forms nest fifty deep and each level kept its content while the next ran: a form painting itself around a 256 MiB stream would have held over 12 GB. Each level gives its bytes back when it is dropped, so content painted again and again keeps drawing, and a stream without filters (borrowed from the file) costs nothing. - Type 0 sampled functions read every sample into a 4-byte value and then a table entry: 256 MiB of 1-bit samples asked for over 8 GB. They now take at most 2^21 table points and 2^23 values (the largest real tables, DeviceN tint transforms such as 33^4 points x 3 outputs, are 1.2 million and 3.6 million), counted with checked arithmetic, and read no further than `/Size` asks. `/Size 0` no longer underflows building the default `/Encode`. Three rounds of independent review found the aggregate cases, buffer doubling past the limit, a chain/predictor shape the first image limit would have truncated, the Type 0 amplifier, and a first, cumulative budget that would have dropped legitimately repeated content, and a first Type 0 cap low enough to refuse real tint transforms; all fixed here. Measured in a release build (render; OS peak commit): - content stream inflating to 2 GB: 389 MB, 0.3 s - 8 x 8 image inflating to 2 GB: 4 MB, 0.2 s - /Contents naming it twenty times: 516 MB, 0.3 s - a form painting itself around it: 644 MB, 0.4 s Synthetic regression tests in render (flate2 is a new dev-dependency there, already in the workspace): - `decompression_bombs_stop_at_the_stream_limit`: 288 MiB bombs through flate2, the fallback inflater (a stored block with a wrong length check, which flate2 refuses and the fallback accepts), LZW and RunLength each decode exactly 256 MiB, and at most 128 bytes as an 8 x 8 image. - `page_contents_and_nested_forms_share_one_budget`: three /Contents references decode one stream's worth; a self-painting form finishes. - `content_painted_again_and_again_keeps_drawing`: a 2 MiB form painted 300 times draws the 300th time. - `sampled_functions_with_billions_of_samples_are_refused`. Ten mutations each turn them red: no limit at all; no limit in flate2, the fallback, LZW, RunLength or for images; an unshared /Contents limit; no page budget; no refund on drop; no Type 0 sample cap. (Taking a form out of the budget no longer compiles: its content is the budget guard.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M2: normalize JPEG2000 samples without overflowing Use bounded integer sample scaling for 1-32-bit JPX images and reject empty dimensions before chunking. Cover public decoding and continued page rendering with synthetic regressions. Co-Authored-By: OpenAI Codex <codex@openai.com> * Never crash: decompression cut at its limit is logged, not silent; vendor README renumbered after #575 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Streetblock <github@streetblock.de> Co-authored-by: OpenAI Codex <codex@openai.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
3af93c7585 |
M5: draw all ten PDF line-ending styles (#520)
* M5: draw all ten PDF line-ending styles Imported lines, polylines and callouts kept Square, Circle, Diamond, Butt, Slash and reverse arrows in /LE but would not redraw them. Those styles now draw, and comment_add and Comment properties can set them. * hu, uk: Line ending --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
c9d0619226 |
Forms: rotate field widgets with /MK /R (#516)
* M6: rotate form field widgets with /MK /R Filling or restyling a rotated field redrew its text upright. Appearances now follow /MK /R, and Field Properties and form_set_props can set the rotation. * Forms: a rotation refused as too small is checked before any other property is written; no indexing by widget number * hu, uk: Rotation: --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> |