Commit Graph
4844 Commits
Author SHA1 Message Date
FredApps a933d2aa4c Merge branch 'main' into feat/fill-sign-rail-default 2026-11-03 14:28:16 +01:00
DewandClaude Opus 5.5 5031dd0827 M7: review follow-up, map the ICU4X licence for license-check
The license-check gate (xtask ci) failed on two things this PR caused:

- NOTICE changed, so its recorded hash in LICENSES.toml no longer
  matched. The ICU4X notice text was re-audited against the
  icu_segmenter_data 2.3.0 crate's LICENSE: the copyright line, the ICU
  portions line, and the permission, warranty and name paragraphs match it
  word for word. NOTICE now also points to the full licence; the hash is
  updated.
- The bundled icu_segmenter_data dictionary had no licence-document
  mapping. licenses/ICU4X-LICENSE.txt is the crate's LICENSE, unchanged
  (Unicode-3.0, origin the crates.io archive), mapped as a [[document]] and
  from a [[bundled]] entry for the dictionary file.

`cargo xtask license-check` now passes.

Requested in FredApps' review of #3492.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
2026-11-03 20:24:05 +07:00
DewandClaude Opus 5.5 f8c29bf262 M7: review follow-up, attribute the ICU4X word dictionaries
icu_segmenter_data compiles the Thai, Lao, Khmer and Burmese word
dictionaries (data/segmenter_dictionary_extended_v1.rs.data, 1,773,172
bytes by the file's own count) into the binary, so they get a [[bundled]]
entry per AGENTS.md §1.3, and NOTICE carries the Unicode licence's
copyright and permission notice, which it requires in the documentation.
The line segmenter loads only these dictionaries; the Chinese and Japanese
one is not linked.

The data is under Unicode-3.0, which deny.toml already allows for code but
the asset allowlist (AGENTS.md §1.2, xtask ALLOWED_LICENCES) did not list,
so this adds it there. That is a policy change for the maintainers to
accept; without it `cargo xtask assets` refuses the entry.

Requested in FredApps' review of #3492.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
2026-11-03 20:24:05 +07:00
DewandClaude Opus 5.5 ff556b537e M7: review follow-up, a Thai first word counts the opening spaces
A first word that fit a row on its own but not after the spaces opening
its paragraph was placed whole and ran past the row's end. Whether the
word is too wide now counts the width those spaces already use, so it
breaks between clusters on the first row instead.

Reported by the CodeRabbit review on #3492. The new case fails without the
fix: "      ประปา" stayed one row, wider than its 52 pt box. shaped_width is
pub(crate) so the test can measure rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
2026-11-03 20:24:05 +07:00
DewandClaude Opus 5.5 fb6830c4dd M7: review follow-up, opening spaces are not a Thai row of their own
A Thai Add text paragraph that opens with spaces held a row of only those
spaces. When the first word did not fit after them, that row was ended
(trimmed to an empty row) and the opening spaces were lost. A row of only
spaces now counts as empty when deciding to end it, so the first word stays
on the first row after the spaces, or breaks between clusters there.

Reported by the CodeRabbit review on #3492; the new case fails without the
fix (at 16 pt the rows were "", "ป", "ระ", ...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
2026-11-03 20:24:05 +07:00
DewandClaude Opus 5.5 d2723b64fa M7: break Thai rows between dictionary words
Thai has no spaces between words, so rewrapping treated a whole Thai run as
one word and Add text broke rows at any shaped cluster, splitting words such
as ประปา or สะอาด across rows.

Find the words with ICU4X's dictionary line segmenter (icu_segmenter 2.3,
compiled dictionary data, no LSTM model; Unicode-3.0). linebreak::breaks
gives every place a row may end inside a run without spaces: the existing
Chinese and Japanese rule plus the dictionary's breaks next to Thai. A row
never starts with a following vowel, a tone or other mark, Mai Yamok or
Paiyannoi, nor ends with a leading vowel, whatever the dictionary says, and
the CJK pair rule now honours the same Thai marks.

- text.rs wrap: unchanged; linebreak::pieces now cuts Thai too.
- text_regions.rs: breaks are worked out once per run of non-space
  characters instead of pair by pair, since Thai needs the whole run.
- added.rs thai_lines: clusters are grouped into words and whole words
  wrap; only a word wider than the row breaks between clusters, as before.
  The spaces where a row ends are dropped, as in the Latin wrapper.

Rewriting a paragraph into Thai still needs a source font with the Thai
glyphs; there is no Thai fallback face for it yet. The compiled dictionaries
(Thai, Lao, Khmer, Burmese) add about 1.8 MB to a stripped release binary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018cECVMvRnc9mHNFfVp7nXd
2026-11-03 20:24:05 +07:00
alehel d4c8b6639a Merge remote-tracking branch 'origin/main' into refactor/page-press-dispatch
# Conflicts:
#	crates/ui-egui/src/tools/edit_text_ui.rs
2026-11-03 14:10:49 +01:00
KenM76andClaude Opus 5.5 342524e130 Merge origin/main into feat/3d-embed
Resolves the ROADMAP.md progress-log conflict by keeping both lines, newest first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 08:09:48 -05:00
ea353f7a59 fix(ui): do not open right-click context menu on image in crop mode (#5835) (#5900)
* fix(ui): do not open right-click context menu on image in crop mode (#5835)

* M7: preserve active crop context routing (#5835)

Prioritize active crops over overlapping images and share measured action-pill geometry with painting. Cover overlapping images, handle margins, page edges, and all view rotations with regression tests.

* fix(ui): test selected cropped image by quad in right-click loop (#5835)

* fix(ui): trim crop right-click routing to the crop box and handles (#5835)

The Done/Cancel/Remove Crop pills are widgets and already take their own
right-clicks, and the canvas context-menu guard already closes the image
menu, so drop the pill hit-test, the crop_pills layout extraction and the
extra secondary-click block. Move the session note to the Progress log.

---------

Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 14:00:16 +01:00
Andrew 6028bdab7c M5.7: the Fill & Sign button remembers tools where they are selected
The quick bar copied the active tool into fill_tool on every draw, so a tool
restored on leaving full screen replaced the last selection, and a tool
selected while the bar was hidden was forgotten. The signature dialog and the
control channel now record the tool when they select it, and the bar only
reads: the active tool if there is one, otherwise the remembered one.
2026-11-03 05:51:32 -07:00
Andrew 2e699f489a M5.7: a test that the Fill & Sign button keeps Date across documents
Picks Date from the flyout, deselects it, and checks that the button still
shows Date and that a click picks it, in a newly opened document and again
after every document has been closed.
2026-11-03 05:51:32 -07:00
Andrew edee301ebf M5.7: the Fill & Sign button follows the active tool and names it
Tools restored without a command (leaving full screen, the temporary
tool, the control channel) left the remembered Fill & Sign tool stale,
so the button could show Checkmark while a click placed a signature. The
bar now takes the remembered tool from the active one, which replaces the
dialog's and control channel's own updates. While a Fill & Sign tool is
active the button opens the flyout, so its label names the tool instead
of describing placing or adding a signature.
2026-11-03 05:51:32 -07:00
Andrew e90c92124c M5.7: the quick bar's Fill & Sign button shows the tool it picks
The button showed a generic pen until clicked, then swapped to Add text's
icon. It now works like the Comment, Highlight and Draw groups: it shows
the last Fill & Sign tool used (Signature at first) and clicking it picks
that tool. The flyout marks the active saved signature or initials card,
and the cards get the pointing-hand cursor like the other rows.
2026-11-03 05:51:32 -07:00
Andrew 0a8aa374eb M5.7: pressing the Fill & Sign size slider without moving it adds no undo step
The slider senses drags only, so a press and release counts as a finished
drag even when the value did not move. The edit is now skipped when the
selected text already has that size, compared to the three decimals the size
is saved with.
2026-11-03 05:23:14 -07:00
Andrew 546a9d90a5 M5.7: the a-large-small attribution names where the icon is embedded
icon_data.rs moved to crates/ui-egui/src/widgets/. ATTRIBUTION.md is
regenerated with cargo xtask assets --write.
2026-11-03 05:23:14 -07:00
Andrew 84aa947c0f M5.7: the quick bar's comments no longer say Fill & Sign widens it
The Text and Date controls now fit the 32 pt column, so the bar stays at its
narrowest with every tool. The centring still measures the rendered width.
2026-11-03 05:23:14 -07:00
Andrew b304799812 M5.7: the Fill & Sign Font size button uses Lucide's a-large-small icon
The sliders icon is also Draw's Line thickness and doesn't say "text". Adds
one icon to the Lucide set, from lucide-static 1.49.0, with its attribution.
2026-11-03 05:23:14 -07:00
Andrew fbd2813d7a M5.7: the Fill & Sign text controls use a colour dot and a size flyout
The quick bar is too narrow for the "Font size:" label, number box and
"Colour" text, which wrapped mid-word. They now work like Draw's controls:
a divider, a colour dot that opens the swatches, and an icon that opens a
Font size flyout with a slider. Picking a colour also recolours the selected
Fill & Sign text, as Draw's colour does for its own marks.
2026-11-03 05:23:14 -07:00
Harsh VermaandFredApps 01ba3ac9d1 Feat/unify hotbar quick tools (Fixes #5890) (#5909)
* feat(ui): unify quick bar and hotbar and refresh settings dialog (#5890)

* style(ui): compact hotbar tools customization menu

* fix(ui): keep hotbar settings visible when docked to sides and address review findings

- Add placed bool to Hotbar to distinguish explicitly placed [0.0, 0.0] from default floating margin offset
- Check !a.locked before applying quick bar color edits to annotations
- In non-full-side docked vertical layout, scroll tools within available height so settings button remains visible in compact windows
- Remove max_width(cell) on vertical full-side scroll area to avoid clipping wider controls
- Restore outer vertical scrolling with max_height on Hotbar tools picker window
- Add test verifying docked hotbar in compact windows keeps settings button visible, and assert controls do not overlap

* feat(ui): close hotbar settings menu when clicking outside or pressing Escape

- Detect pointer clicks outside the settings menu window bounds and dismiss the menu automatically
- Keep settings button click behavior to allow opening and keeping the menu open
- Support Escape key to dismiss the settings menu
- Add integration test verifying clicking outside on canvas or on another tool dismisses the menu

* style(ui): restore hotbar settings dialog design to match concept mockup

* fix(ui): cap hotbar tools picker width to viewport on narrow screens

* fix(ui): anchor hotbar tools picker to rendered bar edges to avoid overlap

* fix(ui): keep upgraded hotbars in the margin and label the settings controls (#5890)

Preferences written before this change have no `placed` key, so every
existing user's untouched floating, horizontal, top-centre hotbar would
have jumped out of the margin. Such a bar now takes the new default; any
spot the user chose, including the top-left corner, is kept.

The side picker reuses the Preferences segmented pill control, so its
options are themed, reachable as radio buttons by label and visible to the
translation scan. The layout switch now carries its label and enabled state.

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-03 13:06:31 +01:00
Grzegorz MartensandClaude Opus 5.5 a2b2bc5e6d M2.6: test that a JPEG refused at its lookup table is left out
CodeRabbit's follow-up on #5825: the indexed JPEG test ran only with an
unlimited allowance, so nothing checked that a refused lookup-table read
leaves a JPEG out (`a_lookup_table_is_read_within_the_exports_allowance`
checks a Flate image). The test now measures what the soft mask costs
alone (no colour space, so nothing is read after it) and allows a step
less than the mask and the table: the export is refused with the
reason, and only the table is refused, nothing after it. A step short
of the mask, the mask alone is refused, so the mask is read first and
the case above is refused at the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 06:57:06 +01:00
KenM76andClaude Opus 5.5 3d106d7e1e Merge origin/main into feat/3d-embed
Resolves the ROADMAP.md progress-log conflict by keeping both lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 00:35:08 -05:00
Fatih KunakandClaude Opus 5.5 14ae839a28 i18n(tr): translate the 288 strings the gaps report lists (#5795)
* i18n(tr): translate the 288 strings the gaps report lists

The translation gaps report listed 288 English strings with no Turkish
translation, among them the Window menu ("Pencere"), the side-panel and
hotbar labels, the signature and timestamp validation messages, the
accessibility checker findings, the plurals and the calendar weekday heads.
The two English stand-ins at the top of the catalog are translated too.
`cargo xtask i18n-report` now lists 0 missing for tr.

Terms follow the existing catalog (annotation = açıklama, digital ID =
dijital kimlik, bookmark = yer işareti, redact = karart). Placeholders and
trailing ellipses match the English source. AI-assisted; not separately
reviewed line by line by a native speaker.

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

* i18n(tr): note the native review of the judgement calls

A native Turkish speaker reviewed the translations the PR flagged as
judgement calls ("of" as a slash, hotbar, navigation rail, the certificate
key-usage terms, the weekday heads) and kept them. The rest stay
AI-assisted, so full proofreading is still pending.

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

* docs(i18n): state the Turkish coverage by the gaps report, not the dated union

The 2,798-key union in localization-parity.md was measured on 2026-10-25 and
predates keys added since, so "every current key" against it did not add up
(CodeRabbit). The Turkish row now says what was measured: 2,867 entries and
0 missing in `cargo xtask i18n-report` on 2026-11-03.

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

* Revert the vendored Cargo.lock changes c700ce42 picked up by mistake

They came from running the vendored crates' tests locally and have nothing to
do with this PR.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:33:33 -05:00
Rahulandrahul05ranjan d9b735973b i18n(fr): translate the 16 strings the report listed as missing (#5736)
The French catalog lacked 16 interface strings: the crop-image and
mouse-wheel-speed labels, the signature-validation findings (RSA key
size, CMS content-type and message-digest rules, timestamp authority
certificate), the certificate recognition sentence, the changed-on-disk
reload warning, the password rule, the table-layout validations, and
the Appearance theme name.

Adds them in the catalog's TSV format. After this the report lists
nothing missing for fr.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
2026-11-03 00:33:26 -05:00
5cb65ff0ee feat(ui-egui): Preferences can invert the middle-button drag (#5790) (#5817)
Add a saved preference, invert_middle_drag (off by default), that
reverses the direction of the middle-button pan and of the latched
auto-scroll. It changes only the direction, not which of the two a
platform uses, so it composes with the Windows auto-scroll work.

Reachable as Preferences > Invert middle-button drag and headlessly
through ui.set invert_middle_drag. Parity "Automatic scrolling" notes
updated; status unchanged.

Tests: a unit test of both paths with the switch on and off (dead zone
kept), a settings round-trip test, and an end-to-end control-channel
drag. Each fails with its fix reverted.


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

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:33:18 -05:00
Hasan N. bf0f3c9f77 M10: macOS print jobs name their title and owner (#5743) (#5757)
* M10: macOS print jobs name their title and owner (#5743)

lp sends no com.apple.print.JobInfo.* attributes, and Kyocera's macOS
filter crashes without PMJobName and PMJobOwner, so the job stops with
cups-filter-crashed and nothing prints. Both go to lp as job options, in
the form lp_args passes on: printable ASCII, up to 64 bytes.

* M10: keep macOS job metadata as the name CUPS should send (#5743)

Review of #5757, checked against this Mac's libcups in memory:

- `'` and `\` were consumed as quoting and `{` opened a collection, and a
  bare true/false became a boolean, so a filter reading PMJobName got
  nothing or the wrong type. They now become `_`, or the fallback.
- CUPS keeps the last of a repeated option, ignoring case, so the
  defaults replaced a value a doc_print caller had set. They are now
  added only for a key the job leaves out.
- Nothing proved the job info reached the spooler. A test now runs both
  submit paths against a stand-in `lp` on PATH.
2026-11-03 00:33:11 -05:00
PiohrunandClaude Opus 5.5 8582c84da8 M11: Optimize runs when a category it discards holds nothing (#5742)
* M11: Optimize runs when a category it discards holds nothing

PDF Optimizer's Discard Objects ticks pass their categories to
remove_hidden, which refuses a run that removes nothing. Ticking "Discard
bookmarks" on a file without bookmarks, or doc_optimize with such a
discard, ended the whole optimization with "there are no redaction marks
to apply". An empty category is now nothing discarded.

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

* M8: doc_remove_hidden says when the listed categories hold nothing

Its error came from remove_hidden's refusal: "there are no redaction marks
to apply". It now checks the scan first, as redact_apply checks for marks,
and says there is nothing to remove. The scan counts what removal takes
(an AcroForm with no fields as 1), so the check refuses only what the
removal would. The tool description says it fails then.

The tool test also runs doc_optimize discarding categories that hold
nothing, end to end.

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

* M11: Optimize passes remove_hidden only the ticked categories that hold something

Catching remove_hidden's refusal kept a copy it may have changed: with
bookmarks ticked it drops an empty outline root before counting nothing,
and its own documentation says to drop such a copy. The optimizer now
scans first and passes only the ticked categories the scan finds something
in, and with none doesn't call it, so an empty outline root stays, as
Remove Hidden Information leaves it.

Corrects 278a588bf's message: the ticks are under Discard Objects and
Discard User Data, and hidden text is one of them.

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

* M8: doc_remove_hidden reads one scan, and says why it can't edit

A document that can't be edited has no scan, so the check read every
category as empty and said the listed ones hold nothing; the reason it
can't be edited comes through again. The check and the count of what was
removed now share the one scan the tool took already.

Corrects ada2a1ccb's message: the removal also takes what its scan counts
as 0 (an empty outline root); what the check matches is that the removal
refuses when its scan counts every listed category as 0.

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

* M11: the optimizer test discards a category that holds something

Mutants of the category filter survived: no test ticked a category that
holds something, or an empty one beside an unticked one that does. A page
with a comment and no bookmarks, discarding both, discards the comment
and only it.

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

* M11: Optimize passes every ticked category, and keeps the copy from before a refusal

c7c018ac6 passed remove_hidden only the ticked categories its scan finds
something in. But with form fields ticked, discarding hidden text also
takes the hidden text the flattened appearances bring onto the page,
which no scan before flattening counts, so with Flatten form fields and
Discard hidden text ticked, the hidden text was dropped from the list and
stayed in the file, where main removes it.

Every ticked category goes to remove_hidden again, as on main. When it
refuses (every category counts 0), it may have changed the copy (an
empty outline root goes with bookmarks, and then it refuses), so the copy
from before the call is kept and nothing is discarded. That also drops
the extra scan c7c018ac6 added.

Corrects c7c018ac6's message: remove_hidden counts first, then drops an
empty outline root, then refuses.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:33:04 -05:00
cb405f207e fix(optimize): make pictures under a stencil mask smaller (#5847)
* fix(optimize): make pictures under a stencil mask smaller

An image with any /Mask was skipped by the colour and grey settings. That is
right for a colour-key mask, which names sample values that resampling and JPEG
would change. A stencil mask is an image of its own, with its own resolution
(ISO 32000-2 8.9.6.3), so the picture can change under it and the mask stays as
it is. Some exporters put one on every picture, even where it hides nothing, so
whole drawing sets of pictures were left at full size.

A mask that can't be read as a stencil, such as a reference to nothing, still
leaves its picture alone.

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

* fix(optimize): only a mask that says /ImageMask true counts as a stencil

A stream /Mask without /ImageMask true let its picture be replaced, although a
mask that isn't a stencil is meant to leave its picture alone. It now does.

The stencil test also checks that the hole still shows what's behind it. The first
pixel all round is left out, because the coarser picture blends into it a little
(at most 26 of 255 in this test).

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

* test(optimize): draw a coloured page behind the stencil test's picture

On a white page, a hole painted white looks the same as an open hole, so the check
on the hole couldn't tell them apart.

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

---------

Co-authored-by: quincy-jc <184324679+quincy-jc@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:32:58 -05:00
ljkx 023af5bdb2 M7: Arrange keeps a top-level inline image a unit of its own (#3146) (#5823)
* M7: Arrange keeps a top-level inline image a unit of its own (#3146)

The parser emits a whole inline image (BI ... ID ... EI) as one BI op, but
content_units scanned on from it for an EI op that never comes, so the unit ran
to the end of the stream. SendBackward on an image after a top-level inline
image then acted like SendToBack, and BringForward/BringToFront on an image in
the swallowed region could do nothing. A BI op is now a unit of its own, like Do
and sh.

* M7: pin Arrange around inline images at every position (#3146)

Check the exact content units and the paint order after each move, not
string positions: several top-level inline images (at the start, side by
side and at the end) are one unit each, BringForward and SendBackward step
past one inline image at a time, BringToFront on the front image is refused
without touching the page, and SendToBack and BringToFront go past inline
images that start or end the stream. A guard checks that an inline image
inside q/Q or marked content stays in that unit. Document content_units.

* M7: Arrange tests check each image's transform through q/Q (#3146)
2026-11-03 00:32:50 -05:00
NetterSilverandClaude Haiku 5.5 872a775010 Expand header/footer tokens preceded by extra '<' characters (#5766)
Found while testing crates/edit/src/marks.rs:247-262 (expand)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-03 00:32:44 -05:00
SSwansonandseanGSISG 97a9110533 M12: FormCalc prints numbers to at most 15 significant digits (#5819)
`num_text` printed every non-integer with twelve fixed decimals. A value with
six or more digits before the point then shows the binary noise of the double
that holds it: a FormCalc calculate `qty * price` with price 279893.88 gave
`279893.880000000005`, and a Sum() of five such amounts gave
`1466917.350000000093`. Real invoice forms (USACE ENG Form 93, a dynamic XFA
payment estimate) show these in their amount and total fields.

Print at most 15 significant digits (the most a double holds for every value)
and at most 12 decimals, as before for small values, so sums that should be
zero still print 0. Only a fraction loses trailing zeros: past 15 digits before
the point no decimals are left and the integer keeps its zeros.

Co-authored-by: seanGSISG <seanGSISG@users.noreply.github.com>
2026-11-03 00:32:38 -05:00
jonathanwxh-cell 49d5997e18 fix(a11y): splice named and multi-stream marked content in mark_tag_artifact (#5263)
Share the stream-splicing path between mark_decorative and mark_tag_artifact so mark_tag_artifact resolves named property lists in /Resources /Properties, resolves indirect /MCR references in /K, leaves untouched and shared content streams intact, and validates the tag parent before mutating /Contents or /ParentTree.
2026-11-02 23:38:55 -05:00
Dylan M. Taylor 97260514e6 fix(create): write a new document's page tree once (#5865)
Create PDF added each page to the page tree through Document::update_dict, which copies the tree's dictionary and its whole /Kids array, so a text of many form feeds took quadratic time: 40,000 pages took 108 s in a debug build. Pages are now collected and /Kids and /Count are written once, for blank, image and text documents.
2026-11-02 23:36:51 -05:00
Dylan M. Taylor 359ed56ffa fix(redact): structure titles go with the descriptions of removed content (#5866)
Redaction and hidden-content removal cleared /Alt, /ActualText and /E from
the structure element owning removed marked content, but kept its /T title,
which producers often fill with the element's text. Ancestor cleanup and
in-place description detection shared a constant that also omitted /T, while
annotation removal had its own list that included it.

One DESCRIPTIONS policy (Alt, ActualText, T, E) now serves the owning
element, its ancestors, in-place detection and annotation removal.
2026-11-02 23:34:18 -05:00
Dylan M. Taylor 69ddcd89bf fix(redact): rewritten form XObjects drop /PCAdded (#5868)
Page content streams rewritten by redaction or hidden-content removal
already dropped /PCAdded, the source text of an added item, but a form
XObject whose content was rewritten kept its original dictionary,
/PCAdded included, so the removed text stayed readable in the saved file.
2026-11-02 23:31:39 -05:00
Dylan M. Taylor 661ded72bb Merge pull request #5748 from Combiler01/fix/single-page-zoom-scroll-lock-5713
fix(ui): prevent wheel lockup at edge in non-continuous view (#5713)
2026-11-02 22:59:04 -05:00
KenM76andClaude Opus 5.5 eaf06f9cb9 fix(cos): a save after undo keeps objects packed in a dropped object stream
When the working snapshot is restored from an earlier revision (undo after a save), the
next save appends to the last saved file through overlay_changes_from. An object stream of
that file that the restored graph frees or reuses the number of took its unchanged members
with it: they stayed listed in the dropped stream, and the file reread with no pages. Such
members are now written again on their own.

Adding a PRC model now edits the catalog (the ADBE extension), which made the model_3d
automation test (save, undo, save) reach this case on CI.

The same change is part of #3305 (skylartaylor), which also covers security changes.

Test: cos a_restored_revision_keeps_what_the_saved_file_held_in_object_streams, failing
with the fix reverted (the reread catalog's /Pages resolves to nothing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 20:28:06 -05:00
Brandon ThomasandClaude Opus 5.5 3936f93a14 Release 0.18.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.18.0
2026-11-02 19:26:43 -05:00
Grzegorz MartensandClaude Opus 5.5 0c9cd267f5 M2.6: a JPEG's colour space is read once for an export
CodeRabbit on #5825: a JPEG that isn't inverted read its colour space to
know whether it is CMYK, and then, with a soft mask, a stencil mask or a colour key keeping it
from being written as it is, read it again for its samples. An indexed one
(not valid, written as it is in the end) paid for its lookup table twice,
and could be refused with an allowance that covers the image and one reading.
The first reading is kept and used.

a_jpegs_lookup_table_is_paid_for_once: an indexed JPEG with a soft mask pays
one reading of its table, and is written as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:51:14 +01:00
KenM76andClaude Opus 5.5 4c087336aa M12: 3D models: declare PRC's Adobe extension before 2.0; refuse a poster with no area
Before PDF 2.0, PRC in a 3D stream is an Adobe extension to PDF 1.7, so a
PRC model added to a file below 2.0 now declares /Extensions /ADBE at
BaseVersion 1.7, ExtensionLevel 1, keeping other extensions and a later
ADBE level, and leaving an /ADBE that is not a dictionary (an array of
them) alone (ISO 32000-2 §7.12). U3D and 2.0 files are unchanged.

A PDF poster whose page box has no area was drawn as nothing behind a
size forced to 1; it is now refused with EditError::Source.

Both found by CodeRabbit. Tests a_prc_model_declares_the_adobe_extension_before_pdf_2
and a_3d_poster_page_without_area_is_refused fail with their fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 18:48:12 -05:00
Grzegorz MartensandClaude Opus 5.5 05300ea5bc M2.6: say what was measured, not why, and test the other block zlib-rs refuses
The Claims review of the branch: the docs said miniz_oxide "builds a block's
tables for every block", which a timing doesn't show. They now say what was
timed: about 1.7 us for every fixed-Huffman block, empty or not. "The worst
comes to about 7 ns a step" was per byte read; a stage's start, about 0.25 us
under zlib-rs (1.6 us for eight over an empty stream, release), isn't priced,
since every way to repeat one takes bytes of the file. The letterhead test's
doc promised more than it checks.

The same review found a second shape zlib-rs refuses where miniz_oxide read
on: a dynamic block whose code has no end-of-block symbol. flate.rs's quirks
and the README row say so, and a_dynamic_code_without_an_end_is_damage checks
that nothing of it is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 f8788dff11 M2.6: say what zlib-rs reads differently, and test which backend inflates
The Consumers review of the commit before this one: zlib-rs refuses a back-reference to before
the start of the output, which miniz_oxide filled with zeros and went on
from. Read strictly such a stream is now refused, and read tolerantly what
came before it is kept, as zlib has it. flate.rs's quirks and the filters
README say so, and a_copy_from_before_the_start_is_damage checks it, which
also fails under miniz_oxide, so it shows which backend runs: the read price
is set for zlib-rs.

The same review: other dependencies still build flate2's default miniz_oxide
backend, and zlib-rs runs because flate2 prefers it when both are built, not
because it is the only one. The Cargo.toml comment says so. The miniz_oxide
measured was 0.9.1, flate2's (0.8.9 is png's); FLATE_READ_WORK's doc says so.
The filters README named miniz_oxide as the backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 6771ddffe8 M2.6: inflate with zlib-rs everywhere, so reading Flate costs 2 steps a byte
The Resources review of the commits before this one found that pricing what Flate reads at 128
steps a byte refused real redactions. Redaction decodes a picture once for
each page that draws it, so 240 pages sharing a 1 MB scan-like Flate picture,
a mark on each, went from Ok (240 cleared, 20.3 s) to ImagesPastAllowance, its
allowance paying for 188 decodes: each paid about 27 times what it had.

The cost being priced is miniz_oxide's: about 1.7 us for every fixed-Huffman
block, empty or not. flate2's zlib-rs backend took about 11 to 13 ns a fixed block,
and the Windows build already used it (craft-update turns
on zip's deflate-flate2-zlib-rs, and with both backends on, flate2 inflates
with zlib-rs, measured). Every platform now builds flate2 with zlib-rs, which
it prefers to the miniz_oxide other dependencies still build.
In a release build (zlib-rs 0.6.8), per byte read: empty fixed blocks 9 ns,
fixed blocks of one literal 6, empty dynamic 21, empty stored 1.3, against
1,383, 768, 106 and 2 before. Each backend inflating what its own
encoder made, 16 MiB of text took 2.6 ms against 6.3 to 13.7, and a
scan-like picture 38 ms against 53.

FLATE_READ_WORK is now 2: per byte read, the worst shape comes to about 7 ns a
step, and a picture an encoder made pays twice its stored length more than unpriced
(an_image_an_encoder_made_pays_little_for_its_reading: at most half again).

The same review: export and compare's Office export read an indexed image's
lookup table outside the allowance, once per image, even with nothing left
(1.4 s each for 1 MiB of empty blocks under miniz_oxide). It is read through
ImageBudget::decode_filters now, and refused past it
(a_lookup_table_is_read_within_the_exports_allowance).

filters, cos, optimize, redact, create, edit, export and engine pass with the
new backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 f16c7db8f4 M2.6: a preset dictionary is refused whatever follows its header
Mutation testing of the commit before this one left the preset-dictionary check in flate.rs
alive: the test's bytes after the header fail to inflate either way. A header
with FDICT set followed by data that inflates as raw deflate is refused too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 10172d7a94 M2.6: a Flate stage pays for what it reads
The image allowance charged a Flate stage the bytes it made, and its input
only as what the stage before it made. Inflating does work for every block it
reads, whether or not the block makes anything: a stream of empty
fixed-Huffman blocks, ten bits each, took 1,383 ns a byte to make nothing
(miniz_oxide 0.9.1, release), one of a literal each 768 ns a byte, empty
dynamic blocks 106, against the 3 ns a step budget.rs states. A 250-byte
stream of four Flates over empty blocks took 47 s and was charged about
34 million steps, so a review of #5698 worked out about 7 hours before a
240 KB file of them was refused, through clearing, Optimize, export, compare
and redaction's images and dictionaries alike.

Where decoding is paid for (decode_tolerant_within, decode_within), a Flate
stage now pays FLATE_READ_WORK, 128 steps, for each byte it reads, before it
runs: a stage the allowance can't pay for doesn't start. Per byte read, the
worst of the shapes measured comes to about 11 ns a step at that price, inside
what a JBIG2 step takes. The raw retry of a zlib header that only looked valid reads the
bytes again, so it pays again, makes no more than is left once it has, and is
refused when it can't be paid for. Byte counts for a byte allowance
(decode_counted, decode_tolerant_counted, as a page's content uses) are
unchanged.

With a tiny file's allowance (2^34 steps), the 250-byte stream is decoded
three times in 141 s, and the fourth stops before its last stage reads the
empty blocks (8.2 ns a step, release). Data an encoder makes took 0.4 to 3.2 ns a byte it
made, reading included, and each byte of a file adds 64 times the price to an
operation's allowance.

Tests: a_flate_stage_pays_for_what_it_reads (empty blocks: 129 steps a byte,
refused a step short with nothing inflated) and a_second_reading_is_paid_for;
the exact costs in filters, cos and optimize tests add what Flate reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
KenM76andClaude Opus 5.5 649e97930f fix(cos): a rebuilt index keeps the read failures its scan found
Rebuilding the cross-reference index from object headers reset the
unreadable-object list a second time, after adding the object-stream
members, so an object the header scan failed to read dropped off
unreadable_objects() unless something read it again. Those failures
still stand against the full index: the added members leave every
scanned object's entry as it was, and a /Length found only then changes
how much of a stream is read, never whether it reads. The list is now
reset once, before the scan; the object cache is still cleared after
it.

Test: a_rebuilt_file_lists_the_objects_its_scan_could_not_read fails
without the fix (the list is empty) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 18:03:00 -05:00
KenM76andClaude Opus 5.5 96d3d1e073 fix(cos): a reused /Length keeps the edits it was read through
The indirect /Length memo kept only the value. A length first resolved
through one clone's edited object stream and reused later in the same
load did not count as an edit read, so an object stream framed by it
could still be parsed with the edited length and put in the
object-stream map and cache the clones share. The memo now keeps
whether resolving the length read edits, and a reuse counts it again.

Test: a_reused_edited_length_leaves_other_clones_members_alone fails
without the fix (the other clone reads None) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 17:56:44 -05:00
KenM76andClaude Opus 5.5 9da253ebee fix(annot): a 3D poster fitted to its rectangle with rounding is taken
A poster box fitted to the annotation rectangle in floating point can
end a few ulps outside it (a 7x3 picture centred in 0 100 3.6 110.8
spans -2.2e-16 to 3.6000000000000005), and the exact containment check
refused it. The check now allows a slack of 1e-9 of the rectangle's
largest coordinate, clamps the box to the rectangle, and only then
requires it to have width and height, so a box wholly within the slack
outside the rectangle is still refused.

Tests: a_3d_poster_fitted_to_its_rectangle_with_rounding_is_taken
fails without the slack (Invalid); the new case in
a_3d_poster_outside_its_rectangle_or_without_a_size_is_refused fails
when the size check runs before the clamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 17:24:42 -05:00
KenM76andClaude Opus 5.5 8cc4d37847 fix(cos): an object stream framed by one clone's edited /Length is not shared
An unedited object stream whose indirect /Length is edited in one clone
was parsed with that clone's length and put in the object-stream map
the clones share, so the other clones read its members within the
edited length. A container load that read the clone's edits is now
kept to that clone, like an edited stream.

Test: an_edited_object_stream_length_leaves_other_clones_members_alone
fails without the fix (the other clone reads None) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 17:18:30 -05:00
KenM76andClaude Opus 5.5 85df8ee668 feat(cos): list the objects that could not be read in doc_info
An object whose load fails reads as null (ISO 32000-2 §7.3.10) and the
file opens, but nothing said so: the repair log records only
document-level repairs, so a file with an unparseable /Info showed no
repair and no reason for its missing title.

pdfcraft-cos Document now records each object whose load fails, with
its generation and the reason, in a set shared by clones and scans like
the object cache: at most 100, keyed by number, first reason kept, cut
to 160 characters, built only for an object not yet listed. It is reset
where the caches are (authenticate, reconstruct); edited objects drop
off a document's list. Document::unreadable_objects() gives neutral
lines ("object 4 0 could not be read: ..."), the engine passes them on,
and automation doc_info adds an unreadable_objects array, kept apart
from repairs so the "has been repaired" notice and the clean-file
checks keep their meaning.

Tests: a cos unit test for listing, dedup, clones, scans and edits; a
cos test for the 100 cap and the no-readable-copy reason; an end-to-end
doc_info test on a synthetic broken file. Each fails with the fix
reverted. The core.repair-log parity entry cites them and says what is
not listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 17:08:05 -05:00
KenM76andClaude Opus 5.5 84d4b8cab9 fix(cos): a load that read one clone's edits is not shared with the others
Clones share the object cache and the parsed object streams. A clone that
freed or replaced an object stream then read a member of it put the result
in those shared maps: a failure (which this branch now caches) or, as on
main, the replacement stream and the member parsed from it. Another clone
whose stream is intact then read that instead of the file.

A load that reads an unsaved edit (an edited object stream, or an edited
indirect /Length) is no longer cached, and an edited object stream is
parsed for its own document only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 16:21:45 -05:00