* 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>
* 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>
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>
* 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>
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
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
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
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
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
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
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
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
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
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
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
`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
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
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
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
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
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
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
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
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
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
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
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
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
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
* 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>
* 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>
* 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>
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
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
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
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
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
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
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
* 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>
* 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>
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>
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
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
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
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
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
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
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
* 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>
* 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>
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
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>
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>
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
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
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
* 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>
* 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>
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
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>
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>
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>
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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