Commit Graph
1422 Commits
Author SHA1 Message Date
mtlcl410andClaude Opus 5.5 14e17ece3b M3: in full screen a double-click selects a word, and full screen selects the Select tool (#1459)
Review on #2070 (CodeRabbit):
- A click turned the page on its release, so a double-click's second click,
  meant to select a word as the PR promises, landed on the next page. The
  turn now waits out egui's double-click time, and is dropped when a
  selection appears or full screen is left; a double-click on blank page
  area still turns once.
- Entering full screen from another tool left it on, and clicks only turn
  pages with Select while the quick tools are hidden. Full screen now selects
  Select and gives the earlier tool back on leaving, as Space's temporary
  Hand tool does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 15:56:30 +08:00
mtlcl410andClaude Opus 5.5 42e63703d9 M12: full screen in two-page view fits the spread it shows (#379)
Review on #2070: fit_zoom fitted the document's largest page in every
two-page display, so a smaller spread stayed small in full screen. While
presenting, one spread at a time, it now fits the spread shown, the cover
page on its own included; in the window, and while scrolling, the zoom stays
one for the document. The spread arithmetic is one helper, shared with
step_page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 15:02:23 +08:00
mtlcl410andClaude Opus 5.5 3ed0dff807 M12: full screen survives a reload and starts after the document's own initial view (#379)
Review follow-ups on #2070.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 15:02:22 +08:00
mtlcl410andClaude Fable 5.1 1e2e7cc79f M3: full screen shows one fitted page at a time, as Acrobat's Full Screen Mode (#1459)
Full screen is the page on black and nothing else: one page (or one spread,
when two-page view was on, #1353) fitted to the screen, from the current
page; a click, Space, Enter, the arrows and Page Up/Down turn pages, a
shift-click and Backspace go back; Esc brings the display from before
back at the page reached. The quick tools are not drawn in full screen.

Transitions, auto-advance, loop and the cursor hiding stay planned.

Refs #379.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:02:20 +08:00
43550c448d M2: a JP2 image in a colour space the decoder refuses is drawn by its component count (#3306) (#3363)
* M2: a JP2 image in a colour space the decoder refuses is drawn by its component count (#3306)

The veraPDF fixture's JPX header names CIEJab (enumerated colour space 19),
which hayro-jpeg2000 refuses, so the image failed and the page drew blank.
When the decoder refuses the image and its colr boxes name such a space, it is
decoded again from a copy naming greyscale, sRGB or CMYK by component count,
as MuPDF and Poppler draw it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: copy the JP2 stream only when a colour enum needs replacing (#3306)

Review follow-up on #3363: a stream the decoder refused for another reason was
copied for nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* Renumber the JP2 colour-space patch (21): #3378 takes hayro-syntax (20)

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>
2026-10-19 02:50:58 -04:00
cbbeb61650 fix(parse): accept spaces between stream and its line break (#3378)
* fix(parse): accept spaces between stream and its line break

Microsoft Reporting Services writes `stream \r\n`, a space before the line break.
hayro-syntax and pdfcraft-cos took the stream data to start at the space, so the
content stream failed to inflate: the page rendered blank (white, or black with
inverted colours) and an edited save corrupted the stream. Spaces and tabs are now
skipped, but only when a line break follows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Number the stream end-of-line patch (20), as vendor/README does

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-19 02:50:21 -04:00
mtlcl410andClaude Opus 5.5 b955c2eefb M2: a tiling pattern drawn once shows its cell at full scale (#3349) (#3356)
* M2: a tiling pattern drawn once shows its cell at full scale (#3349)

PowerPoint writes a picture fill as a tiling pattern with /XStep 2147480100.
Fitting that step into the 3000 px cell pixmap shrank the picture below a
pixel. When what is painted can only show the first cell, the cell's /BBox is
drawn at full scale with a transparent border and padded instead of tiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: a tiling cell whose scale isn't finite keeps the old path (#3349)

Review follow-up on #3356: a /BBox 1e-40 wide passes the zero-area check and
gives an infinite cell scale, which would send non-finite geometry to the cell
renderer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: a stroke's miter counts in the one-cell check (#3349)

Review follow-up on #3356: shows_one_cell took the line width as a stroke's
reach; a miter join reaches half the width times the miter limit. The reach
formula patch (9) uses is now one function, stroke_reach, used by both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: a skewed stroke's reach uses the transform's largest stretch (#3349)

Review on #3356: under a skew, a direction between the axes stretches
further than either axis, so the larger axis stretch could place a miter
tip short of the next cell when it reached it, and the one-cell path left
that cell unpainted. The reach now uses the spectral norm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:50:09 -04:00
TomCatTandClaude Opus 5.5 4d42b48d20 fix(sign): offer only certificates that may sign documents from the Windows store (#3365)
A CAC puts its encryption, logon and signing certificates in the store
under the same name, so the digital ID list showed several identical
entries and only one of them makes a signature that validates. Grey out
the ones whose key usage or extended key usage doesn't allow document
signing, with the reason, the same way an ID without a usable key is
already treated.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:49:57 -04:00
mtlcl410andClaude Opus 5.5 2f21f920a0 fix(windows): name the single-instance socket by the session id, so a launch without SESSIONNAME finds its session's window (#3338) (#3340)
* fix(app): a launch without SESSIONNAME hands its files to the console session's window (#3338)

Task Scheduler, services and some remote shells start PdfCraft without
SESSIONNAME, so the launch looked for instance.sock while the window opened
from Explorer listens on instance-Console.sock, and a second window opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* fix(windows): name the single-instance socket by the session id, not SESSIONNAME (#3338)

Review on #3340 (earlvanze): falling back to the console's socket when
SESSIONNAME is unset could hand a file from a Remote Desktop session to a
console window of the same user in another session. The socket is now named
by the session id in the process token (winsafe, no unsafe): a launch from
Task Scheduler runs in the user's session and finds its window, and
different sessions never share a socket. SESSIONNAME names it only if the id
can't be read.

Tested with a real scheduled task: on main the task's launch (no
SESSIONNAME, session 1) opened a second window; with this change the file
went to the running window in session 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:49:46 -04:00
6733e073ef Keep long menus usable in short windows (#3374)
* M14: keep long menus usable in short windows

* Menus: scroll each top-level menu once, from its caller, so View's list isn't nested in a second scroll area

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>
2026-10-19 02:49:34 -04:00
7d9adcb8f1 fix(preflight): identify PDF standards by XML namespace (#3364)
* fix(preflight): identify standards by XML namespace

* fix(preflight): bound XMP attribute parsing work

* fix(preflight): cap XMP during stream decoding

* Preflight: read the PDF/A and PDF/UA identification by its usual prefix when the XMP can't be parsed as XML

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>
2026-10-19 02:48:40 -04:00
Earl Co 236ce9a064 Clear image pixels intersected by redaction marks (#3368) 2026-10-19 02:48:29 -04:00
Earl Co 66f1085688 Keep document scroll position across viewer modes (#3362) 2026-10-19 02:48:18 -04:00
MarekSpodBiedry 9d23505dd7 Clamp the zoom-anchor scroll to the content so zooming out no longer jolts the page (#3266) (#3350) 2026-10-19 02:48:06 -04:00
Earl Co 6e61b50503 Fix clock-dependent form properties test (#3367) 2026-10-19 02:47:55 -04:00
Chiranjeevi U 4db6ac4ae2 M3: Accelerate coarse wheel scrolling (#1094)
* Viewer: accelerate coarse wheel scrolling (#1072)

Line/Page notches moved a fixed eased step, so a fast spin felt slow.
Accumulate one frame of coarse input per axis, scale by arrival rate
(1.0 idle, up to 6.0 spinning), and apply it directly like precise
touchpad input.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Viewer: add only the fast-spin surplus on top of egui smoothing

Single notches flow through egui untouched, so isolated input matches
the scroll speed preference exactly; page units are never boosted.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

---------

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
2026-10-18 16:15:39 -06:00
dab3149110 M2: a fax image with fewer /Rows than its /Height is decoded to its height (#3308) (#3316)
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 17:08:32 -04:00
6fd49d6565 M1: a saved file keeps its creation date (#2658) (#3291)
* M1: a saved file keeps its creation date (#2658)

Every save writes a staging file and renames it over the target, so the
file's creation date became the save time. Before the rename the staging
file now takes the replaced file's creation date, where the system lets
a program set one (Windows and macOS, through std's FileTimesExt;
best effort). A new file keeps its own date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* Staging: compile the creation-date step only where it applies; say so in write_atomically's doc

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>
2026-10-18 17:08:02 -04:00
NetterSilverandClaude Haiku 5.5 c00ee14be8 Find and edit images whose Do or cm spans two content streams (#3294)
Fixes #3113

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 17:07:51 -04:00
NetterSilverandClaude Haiku 5.5 e2d4814089 Read /ActualText from named property lists in edit text and export (#3301)
Fixes #3110

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 17:07:39 -04:00
mtlcl410andClaude Opus 5.5 16f36810cc M7: work out each font's space width once per paragraph rewrite (#2616) (#3297)
* M7: work out each font's space width once per paragraph rewrite (#2616)

Each keystroke in Edit text rebuilds the paragraph's draft. When a source
font can't show a space (LaTeX and Sphinx subsets space words with TJ
kerning instead), source_space reads every font on the page, building
Metrics for each. The draft did that once per character, so a keystroke
in a 380-character paragraph of NVIDIA's vGPU guide took ~460 ms. The
widths are now worked out once per font before the per-character pass:
~71 ms there, ~16 ms for a 200-character paragraph (was ~255 ms), with
identical layouts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

* M2: work out space widths only for the fonts a paragraph uses (#2616)

Review follow-up: the eager map scanned the page once per page font even for a
short edit on a page of many fonts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:07:29 -04:00
oracola91 628f43e17a fix(ui): the recovery folder is owner-only on Unix (#3320)
Recovery snapshots are unsaved copies of open documents. The folder was
made with the default mode, so on a shared Linux or macOS machine other
accounts could list it and read the snapshots of documents that are not
encrypted. The folder is now made owner-only (0o700), and a folder an
earlier version made is tightened when an instance starts or writes a
snapshot, as image_pick does for its folder. A volume that can't change
modes (FAT, some network shares) works as before, with one warning in
the log.
2026-10-18 17:07:17 -04:00
mtlcl410andClaude Opus 5.5 6b099bef72 docs: vendor/README keeps one hayro-interpret row, with the patches lost to number collisions (#3312)
Merges left four hayro-interpret rows; the current one lacked the
transparency-group, Type 3 rendering-mode and Lab white-point patches (each
numbered 25, 27 or 28 on a branch where another patch took that number).
They are now (41), (42) and (43) in the one row, with their tests.


Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:07:05 -04:00
104411dc7a Read doubled-sign integers like --280 in the COS lexer (#3299)
* Read doubled-sign integers like --280 in the COS lexer

Fixes #3111

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* COS lexer: strip the doubled sign without allocating

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 17:06:53 -04:00
Morten Husted Lochmann f280ca8e6d M10: exported TIFF and JPEG pages record the resolution they were drawn at (#3332)
* M10: exported TIFF and JPEG pages record the resolution they were drawn at

A page exported as TIFF said 1 x 1 with no unit, whatever the dpi, and
a JPEG export said nothing, so other programs showed and printed them at
a default (a 300-dpi page shown at 72 dpi is four times too big).
encode_image now takes the dpi: TIFF X/YResolution in inches, the JFIF
density in dots per inch.

PNG stays without pHYs. It counts pixels per metre, so 72 dpi would read
back as 72.009, and a page made from an exported PNG (doc_create from
images, as enhance_camera_image_through_tools does) would come out 0.01%
small.

Exporter records the resolution a page was actually drawn at (dpi_used),
so a page too large for the renderer says the lower one. render records
--dpi, since it refuses oversized pages. The TIFF is written with the
tiff crate, already in the graph, because image's TiffEncoder can't set
a resolution.

* M10: TIFF page exports are LZW-compressed, as documented

encode_tiff's comment said "LZW-compressed by the encoder's default",
but the TIFF was written uncompressed: a Letter page at 300 dpi came to
25 MB. It is now LZW without a predictor, the compression TIFF readers
support most widely after none.

* Contributors: add my name for the About window
2026-10-18 17:06:41 -04:00
58795e9188 cli: info names the standard a document declares (#3328)
* cli: info names the standard a document declares

A PDF/A file was summarised with no mention of PDF/A. `info` prints two dozen
fields -- tagged, xfa, javascript, layers -- and said nothing about the
standard, while the app's Standards panel reads the same declaration from the
catalog's XMP and shows "PDF/A-2b" (#3319).

`info` gains `pdfa` (the part and conformance together, e.g. "2B"), `pdfua` and
`output_intents`, from `pdfcraft_preflight::declared` -- the call the Standards
panel makes, so the two agree by construction rather than by a second reading
of the metadata. A document that declares nothing leaves them null and empty.

Read in the CLI rather than in `pdfcraft_render::Inspect`: render is layer 3
and preflight layer 4, so the summary struct can't reach it, while an app may
depend on any layer.

* cli: read the declaration from the bytes already in hand

`Document::open_with_password` takes an `Arc<Vec<u8>>` and `info` already holds
one, so `bytes.to_vec()` copied the whole file for every call -- a file-sized
allocation on a large PDF, for nothing. Clone the `Arc` instead.

* info: name the declared PDF/A level as the automation tools do (PDF/A-2b)

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>
2026-10-18 17:06:29 -04:00
NetterSilverandClaude Haiku 5.5 8d6254aa0b Render weekday letters in FormCalc date pictures (#3321)
Found while testing crates/js/src/formcalc.rs:1929-1957 (format_date), with DateFmt returning "EEEE, MMMM D, YYYY" at :2333

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 17:06:18 -04:00
NetterSilverandClaude Haiku 5.5 666648349b Drop U+FFFE and U+FFFF from XFDF/XML form-data export (#3330)
Found while testing crates/xfdf/src/lib.rs:192-209 (esc), used by export_xfdf (:445) and export_data XML (:594-612)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 17:06:07 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 0c9c621a1f ci(deps): bump cachix/install-nix-action (#3322)
Bumps the actions group with 1 update: [cachix/install-nix-action](https://github.com/cachix/install-nix-action).


Updates `cachix/install-nix-action` from 31.11.1 to 31.11.2
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](https://github.com/cachix/install-nix-action/compare/13d8dd58da0234aa297dedd986986ccb8e7f3e24...448cde739abec1eaf0646249e3d25bb8d9bc7702)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.11.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-18 17:05:55 -04:00
Ahmad MuandBrandon Thomas 38999b3e90 fix(edit): isolate added marks from unfinished marked content (#3279)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 17:05:44 -04:00
Perchenko Anton 6fc17441ac fix(forms): edit button labels in field properties (#3064)
* M6: edit push-button labels in field properties (#2892)

* M6: keep roadmap status blockquotes valid

* M6: keep button-caption roadmap notes minimal
2026-10-18 17:04:49 -04:00
cyfung1031 acdd8d251c M2: copy Flate back-references in runs (#2035)
* M2: copy Flate back-references in runs

* M2: fold Flate note into hayro-syntax row
2026-10-18 17:03:46 -04:00
Dylan M. Taylor a8cd797a91 ci: configure Dependabot for Cargo and GitHub Actions (#3304)
* ci: configure Dependabot for Cargo and GitHub Actions

* ci: add Cargo update entry for /crates/filters/fuzz in Dependabot

* ci: consolidate Cargo Dependabot directories in pdfcraft
2026-10-18 15:11:14 -04:00
Mawfyy 932bf9c6bf M4: Ctrl-drag copies pages in the Organize grid (#2997) (#3092)
* M4: Ctrl-drag copies pages in the Organize grid (#2997)

Dragging thumbnails in the Organize page grid with Ctrl held (Option on
macOS) drops copies at the gap instead of moving the pages: a "+" badge
by the pointer, the copies land at the gap selected, and one undo step
takes them out. The insertion point joins the engine edit
(Edit::DuplicatePages { at }) so the copy is one undo step rather than a
duplicate plus a move.

page_duplicate takes a 1-based "at" (the first copy becomes page "at"),
so agents can drive the same thing, and the UI control channel's
ui.drag modifiers now reach the input state -- which gestures that read
it (this copy-drag, and Alt-drag column select) need.

Closes #2997.

* fix(ui): take the grid's copy modifier from the runtime OS (#3092 review)

Option-drag in the Organize grid moved the pages instead of copying them in a Mac
browser: `cfg!(target_os = "macos")` is false in the wasm build, and eframe sets the
context's OS from the browser. `copy_modifier` now takes the context and asks
`commands::mac_shortcuts`, as the shortcut labels do.

The drag helpers in `tests/editing.rs` and `tests/control.rs` picked the modifier the
same compile-time way, so they would have sent Ctrl on a Mac host; both read the
context's OS now. The new regression test
`option_copies_and_ctrl_moves_when_the_context_reports_macos` drives a Mac context —
Ctrl moves, Option copies — and fails without this change.
2026-10-18 11:13:22 -06:00
Dylan M. Taylor 98cbd38a8e Merge pull request #3161 from dylanmtaylor/chore/drop-competitor-references
docs: drop references to competing commercial software
2026-10-18 11:31:27 -04:00
aiongg b0e5c6eba2 Merge pull request #3041 from aiongg/next
Integration batch: 38 PRs on main
2026-10-18 11:25:56 -04:00
aiongg 40d7b0bc47 Merge upstream main a12fe8f41 into next 2026-10-18 14:56:01 +00:00
aiongg eedf250b4c Merge upstream main ecffce69e into next
crates/cos/src/document.rs: both new accessors, header_offset (#2525) and reconstructed (#2029).
parity/acrobat-features.toml: the malformed-xref entry has both sides' evidence and notes (#2029 full save of a repaired file, #2717 table numbered from 1).
2026-10-18 14:50:20 +00:00
a12fe8f411 M4: custom document properties through the engine and the tools (#2076)
* M4: custom document properties through the engine and the tools (#organize.custom-properties)

/Info entries beyond the ones the spec names are custom properties:
organize lists them (custom_info) and set_info accepts any such name
after Acrobat's checks (no standard name, no empty name, no spaces,
delimiters, / or #, at most 127 bytes); inspection lists them too
(DocInfo::custom); doc_info reports them under custom and doc_set_info
sets or clears them. The Custom tab in Document Properties and the XMP
mirror stay planned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

* M4: custom document properties list the same before and after an edit

Review follow-up. The open-time reader (render/inspect.rs) now uses the
engine's rule (pdfcraft_organize::custom_info, trimmed, empty values
dropped): text strings only, NUL/whitespace trimmed, empty values left out,
and the 256 cap counting listed entries. Before, it also listed name
values (`/Dept /RnD`) and capped before filtering, so the list could change
after the first unrelated edit. An automation test opens a file with a name
value, blank, NUL-padded, empty and numeric entries and checks doc_info's
custom list is the same before and after setting the Title.

Drops the ROADMAP.md log line, which conflicted with main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

* M4: custom properties drop NUL/whitespace padding in one pass, before the cap

Review follow-up. custom_info now trims NULs and whitespace in any mix
(" \0Lab\0 " is "Lab"), leaves out values that are only padding, and does
both before taking 256, so blank entries can't crowd real ones out. It is
the one rule: the engine uses it as it is, and the open-time reader trims
the same way. Tests: 300 blank entries before a real one, and mixed
padding, in organize and through doc_info.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 10:49:30 -04:00
Dylan M. Taylor 9a7824557e docs: use generic office terms and clarify fidelity harness and parity tracking reference 2026-10-18 10:49:21 -04:00
Dylan M. Taylor feb6c38580 docs: drop references to competing commercial software 2026-10-18 10:48:33 -04:00
mtlcl410andClaude Opus 5.5 ecffce69ee fix(engine): Save writes a repaired file in full even without edits (#1937) (#2029)
* fix(engine): Save writes a repaired file in full even without edits (#1937)

A damaged file is repaired on open (its cross-reference index rebuilt from
object headers, as Document Properties reports), but Save wrote it back as it
was: `Session::save_bytes` returned the bytes as opened whenever nothing was
edited, and a repair is not an edit. The "repaired" file on disk was still the
damaged one.

`save_bytes` now writes a reconstructed document in full even with no edits.
`Document::reconstructed()` in cos names the condition (no revision chain:
rebuilt, or built from scratch), and `write_incremental` uses it too, so the
writer and the engine share one definition.

Tests: `engine::tests::a_repaired_file_is_rewritten_by_save_even_without_edits`
(table offsets shifted; no table at all) and
`automation::saving_a_repaired_file_puts_the_repair_on_disk` (doc_open,
doc_info, doc_save over the same path, reopen). Both fail with the condition
removed: the saved bytes equal the damaged file. parity: core.repair-broken-files
cites them and notes that a repair still does not count as an unsaved change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

* fix(engine): a signed repaired file keeps its bytes on Save without edits (#1937)

Review follow-up: writing a reconstructed file in full on Save puts the
repair on disk, but for a signed file it would also invalidate its
signatures without warning. Save now skips the early return only when the
document is reconstructed and not signed. A regression test signs a
document, appends a `startxref` that points nowhere (the signed bytes stay
intact, the cross-reference data must be rebuilt) and checks that Save
without edits returns those bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 10:47:28 -04:00
aiongg b6b02a1b27 Merge upstream main 1952a8880 into next
ROADMAP.md, docs/roadmap.md: union of both sides' lines, each once.
parity/acrobat-features.toml: the XFA entry has both sides' evidence and notes (#2604 page areas, #2813 CLI layout).
2026-10-18 14:22:30 +00:00
1952a88801 fix(cli): use laid-out pages for dynamic XFA forms (#2813)
Fixes #1645. For a dynamic XFA form, `info` reported the placeholder
page and field counts, and `text` and `render` read the placeholder
pages, while the app and the automation tools lay the template out.

The CLI now opens dynamic XFA forms through the engine session and uses
its laid-out document for page and field metadata, text and pixels.
`info` keeps the input's file size and also reports the original counts
as `pdf_pages` and `acroform_fields`; `text` checks the copy permission
before extracting. The input file is never written.

Checks run (rustc 1.99.0): cargo fmt --check; cargo clippy -p
pdfcraft-cli --all-targets -- -D warnings; cargo test -p pdfcraft-cli;
cargo xtask layers; cargo xtask assets.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:54:00 -04:00
6525140955 M3: type an exact zoom into the rail's zoom field (#1755)
* M3: type an exact zoom into the rail's zoom field

The rail's zoom percentage was a read-only label. It is now a text field:
Enter applies a typed percentage (150, 150%, 62.5 %), Escape or unparsable
input keeps the current zoom. The control channel's zoom option uses the
same parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M3: document parse_zoom_percent

* i18n: Vietnamese for the zoom field's tooltip

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:53:24 -04:00
Marco ArnulfoandBrandon Thomas b68b3c1119 M3: the Pages panel follows the current page (#2785)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:52:40 -04:00
2c4989413f M7: the Edit text caret is as tall as the row it is on (#2462)
In a region that mixes sizes (a 24 pt title over 12 pt lines), the
inline editor's caret on a small row was as tall as the region's first
size and reached into the row below. egui draws the caret at least one
row of the TextEdit's font tall, and that font had the paragraph's first
size. Each row already carries its source row's height, so the
TextEdit's font is now half the region's smallest size and only sets
that floor.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:51:52 -04:00
Andrew BarnesandBrandon Thomas 6ce3bc198b fix(gpu): request adapter-supported device limits (#2383) (#2433)
Clamp the surface device descriptor to the adapter's capabilities while
preserving its maximum 2D texture extent. Cover V3D shader variables,
lower GL capabilities, alignment ordering and capable-device defaults.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:50:42 -04:00
Ian HollowayandBrandon Thomas 1581cd1edd M10: keep the OCR scan fixture at its original page size (#2399)
Assisted-by: GPT-6

Signed-off-by: IanHollow <72767437+IanHollow@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:50:16 -04:00
hms5232Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>Brandon Thomas
a50ddd4de1 fix(i18n): zh-hant catalog uses Taiwan vocabulary consistently (#2856)
Several entries carried mainland-Chinese computing terms in a catalog
written in the vocabulary used in Taiwan:

- 丟失 → 遺失 (unsaved-changes warning)
- 郵政編碼 → 郵遞區號 (Zip Code field formats)
- 自簽名 → 自我簽署 (self-signed digital IDs, three entries)
- 灰度 → 灰階 (Grayscale Images, Print in grayscale; the rest of the
  file already used 灰階)
- 匹配 → 相符 (ten search/match entries; 符合 stays for the
  fit/conform sense: 符合頁面, 符合 PDF/A)

Translation column only — source keys, placeholders and ellipses
unchanged. ATTRIBUTION.toml hash re-pinned.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:49:36 -04:00