100 Commits
Author SHA1 Message Date
mtlcl410andClaude Opus 5.5 e6b9792539 M4: bookmarks show and set their title colour and style (ISO 32000-2 Tables 151, 152) (#4171)
* M4: the Bookmarks panel shows each bookmark's colour and style (#3193)

ISO 32000-2 §12.3.3: an outline item's /C is "An array of three numbers
in the range 0.0 to 1.0, representing the components in the DeviceRGB
colour space of the colour that shall be used for the outline entry's
text" (Table 151), and its /F flags say italic (bit 1) and bold (bit 2)
(Table 152). Both were ignored: every title was drawn in the theme's
text colour and face.

The inspector (OutlineItem) and the bookmark list (organize::Bookmark,
used once a document is edited) now read them: each component is kept
within 0-1, and black (the default) or a /C that isn't three numbers
reads as no colour. The panel draws bold titles in the semibold face,
italic ones slanted, and the colour as given, lightened towards the text
colour only as far as it needs to read on the panel (3:1), so dark blue
stays visible in the dark theme. bookmark_list reports color, bold and
italic where they differ from the defaults.

Mutants, each failing a test: italic or bold bit ignored (both readers,
the panel), black kept as a colour, no clamping, a four-number /C
accepted, colour ignored or truncated in the panel, the contrast floor
removed, always jumping to the text colour. Checks for non-finite
numbers were equivalent (clamping already covers any size), so they
are gone.

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

* M4: set a bookmark's style and colour from its menu or bookmark_set_style (#3193)

#3193 asks for a way to mark bookmarks by colour, for example chapters
that are rarely used. ISO 32000-2 §12.3.3 gives every outline item a
title colour (/C, Table 151, DeviceRGB, default black) and style flags
(/F, Table 152: bit 1 italic, bit 2 bold, default 0); the previous
commit shows them, and this one lets the user change them.

- organize: set_bookmark_color writes /C, removing it for black (the
  default) and refusing a component outside 0-1; set_bookmark_style
  sets or clears the bold and italic bits, keeps any bit the table
  doesn't define, and removes /F when no bit is left.
- engine: SetBookmarkColor and SetBookmarkStyle edits, undoable, under
  the same assemble permission as the other bookmark edits. Their undo
  labels are the menus' own names, already translated in every
  catalogue.
- ui-egui: the bookmark menu gains Style (Bold and Italic check boxes)
  and Colour (the comment swatch grid, with Other colour...). Only
  existing UI strings are used.
- automation: bookmark_set_style takes any of color ([r, g, b], 0-1),
  bold and italic, and changes only what it is given.

Mutants, each failing a test: no range check (library and tool), black
kept as /C, a bit not set, a bit not cleared, an untouched bit cleared,
/F 0 written, the bits swapped (library and menu), the tool accepting
nothing to change or ignoring bold/italic, the menu's colour ignored.

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

* M4: a colour and a style set in one bookmark_set_style call are one undo step (#3193)

Review on #4171 (CodeRabbit). Given both, the tool applied two edits,
so undo took back only the style and left the colour, and a failing
second edit left the first applied. They now go in one Edit::Batch
named "Style", all or nothing. "Colour" and "Style" join EDIT_LABELS,
and "Style" BATCH_LABELS, so the history can translate them.

The test sets colour and style together and undoes once; with the two
edits applied separately it fails.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:59 -04:00
mtlcl410andClaude Opus 5.5 ac41758cf3 M3: a Launch link that names a PDF opens it in PdfCraft (ISO 32000-2 §12.6.4.6) (#4185)
* M3: a Launch link that names a PDF opens it in PdfCraft (#4168)

Documents that cross-reference each other often use Launch actions to
open another PDF next to them (#4168: 268 links such as
..\se\se_ST.pdf). PdfCraft follows no Launch action, so clicking them
did nothing, while the same link written as a GoToR opens.

ISO 32000-2 §12.6.4.6: a Launch action "launches an application or
opens or prints a document". Starting programs is not something a
document should be able to do, but opening a PDF is what a remote
Go-To already does. So a Launch action whose /F names a .pdf file, and
that has no /Win launch parameters (application, operation,
arguments), is read as the remote Go-To to that file's first page
(LinkTarget::RemoteGoTo with launch: true). It then goes through the
same path as one: PDF only, --root confinement, confirmation for
absolute paths, opened as a document and never handed to the OS.
Windows writers separate such paths with \, where §7.11.2.1 uses /, so
a path without any / has its \ read as /. doc_info's link target says
"action": "Launch" or "GoToR".

Tests: launch_actions_that_name_a_pdf_open_it_like_a_remote_link
(relative with \, a file specification, an executable, /Win, a bare
.pdf, a mixed path, a URL file system) and
a_launch_link_to_a_pdf_opens_it_like_a_remote_link (link_open opens
the PDF in the root; a program link opens nothing). Mutants, each
failing: the /Win check dropped, \ always or never read as /, any
extension, a bare ".pdf", case-sensitive ".pdf", Launch not read.

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

* M3: a Launch link with a stray /D still opens its PDF (#4168)

Launch (ISO 32000-2 Table 207) has no /D, but launch_pdf went through
remote_go_to, which rejects the action when /D isn't a valid GoToR
destination. A Launch naming c.pdf with /D 0 stayed Other(Launch).
The /F parsing is now shared (action_file) and Launch ignores /D.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:32 -04:00
mtlcl410andClaude Opus 5.5 20a395c0ae M5: sticky notes draw the icon they name (ISO 32000-2 Table 175) (#4182)
A note's /Name picks its icon, and ISO 32000-2 §12.5.6.4, Table 175 asks
every viewer to provide icons for seven standard names: Comment, Key,
Note, Help, NewParagraph, Paragraph, Insert ("Additional names may be
supported as well"). PdfCraft drew two shapes: Comment, and a page for
everything else. So choosing Key, Help, Insert, NewParagraph or
Paragraph in Properties changed nothing visible, and notes in received
files that use these names, or further ones such as Check or Star, all
looked like the same page. Recolouring such a note redrew it as a page.

note_icon now draws a shape of its own for each standard name and for
seven further names in common use (Check, Circle, Cross, CrossHairs,
Star, RightArrow, UpArrow). They are simple geometric drawings written
for PdfCraft, filled in the note's colour with a dark outline like the
existing two. NoteIcon gains the further names (NoteIcon::ALL); the
Properties chooser and comment_add offer all fourteen. Another name
still draws as Note, the table's default.

Tests: every_note_icon_draws_its_own_shape (all fourteen differ; an
unknown name and a missing one draw Note) and
a_star_note_keeps_its_star_when_recoloured. Mutants, each failing them:
from_name limited to the standard seven, Star or Key drawn as Note, an
unknown name drawn as Comment. One survivor reshaped Circle while
keeping it distinct, which the distinctness test rightly allows.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:26 -04:00
mtlcl410andClaude Opus 5.5 4825b67d15 M10: odd and even pages are page numbers, as Acrobat counts them (#3984) (#4034)
* M10: odd and even pages are page numbers, as Acrobat counts them (#3984)

Even pages only with the range 4–11 printed pages 5, 7, 9 and 11: `narrow` took odd and
even places in the chosen pages, and its comment said Acrobat does the same. Acrobat
counts page numbers: "if you set the range to 2, 7–10 and choose Even Pages Only, only
pages 2, 8, and 10 print" (Adobe Acrobat DC Help, last updated 11/28/2019, Options in the
Print dialog box, p. 515; the same example in Using Adobe Acrobat 9 Pro, p. 413). Odd and
even now keep the pages whose number is odd or even, for a typed range, picked thumbnails
and the current page alike, in the order given, and Reverse turns that round. The header,
footer and watermark ranges (marks_ui) and Extract's odd/even filter in #3729 already
count page numbers.

The new test pins Acrobat's example and the report's ranges. select_listed's tests and
cut_stack_printing_through_tools counted places; the latter now asks for even pages of
2–10, which by page number are the 2, 4, 6, 8 and 10 it printed before, so its expected
output is unchanged and would fail with places (3, 5, 7, 9).

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

* M10: say that odd and even are page numbers in doc_print, and what page labels still lack (#3984)

Review on #4034: the doc_print description says odd/even count page numbers, and the parity note says a document with page labels still counts page numbers there, which the Acrobat help consulted doesn't settle.

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

* M10: cite the cut-stack print test at its path under tests/it (#3984)

main moved the automation integration tests into tests/it, so the
evidence path added for odd/even printing no longer existed and
`cargo xtask parity` failed after the rebase.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:02 -04:00
mtlcl410andClaude Opus 5.5 7a2907601d docs: patch (38)'s regression in the vendor README's Test column (#2946)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-22 08:40:08 +08:00
mtlcl410andClaude Opus 5.5 09668ce885 M10: a swapped post 2.0 table's glyph names serve /Differences (#2946)
Review follow-up on #2949.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-22 08:40:08 +08:00
mtlcl410andClaude Opus 5.5 b67a8bf268 M2: a post table version with swapped halves doesn't make a font unusable (#2946)
Vendored hayro-interpret refuses an embedded font whose `post` table
version isn't 1.0, 2.0, 2.5 or 3.0 (a guard from pdf.js issue 9462).
ScanSoft PDF Create! writes the version of every font it embeds with its
16-bit halves swapped (`00 00 00 03` for 3.0), so those fonts were refused
and their text drawn with a substitute: Hebrew came out as Latin
look-alikes or not at all.

1.0, 2.0 and 3.0 with swapped halves are now accepted too; any other
unknown version is still refused. PdfCraft patch (38) in vendor/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-22 08:40:08 +08:00
mtlcl410andClaude Opus 5.5 6411cc803f M5: a Replace text comment is named Replace text, not Strikethrough (#3947)
The Replace text tool saves a StrikeOut with /IT /StrikeOutTextEdit and a caret
reply. The Comments panel named it by subtype only, and tool_for mapped it to the
Strikethrough tool, so Make Current Properties Default on it changed the
Strikethrough tool's defaults. Both now read the intent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-22 07:47:05 +08:00
mtlcl410andClaude Opus 5.5 4ac31b3ffe M9: stop the side-by-side frame as soon as either half has closed (#3915)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-22 05:07:42 +08:00
mtlcl410andClaude Opus 5.5 f945eff68a M9: closing a half of a side-by-side comparison keeps the app running (#3915)
Drawing a half can close its own tab (the snapshot bar's Close), which removed
or shifted the view indexes compare_area kept using: the right half's close
panicked at the scroll sync, the left half's in document_area for the other half.
Each half is now found again by id, and the frame ends when one is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-22 04:56:40 +08:00
mtlcl410andClaude Opus 5.5 81c08be3ea M6: a field without /TU shows no tooltip of its own (#3854)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-22 03:37:01 +08:00
mtlcl410andClaude Opus 5.5 33150203a8 M6: test the read-only and blank-/TU tooltip cases (#3854)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-22 03:37:01 +08:00
mtlcl410andClaude Opus 5.5 7ddb6226b8 M6: hovering a form field shows its tooltip (#3854)
The field's /TU is read into FormField::tooltip and shown in the Fields
panel, but resting the pointer on the field on the page showed nothing.
page_input now attaches it to the hover, as Acrobat does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-22 03:37:01 +08:00
mtlcl410andClaude Opus 5.5 de25be16e5 test: the off-page paragraph test compares the gap with the same pixels without Edit text (#3061)
Review on #3070: the gap between pages isn't one colour on every platform;
on ubuntu CI the pixel at x = 416 was one level off the fixed sample taken
40 px from the page's right edge. The test now takes a screenshot of the
same pages before Edit text is on (no paragraph outline), checks that the
pages stay where they were, and compares every gap pixel with the same
pixel there. Removing either clip still fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-20 18:50:05 +08:00
mtlcl410andClaude Opus 5.5 d5002807be M10: a turned fill or clip covers the page only if its corners do (#2992)
Review on #2994: fill rectangles and the clip were kept as their page-space
bounding boxes, so a rectangle turned or sheared by the CTM could pass as
covering the page while leaving its corners out, and a following fill became
Word's page colour. Each rectangle is now kept as its four page-space
corners, the clip as the rectangles it intersects, and the crop box counts as
covered when its corners lie inside each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 ca0e0b08f3 M10: a fill whose second rectangle cuts a hole is no page background (#2992)
Review follow-ups on #2994: under even-odd any second rectangle, under nonzero
one wound the other way, can leave a hole the Word page colour would cover.
The parity note says the page colour hasn't been opened in Word yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 8707e87aff M10: give the #2806 export tests' pages a background after rebasing (#2992)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 d85d49a4c6 M10: Export to Word keeps the page background colour (#2992)
A page coloured with Edit a PDF > Background exported to Word as a
white page: export pages carried text and images only, and the Word
section has no page colour.

- pdfcraft-edit `page_background`: the colour of the first thing a
  page paints, when that is an opaque fill (no soft mask, Normal blend)
  in a plain gray, RGB or CMYK colour, of a path with a rectangle
  covering the visible page, not clipped smaller; `None` otherwise.
- `export::Page::background` carries it; the Word writer sets it as
  the document background (`w:background`, plus the
  `w:displayBackgroundShape` setting Word needs to show it) when every
  page shares one non-white colour. Word has one background per
  document, so differing colours are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 f6255c0ed6 M3: numbered glyph names read as nothing again, not as a StandardEncoding guess (#3169, #3170)
Review on #3239: a name that only numbers the glyph (C39, c251) says
nothing about the character. Reading it as the code's StandardEncoding
character invents text wherever the font's /Encoding puts the glyph
elsewhere: with /C1 at 233 it extracted O-slash for a glyph drawn as
e-acute. numbered_glyph_name and the fallback are removed; the AGL
specification's full mapping stays.

The test puts one glyph per line: the test font has none of these glyphs,
so they have no width, and on one line a later 'fi' sat on the first and
was dropped as its fake-bold copy once the guessed glyphs between them
were gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
2026-10-20 15:54:51 +08:00
mtlcl410andClaude Opus 5.5 da351cda8f fix(forms): cache field appearances drawn for display, redraw only what changed (#3470)
Review follow-up (CodeRabbit on #3475). A form edit refreshes the display
copy on every keystroke, and with /NeedAppearances that redrew every field
of the form: 68 ms per edit on a 1000-field form against 7 ms on main.

FieldAppearanceCache keeps each widget's drawing with the objects it was
drawn from: the widget and its field ancestors, the form dictionary and
the fonts of its /DR, each with the objects its entries refer to. An
entry is used only while each of them is still the same object
(Arc::ptr_eq); the document replaces an object it edits, so a changed
input is a miss, never a stale hit. The engine keeps one per editor. A
field edit on that form now takes 13 ms.

The repair note on a failed display write now names field appearances
too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 22:26:31 +08:00
mtlcl410andClaude Opus 5.5 b661ba4584 fix(forms): draw fields the file leaves to the viewer, for display only (#3470)
A form with /NeedAppearances true (ISO 32000-2 Table 224: the writer
"has not provided appearance streams for all visible widget
annotations") showed its text fields empty: PdfCraft only read the flag
when writing, and the display copy that draws missing appearances skips
widgets. Filled forms from tools that leave drawing to the viewer looked
blank, while Acrobat, Firefox and MuPDF show the values.

pdfcraft_forms::with_field_appearances returns a copy with text and
choice widgets drawn from their values (every one when the flag is set,
otherwise those with no /AP /N), with the generator a typed value uses.
The engine's display copy (page view, page images, Print as Image, CLI
render) applies it after the comment appearances. The document itself
is not changed, so saving writes the file as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 21:53:42 +08:00
mtlcl410andClaude Opus 5.5 23a032ef3b fix(engine): a reload keeps reading colours and hidden comments (#3443)
`Session::reload` (the file changed on disk) opens the new bytes with a
default render config, so a document shown white on black, or with its
comments hidden, went back to the file's own colours and comments until
the preference was toggled. The UI re-applies the preference only on
open. Reload now carries both display settings over and rebuilds the
renderer with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 19:17:21 +08:00
mtlcl410andClaude Opus 5.5 bae6981959 fix(render): the device decides whether a knockout group's image clears its rectangle (#2971)
Review on #2974: the interpreter decided with a full-resolution decode
(`decoded_raster(None)`) before the renderer decoded at its draw size, a
second decode of every downscaled image under another cache key; and an
image a preview skips for its size still cleared what was under it.
`Device::knockout_image` now lets the renderer decide, with the same
eligibility and decode size as `draw_image` (shared `image_draw_size`), so
the draw reuses that decode and a skipped image clears nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 16:34:34 +08:00
mtlcl410andClaude Opus 5.5 3fff309dd3 M3: a link followed in full screen cancels the click's pending page turn (#1459)
Review on #2070: a click on the page schedules a turn after the
double-click time, and a link clicked before then jumped and then the page
still moved on. Following a link now drops the pending turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 16:21:20 +08:00
mtlcl410andClaude Opus 5.5 a6b2ad7fb5 fix(render): resolved JP2 palette colours keep 8 bits whatever the index precision (#3179)
Review on #3184: with no /BitsPerComponent the decoded image took the
codestream's precision, which for a palette image is that of the indices.
The resolved colours are decoded to 8 bits, so a 1-bit index rescaled them to
0 or 255 per channel ((64, 128, 192) became (0, 255, 255)). When the palette
is applied the samples are now reported as 8-bit; an image without a palette,
or one whose raw indices are kept for an /Indexed space, keeps its precision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 16:10:17 +08:00
mtlcl410andClaude Opus 5.5 3df8a6386d fix(render): a knockout clear follows a shading's /BBox and skips images that don't decode (#2971)
Review on #2974. In the erase pass `set_paint` returned no clip, so a
shading fill cleared its whole path while the redraw is clipped to the
shading's /BBox (ISO 32000-2 Table 77): red outside the box stayed erased.
It now returns the shading's clip there too. And an unmasked image cleared
its placement before decoding, so an image that doesn't decode (over
MAX_IMAGE_PIXELS, damaged data) erased the group's earlier artwork and drew
nothing; the clear now happens only when the image decodes, asked only in a
knockout group (Device::in_knockout_group). The stencil case in the test now
also checks that the stencil draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 16:03:57 +08:00
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 771129594c test(render): the non-separable blend modes keep their colour over an empty page too (#2970)
Hue, Saturation, Color and Luminosity blended with the white paper the
same way; the fix covers them, and the test now checks them over an
empty part of the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:39:51 +08:00
mtlcl410andClaude Opus 5.5 8836088551 fix(render): blend modes over an empty part of the page keep their colour (#2970)
Since #1557 the page background was painted inside the page
transparency group, as its initial backdrop, so that a Multiply
highlight under reading colours blends with the dark background. With
document colours that made every object blend with the white paper:
a green /BM /Screen fill over an empty part of the page came out
white, and Difference or Exclusion magenta. Only Normal, Multiply,
Darken and HardLight, for which blending with white gives back the
source colour, looked right. 0.8.0 was correct.

The page's objects composite with an initially transparent backdrop
and the page group is then put on the paper (ISO 32000-2, page group).
Vendored hayro patch (18) now paints the background inside the group
only under reading colours, where #1548 needs it, and outside it
otherwise, as before #1557.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:39:50 +08:00
mtlcl410andClaude Opus 5.5 82023e2ffe fix(render): the one-cell tiling renderer starts outside any knockout group (#2971)
After rebasing onto main: #3356's one-cell pattern renderer (merged) builds
a Renderer without the knockout fields this PR adds. Like the tiled cell's
renderer, a pattern cell is painted as its own artwork, so it starts with
no knockout group open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 15:38:36 +08:00
mtlcl410andClaude Opus 5.5 2a93b3152c test: the knockout fixture's nested form declares its real /Length (#2971)
Review follow-up on #2974: the nested group's checks no longer depend on the
parser repairing a short /Length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 15:37:30 +08:00
mtlcl410andClaude Opus 5.5 2627384757 fix(render): images and nested groups knock out in a knockout group too (#2971)
An image or a nested group is drawn into a layer of its own, which
covers the whole canvas (transparent where nothing was drawn), so it
can't be composited with Compose::Copy. What is under its shape is
now cleared first and the layer then composites over it:

- an unmasked image's shape is its placement rectangle: the
  interpreter hands it to the device (`Device::knockout_shape`), which
  clears it with a transparent Copy fill;
- a nested group's shape is the union of its elements' shapes: the
  interpreter draws its content once in the device's erase mode
  (`knockout_erase_wanted`, `begin_knockout_erase`,
  `end_knockout_erase`), where every paint clears under its shape and
  nothing is layered or drawn as an image, and then draws the group,
  always in a layer of its own (drawn straight onto the knockout
  group, its own elements knocked each other out).

A masked image's shape is its mask; it composites as before, which
matches MuPDF where the mask is opaque or transparent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:37:30 +08:00
mtlcl410andClaude Opus 5.5 d2bcda45fd fix(render): knockout transparency groups knock out (#2971)
In a knockout group (/K true) each element composites with the group's
initial backdrop, not with the elements drawn before it (ISO 32000-2,
knockout groups). The renderer read /K (hayro-interpret patch 28) but
drew such a group like any other, so overlapping semi-transparent
shapes in it blended with each other.

hayro-interpret patch (39) tells the device a group is a knockout
group (`Device::set_knockout`, default no-op). hayro patch (19) then
draws that group's fills, strokes, glyph outlines and shadings with
Compose::Copy, which vello_cpu weights by coverage, so each replaces
what the group drew under its shape. Images and nested groups are
drawn into a group layer of their own and still composite normally:
copying a whole layer would erase everything outside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:36:46 +08:00
mtlcl410andClaude Opus 5.5 312a97d21c fix(render): restore the test's closing brace lost when rebasing onto main (#3179)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 15:33:25 +08:00
mtlcl410andClaude Opus 5.5 857fef3b1a fix(render): say that an unresolvable /ColorSpace also gets the JP2 palette (#3179)
Review follow-up: num_components is unset both when the dictionary has no
/ColorSpace and when it has one that does not resolve. In the second case
the JP2 header's colours are the only ones available, so applying the
palette is intended; the comment and vendor/README.md now say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:32:53 +08:00
mtlcl410andClaude Opus 5.5 e1aeb00be9 fix(render): apply the JP2 palette of a JPX image with no /ColorSpace (#3179)
hayro-syntax decoded every JPX image with resolve_palette_indices off. That
is right when the image dictionary gives a /ColorSpace (an /Indexed space
needs the raw indices), but with no /ColorSpace the colours come from the
JP2 header (ISO 32000-2 §7.4.9) and nothing applied its palette: the
indices were drawn as a gray image. The palette is now resolved exactly
when the dictionary has no /ColorSpace.

Two unit tests decode a 4x2 palette JP2 written for the test: without a
colour space it is red/blue RGB, with /Indexed the raw indices stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 15:32:47 +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
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
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
mtlcl410andClaude Opus 5.5 49fa11942f M10: glyph names with lowercase hex digits map to nothing (#3170)
Review on #3239: the Adobe Glyph List Specification's `uni` and `u` forms
take uppercase hexadecimal digits only (section 2), and its examples map
`uni20ac` to an empty string. glyph_name_to_text accepted lowercase too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 12:22:00 +08:00
mtlcl410andClaude Opus 5.5 0de546587d fix(windows): present Direct3D 12 through a composition visual, so resizing doesn't stretch (#2738)
While the window was being resized, Windows stretched the window's swap
chain, still at the old size, to the new one: the text shimmered and the
left panel's width bounced for as long as the drag lasted. A DirectComposition
visual's last frame is shown at its own size instead. WGPU_DX12_PRESENTATION_SYSTEM
still overrides it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 09:09:01 +08:00
mtlcl410andClaude Opus 5.5 88e7a1ce86 test(render): a shared-edge pixel keeps the colour at its corner (#3035 review)
Review follow-up: a red/blue regression for the centre-coverage pass. The
shared diagonal sits at x + y = 50.25 in device space (`cm` moves the mesh
down a quarter point), so pixel (20, 30) has its corner in the red triangle
and its centre in the blue one. It must stay red with either triangle
first; replacing `or_insert_with` with `insert` turns it blue when the red
triangle comes first, and the test fails.

Rebased onto main; the vendored hayro-interpret patch is renumbered 36
(41 before; 35 is taken by PR #2977).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 06:16:29 +08:00
mtlcl410andClaude Opus 5.5 421a0dd9c9 fix(render): mesh triangles that share an edge leave no seam
The mesh sampler took each pixel's colour at its corner, and only when the
corner was strictly inside a triangle. A corner on an edge two triangles
share belonged to neither, so a Type 4-7 shading drew a white seam along
every shared edge that ran through the sample grid, and dropped pixels
along the mesh's own sides (a 30 x 30 square cut along its diagonal:
812 of 900 pixels; MuPDF 900).

Such a pixel now goes to the triangle that covers its centre, edges
included (Triangle::covers), and still takes the colour at its corner, so
every pixel that was drawn keeps its colour. Vendored hayro-interpret
patch 41.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:16:05 +08:00
mtlcl410andClaude Opus 5.5 785d10724f fix(render): pattern-filled stencil masks take the fill paint at the opacity once (#2975)
Review follow-up. A pattern paint keeps the fill or stroke opacity it was
set with (`scn`), so resetting the graphics state's fill alpha did not stop
a pattern-filled stencil mask from being drawn at the opacity squared:
`Pattern::without_opacity` now drops it for the mask's paint (patch 35).
The renderer also painted a pattern-filled stencil mask with the stroke
paint, so an uncoloured tiling pattern drew in its stroke colour (black by
default); it now uses the fill paint.

The stencil test covers a colour fill, an uncoloured tiling pattern and a
shading pattern at ca 1, 0.5 and 0.25; each of the three changes, removed,
makes it fail. Rebased onto main; the vendored hayro-interpret patch is now
number 35 (36 was taken on main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:07:46 +08:00
mtlcl410andClaude Opus 5.5 1cf6ca5579 fix(render): a stencil mask is drawn at the fill opacity once (#2975)
hayro-interpret draws every image XObject inside a transparency group
that applies the fill opacity (ca). A stencil mask (/ImageMask true)
also takes the fill colour, with that same alpha, as its paint, so the
opacity was applied twice: at ca 0.5 a mask was drawn at 25 %, at
ca 0.25 at about 6 %. Rectangles and raster images were right.

Vendored hayro-interpret patch (36) resets the fill alpha to 1 once
the image's group is pushed, before the stencil's paint is taken; the
saved state is restored after the image. Type 3 glyph and pattern
stencils take their paint from the glyph or pattern and are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:07:09 +08:00
mtlcl410andClaude Opus 5.5 d0051fe68b M10: a visibility expression naming an object that isn't a listed group is malformed (#2986)
Review follow-ups on #2987; the patch (40) test reference moves into the Test column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 06:05:16 +08:00
mtlcl410andClaude Opus 5.5 39b96ccf3d fix(render): a visibility expression decides what an OCMD shows (#2986)
An optional content membership dictionary may carry a visibility
expression (/VE, PDF 1.6): [/And e...], [/Or e...] or [/Not e], each e an
optional content group or a nested expression. It takes precedence
over /OCGs and /P (ISO 32000-2 8.11.2.2). hayro-interpret ignored it, so
content under such a dictionary stayed visible when its layers were
hidden in the Layers panel.

Vendored hayro-interpret patch (40) evaluates the expression, nested
up to 32 deep. A malformed expression (unknown operator, /Not without
exactly one operand, /And or /Or without any, an operand that is
neither a group nor an expression, too deep) falls back to /OCGs and
/P as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 06:05:00 +08: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
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
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
mtlcl410andClaude Opus 5.5 3ef3c9efa9 M8: tag cleanup refuses what it can't finish instead of removing the tree (#3302)
Maintainer review on #3285: an element written in place that owns redacted
marked content or tags a removed annotation, an inherited page the lookup gave
up on (over 256 ancestors or a loop), and a walk past its limit are now
RedactError::Tags, so nothing is changed. Removing only /StructTreeRoot left
outline /SE and GoTo /SD reaching the elements. Inherited pages are cached per
ancestor. clear_every_element and drop_tree are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 03:20:34 +08:00
mtlcl410andClaude Opus 5.5 ecb990c8ce M10: glyph names: a uni part with a surrogate group maps to nothing; numbered names read under a suffix (#3170)
Review follow-ups on #3239.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 01:57:14 +08:00
mtlcl410andClaude Opus 5.5 36cfc8566e M10: dynamicRender is read only from markup whose closing tags match (#3180)
Review follow-up on #3192.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 01:53:12 +08:00
mtlcl410andClaude Opus 5.5 f62bf71a77 M8: tag cleanup keeps inherited pages, fails closed over replaced streams and in-place elements (#3302)
Review follow-ups on #3285: walk /K before the parent and ID trees and look up
an element's inherited /Pg through /P; drop the structure tree when a cut-short
walk could leave MCRs naming a replaced stream; drop it when an element written
in place tags a removed annotation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 01:33:42 +08:00
mtlcl410andClaude Opus 5.5 ce3c0e46c9 M8: redaction clears tag text of elements reached only through the parent or ID tree, and fails closed past the walk limit (#3302)
tags::clean, which drops /Alt, /ActualText and /E that repeat redacted
text, walked the structure tree from the root's /K only, so an element
listed only in /ParentTree or /IDTree kept the redacted words. It now
starts from both trees too, as drop_object_refs does since 3f543ca1.

Past its 2,000,000-reference cap the walk used to skip the rest and
report success. It now fails closed: every structure element in the
document, found by reading each object rather than through the tree,
loses its alternate and actual text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 00:52:18 +08:00
mtlcl410andClaude Opus 5.5 e6de108972 M8: note that structure elements written in place keep their references (#2447)
Review note from Losminek on #3285.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 00:44:58 +08:00
mtlcl410andClaude Opus 5.5 9194ec457f M7: the paragraph editor draws on its page only (#3061)
A paragraph running past its page's bottom edge, as HTML-to-PDF converters
write when they rely on the page clip, opened in an editor that covered the
next page with its box and text, and paragraph outlines crossed the gap
between pages. The editor is clipped to its page within the document area
(it also covered the tool bars when scrolled up), and outlines to their page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-19 00:38:46 +08:00
mtlcl410andClaude Opus 5.5 3f543ca17f M8: redaction drops OBJRs of elements reached only through the parent or ID tree (#2447)
Follow-up to #2532 (review by aiongg): the structure-tree walk started
only at the root's /K, so an element listed only in /ParentTree or
/IDTree kept its object reference to a removed link or comment, and the
root kept that annotation reachable in a full rewrite. The walk now
also starts from both trees and follows their nodes (/Nums, /Names,
/Kids), written in place or as their own objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-19 00:09:17 +08:00
mtlcl410andClaude Opus 5.5 5486100d04 chore: refresh catalogue hashes after rebasing onto main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-18 23:03:36 +08:00
mtlcl410andClaude Opus 5.5 6be953459c fix(print): the options read ends at its deadline and a dead read wakes the dialog (#1818)
Review follow-up on #2108:
- output_within waited on the reader thread with join, so a process the
  command started could hold the output pipe, and the wait, long past the
  limit after the command exited or was killed. The output now comes back
  over a channel with recv_timeout; such a reader finishes on its own.
- the printer-list and options workers ask for a frame from a guard
  dropped on exit, also when the source panics, so an idle dialog sees
  the closed channel without waiting for another event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-18 23:03:31 +08:00
mtlcl410andClaude Opus 5.5 b59b91f0be fix(print): Vietnamese for the printer-options loading note (#1818)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-18 23:03:31 +08:00
mtlcl410andClaude Fable 5.1 e3847ba076 fix(print): pass the lpoptions limit outside the stand-in command in the CUPS test
The limit landed inside `command(...)` in one Unix-only test, which this
Windows machine never compiles; checked now with a Linux target.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-18 23:03:31 +08:00
mtlcl410andClaude Fable 5.1 7ec435eb4d fix(print): Properties… reads CUPS driver options off the GUI thread, with a deadline for lpoptions (#1818)
The Print dialog's Properties… ran lpoptions on the GUI thread with no
deadline, so a CUPS that stopped answering froze the window. The read
now runs on its own thread (the panel says so meanwhile, and a read
that dies offers nothing rather than waiting), and lpoptions gets ten
seconds through spool::output_within, which kills and reaps a command
at its deadline; the PPD's own defaults then stand. One UI string in
the 21 catalogues.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-18 23:03:31 +08:00
mtlcl410andClaude Opus 5.5 024407992d style: cargo fmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-18 22:56:57 +08: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
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
mtlcl410andClaude Opus 5.5 4899072089 fix(forms): read dynamicRender only from the configuration packet itself (#3180)
Review follow-up: in a whole-XDP stream, a config/acrobat/acrobat7/
dynamicRender path inside xfa:datasets/xfa:data came before the real
config packet and decided the form type. The lookup now follows direct
children only: the config child of the xdp root (or the config packet
itself), then acrobat, acrobat7 and dynamicRender.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-18 22:14:02 +08:00
mtlcl410andClaude Opus 5.5 2bd6d1357d M5: flattening comments leaves 3D, multimedia and print-production annotations (#3087) (#3089)
doc_flatten with comments chose every non-widget annotation except Link
and Popup, so a comments-only flatten baked in and deleted 3D, Screen,
Movie, RichMedia, PrinterMark, TrapNet and Watermark annotations with
their data (a 3D annotation's U3D model and default view). The comments
branch now uses pdfcraft_annot::is_comment_subtype, the classifier the
comment list uses; widgets follow `fields` only.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:48:26 -04:00
mtlcl410andClaude Opus 5.5 f0c275669a M7: never encode typed text with CID 0 (#3223) (#3235)
Word's Type 0 subsets map CID 0 (.notdef) as well as the real space glyph
to U+0020 in ToUnicode and list a width for CID 0. encode keeps the lowest
code for each character, so an edited line wrote every space as <0000> and
drew a box where each space was. CID 0 is .notdef whatever ToUnicode says,
so has_glyph now refuses it and the space takes the real CID.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:48:14 -04:00
mtlcl410andClaude Opus 5.5 1c338b3ea6 M8: a phone number takes one slash, right after its area code (#3205)
Review follow-up: "0426/12/3456" and "0426 12/3456" matched, since any
slash between digits counted. Only one slash is allowed now, and only
right after the first (area code) group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-18 19:48:53 +08:00
mtlcl410andClaude Opus 5.5 0b731453d3 M3: extract text from glyph names outside the Adobe Glyph List (#3169, #3170)
A simple font without a usable /ToUnicode reads its text from glyph
names, and a name missing from the glyph list dropped the glyph from the
extracted text while the page still drew. An 821-page book set in Type1C
fonts named C39, C40, ... extracted 4 KB of front matter and nothing else.

- glyph_name_to_text applies the Adobe Glyph List Specification's full
  mapping: drop from the first '.', split on '_', map each component
  through the list or as uniXXXX / uXXXX. f_i, f_l and hyphen.case now
  read as fi, fl and '-'. Type 1, CFF, standard and embedded TrueType
  simple fonts use it.
- A Type 1 / CFF / standard font name that only numbers the glyph (one
  or two letters then digits, ZapfDingbats' aN excepted) reads as the
  code's StandardEncoding character, so C174 is the fi ligature and C208
  an em dash.

The book now extracts 1.9 MB (pdftotext: 1.85 MB), with fi and em dashes
where pdftotext writes Latin-1 mojibake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-18 19:46:39 +08:00
mtlcl410andClaude Opus 5.5 73aea6e0ac M8: phone pattern finds slashed and labelled unseparated numbers (#3205)
Italian documents write landlines with a slash between area code and
number (0426/123456), and mobiles without separators (3451234567,
+393451234567). The phone pattern needed a separator other than a slash,
so these stayed readable after redaction.

- A slash joins two digits in a number with a trunk-0 area code; a
  North American or + number with a slash still doesn't match, so
  reference numbers like 2024/123456 stay unmarked.
- A number without separators matches only right after a phone label
  (Tel., cell., Fax:, telefono, mobile, ...). Unlabelled runs of digits
  (ids, VAT numbers) still don't match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-18 19:37:27 +08:00
94f1a0d157 fix(engine): a new or recovered document stays unsaved through undo (#2346)
* fix(engine): a new or recovered document stays unsaved through undo

#1435 made undo back to the last saved bytes clean. For a document
created by Combine, Extract or Create, or restored from recovery, those
bytes were never written where the document belongs, so edit then Undo
made it clean: Close no longer asked to save and autosave stopped keeping
it. Editor::unsaved_file, set by open_new and mark_recovered and cleared
by mark_saved, keeps such a document unsaved until it is saved; Revert
follows the same rule.

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

* fix(engine): test that doc_close refuses a recovered or new document after undo

Review follow-up on #2346: a recovered document and a new one, rotated
and undone, are refused by doc_close without discard_changes, and close
with it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:47:43 -04:00
mtlcl410andClaude Opus 5.5 2c4b84f6af M8: redaction removes the replies of the annotations it removes (#2659) (#2697)
* M8: redaction removes the replies of the annotations it removes (#2659)

A reply (/IRT) placed away from the mark survived, and its /IRT kept the
removed annotation - its comment text and the QuadPoints of the redacted
words - in the saved file. Replies to a removed annotation, and replies to
those, now go with it.

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

* M8: test replies listed before their parent, with a pop-up on a reply (#2659)

Review of #2697: the replies precede the annotation they answer in
/Annots, deepest first, and a reply has a pop-up. A single pass in
/Annots order would keep them; the test fails with the loop cut to one
pass, which the first test does not notice.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:45:46 -04:00
mtlcl410andClaude Opus 5.5 0f870c7b7d fix(forms): read dynamicRender only at config/acrobat/acrobat7, in UTF-8 or UTF-16 (#3180)
Review follow-up. The config packet is decoded as UTF-16 (byte order mark,
or `<` written as two bytes) or UTF-8 before the scan, as crates/xfa reads
packets. The scan walks tags, skipping comments, CDATA sections, processing
instructions and declarations, and takes the value only at
config/acrobat/acrobat7/dynamicRender, so the same name in form data, in a
comment or elsewhere in the config no longer counts.

The engine test now adds the element to the fixture's own config packet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-18 15:33:57 +08:00
mtlcl410andClaude Opus 5.5 e827a2a17c M6: a box drawn with points along its sides is detected as a box (#2942) (#2947)
Field detection read a path as a rectangle only when it had exactly four
corners, and gave up on a subpath once it reached five points. The French
income tax form 2042 draws its letter cells with a midpoint on each
vertical side (six points), so most of its cells were not found.

A subpath now keeps up to 16 points, and a point lying on a straight side
between its neighbours is dropped before the four-corner test. A point
that turns back along a side is not between its neighbours and stays, so
a path that doubles back is still not a box.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:14:36 -04:00
mtlcl410andClaude Opus 5.5 15af2860f1 fix(ocr): the text layer restores the rendering mode it sets (#3029) (#3033)
text_layer wrote '/OCR BMC BT 3 Tr /PCHelv 1 Tf ... ET EMC'. Tr and the
font are graphics state, and ET doesn't reset them, so every content
stream after the layer on the page inherited 3 Tr: a header or footer
added after OCR was in the file but drew invisibly, in PdfCraft and in
MuPDF. The layer is now wrapped in q ... Q inside its marked content.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:12:37 -04:00
mtlcl410andClaude Opus 5.5 4ff53a8d8e fix(engine): printing keeps layers hidden in the Layers panel hidden (#3046) (#3050)
The print-ready PDF copied the document's /OCProperties as they are, so it
printed the layers in their default state: a layer hidden in the Layers
panel disappeared on screen and came back on paper. Print as image already
used the view's layer choices (it renders with the document's config).

Once the layers have been changed, the copy imposed for printing now has
its default configuration (/D) set to them: /BaseState /ON, /ON the
visible layers, /OFF the hidden ones, its other entries kept. With the
layers untouched, the document's own configuration prints as before.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:12:26 -04:00
mtlcl410andClaude Opus 5.5 fe3e07c244 fix(forms): an XFA form whose config forbids dynamic rendering is static (#3180)
An XFA form with no AcroForm fields was always classified as dynamic, so
opening it laid the template out into new pages and appended them as a
revision: the pages the user saw, and any later save, were PdfCraft's
layout rather than the file's own pages, with nothing saying so. Adobe
Designer writes <dynamicRender>forbidden</dynamicRender> in the config
packet of a static PDF form; such a form is now static and its pages are
kept. /NeedsRendering still makes a form dynamic, and with no word from
the config a form without fields stays dynamic as before.

The config packet is found in an /XFA array or a whole-XDP stream, read
with a 16 MiB decode cap, and scanned for the one element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-18 15:07:38 +08:00
mtlcl410andClaude Opus 5.5 b15fe91627 test: the image-drag pixel test waits for the page raster
image_pixels_follow_the_frame_while_a_corner_is_dragged drags before the
asynchronous page raster has arrived on a slow runner, and the preview
(drawn from that raster) then draws no picture: 'not only at []' on
windows-latest for #3070 and #3071 (a deny.toml-only change). With a
300 ms render delay added locally, main's test fails the same way and
this one passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-18 07:42:07 +08:00
mtlcl410andClaude Opus 5.5 92990b2e2c M3: layer_set shows or hides a layer, and page_render draws with the document's view settings (#2986)
The Layers panel had no automation tool: doc_info listed the layers, but
nothing could show or hide one, so layer visibility could not be checked
headlessly. layer_set does what the panel's eye icon does, naming a layer
by name (every group with that name, as CAD exporters repeat names) or by
its 1-based position in doc_info's list.

page_render built its own RenderConfig with only the password and the
reading colours, so it ignored layer visibility and comments_hide as
well: hiding comments through the tool changed nothing it drew. The
automation renderer now takes the document's own RenderConfig
(Session::render_config), and both tools drop the cached renderer when
they change a setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 21:09:57 +08:00
mtlcl410andClaude Opus 5.5 0b77f4cb77 fix(ui): Fill & Sign font size resizes the selected text (#1274)
The Font size control on the Fill & Sign bar only set the size of text
typed afterwards; text already placed and picked up kept its size, so the
only way to resize it was to type it again. With a placed Fill & Sign text
or date selected, a size typed into the control (or the end of a drag) now
applies to it through Edit::FontSizeAnnotation, one undo step per change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 20:21:22 +08:00
mtlcl410andClaude Opus 5.5 4a7d556a32 fix(cos): write whole reals past the 32-bit integer range as reals
write_real printed a whole real below 1e15 as an integer, and a larger one
through Display, which has no decimal point either. So 4e9 or 1e30 was
written as an integer token outside the range ISO 32000-2 Annex C gives
integers (±2^31-1), and read back as Int (or, in readers with 32-bit
integers, overflowed). Whole reals now use the integer form only within
that range; past it they keep a trailing '.' (§7.3.3 allows "4.").

Raised in the review of #2877.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 17:46:05 +08:00
mtlcl410andClaude Opus 5.5 5b813ce985 fix(sign): explain 0xC0000225 when a token's key provider can't find the key (#1535)
STATUS_NOT_FOUND passed through by a smart card or token provider (an A3
certificate whose token isn't connected) was shown as a bare code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 16:39:54 +08:00
mtlcl410andClaude Opus 5.5 07524f5e8c fix(sign): explain 0x8009001D when a store key's provider fails to load (#2207)
Windows reports NTE_PROVIDER_DLL_FAIL when the key storage provider
holding a certificate's private key can't load its DLL: smart card or
token middleware that is missing or broken, or a provider that exists
only as 32-bit code a 64-bit process can't load. The certificate list
showed it only as "Windows error 0x8009001D"; it now says what that
means, as it already did for a missing card or provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 15:29:59 +08:00
mtlcl410andClaude Opus 5.5 f5a2376c86 fix(redact): a structure tree too large to walk goes whole (#2447)
Review follow-up on #2532: drop_object_refs stopped visiting at 2,000,000
references and still reported success, so a reference beyond that could
keep a removed annotation reachable. A tree the walk can't finish is now
removed whole, with /MarkInfo: fail-closed, at the cost of the tags. The
limit is a parameter of drop_object_refs_within so the test can reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 02:02:23 +08:00
mtlcl410andClaude Opus 5.5 4d6812503d fix(redact): flattened widgets and far pop-ups leave the structure tree too (#2447)
Review follow-up on #2532:
- the structure-tree references dropped are those of every annotation no
  page lists any more after Sanitize, so widgets flattened with "form
  fields" go too, with their field value;
- the annotations to remove are collected on every page first, so a
  pop-up listed on another page than its parent goes with it;
- an element left with nothing to tag drops /Alt, /ActualText, /T and /E,
  which can repeat the URL or comment;
- the test's byte search saves without object streams, so it can see the
  strings it looks for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 02:02:23 +08:00
mtlcl410andClaude Opus 5.5 b7b6e6fe09 fix(redact): removed links and comments leave the structure tree too (#2447)
Remove Hidden Information and Sanitize took a link or comment off its
page's /Annots, but a tagged PDF's structure tree still referenced it
through an /OBJR, so the full rewrite kept the annotation, its URL or
text included, while link_list reported none. The annotations removed
from every page are now collected, and tags::drop_object_refs drops the
structure elements' object references to them (direct or indirect,
array or single kid), with the same seen-set bound as tags::clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 02:02:23 +08:00
mtlcl410andClaude Opus 5.5 4a35a1cb23 M9: rewriting a certified form's XFA data is a form fill (#2759)
The IRCC IMM forms are certified (DocMDP P=2) and then saved once more
with Reader Extensions: a later revision adds the UR3 usage-rights
signature and rewrites the XFA `datasets` packet. The diff classified any
rewritten stream that wasn't page content or the catalog's XMP as "other
changes", so these untouched forms validated as invalid.

A stream the signed revision's /AcroForm /XFA array names `datasets` or
`form` (the form's data and state) is now a form fill when rewritten,
permitted at P=2 and P=3 and refused at P=1 as before. The roles come from
the signed revision, so a later revision that names the template
"datasets" doesn't make rewriting it a form fill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-17 01:21:28 +08:00
mtlcl410andClaude Opus 5.5 aea1d3d127 M1: put valid_xref_range's doc comment back, and no trailing spaces in the tests (#2676)
Review nits on #2717: the new doc comment had been inserted inside
valid_xref_range's; the tests' xref tables are now one-line literals with
\n escapes, so git diff --check is clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 22:30:55 +08:00
mtlcl410andClaude Opus 5.5 c6acaefd35 M1: read an xref table numbered from 1 that begins with object 0 (#2676)
Some producers head a classic xref table "1 n" and then write object 0's
free entry first, so every row is one number off and object 1 reads as
free. The renderer coped, but cos did not: printing a scanned invoice
built from cos dropped object 1 (page 1's image) and printed the page
blank. Such a subsection is now read from 0 when its second row locates
an object that is numbered 1, in both xref readers, and the repair is
logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 22:30:55 +08:00
mtlcl410andClaude Opus 5.5 942be18504 parity: form.fill-sign-date has 14 presets now
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 19:20:48 +08:00
mtlcl410andClaude Opus 5.5 b97e59190f M5: year-first slash dates (yyyy/mm/dd, yyyy/m/d) for Fill & Sign and headers
Taiwan, Japan and China write dates year first with slashes (Windows'
zh-TW short date is yyyy/M/d), and neither the Fill & Sign presets nor
the header and footer date tokens offered it. Both now have yyyy/mm/dd
and yyyy/m/d. The header and footer list gains them at its end, since
its selection is an index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 19:20:48 +08:00
mtlcl410andClaude Opus 5.5 30c174be9e M3: closing PdfCraft while minimized no longer saves a 0 x 0 window
eframe stores the window's size and place just before App::save. A
minimized window has none: Windows reports 0 x 0 at (-32000, -32000),
and the next start opened at egui's 64-point floor in the screen's
corner (seen on a 6K display at 225%: a 144 x 206 px window). While the
window is minimized the app now puts back the last settings it was
shown with (or none), and a window that opens at that floor from
settings saved before this fix is given a usable size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 19:20:47 +08:00
mtlcl410andClaude Opus 5.5 96478f325d fix(ui): test that the Explorer argument reaches the process as written (#2579)
Review of #2625: the test checked the argument reveal_command builds but
not how it is passed, so it still passed with raw_arg replaced by args.
The process is now built by reveal_process, and a Windows test runs it
on `cmd /c echo`, which prints its command line back: with Rust's own
quoting the argument comes back wrapped in quotes with the inner ones
escaped, and the test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 17:40:55 +08:00
mtlcl410andClaude Opus 5.5 f9db1e31fd M10: running text is not a table row in Word, HTML and PowerPoint export (#2806)
Table detection took a sentence whose link was set in another colour
and font, split into blocks that run on into each other, for two table
columns, and took a paragraph's lines, whose box overlaps the line
before, for a cell beside it. The table it built put the paragraph's
first line in one cell, a fragment of the link in another, and dropped
the paragraph's later lines. A block that starts where another on the
same line ends now adds no column of its own, and a row whose cells
overlap is not a table row; a superscript beside a number stays in its
table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 16:47:49 +08:00