Commit Graph
4186 Commits
Author SHA1 Message Date
KenM76 f837fa652e Merge remote-tracking branch 'origin/main' into feat/prc-render
# Conflicts:
#	Cargo.toml
2026-11-03 22:20:39 -05:00
Dylan M. Taylor 8bb0528bed Merge pull request #6078 from storytold/rel-0.18.1
Release: PdfCraft v0.18.1
v0.18.1
2026-11-03 22:15:19 -05:00
Dylan M. Taylor 98845f1786 Release: PdfCraft v0.18.1
Bump the workspace version for the release that moves the craft-i18n pin
to 575cfde, serving the latest interface translations.
2026-11-03 21:51:23 -05:00
Martin BallandClaude Opus 5.5 a4a109ebaa i18n: move the craft-i18n pin to 575cfde (#6068)
* i18n: move the craft-i18n pin to b1f1d39

Brings in the catalog rows moved from open PdfCraft PRs
(storytold/craft-i18n#137-#144), about 1,190 Catalan lines among them.

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

* i18n: move the craft-i18n pin to 575cfde (storytold/craft-i18n#147): translations write menu paths with > or →, not ▸

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 21:48:33 -05:00
KenM76andClaude Opus 5.5 ce393b276b M12: prc-render review fixes (missing tessellations, vertex ceiling, seams, perspective shade)
- assemble: a placement naming a tessellation its file structure does not
  have is now PrcError::Malformed instead of a silently missing part;
  wires, markups and compressed tessellations are still skipped.
- assemble: positions, normals and texture coordinates are charged against
  a new MAX_ASSEMBLED_VERTICES (2^24) with RenderError::TooManyVertices, so
  a one-triangle mesh with many vertices placed many times cannot outgrow
  the triangle ceiling's memory bound. A refused mesh spends nothing.
- raster: a field of view too small for a finite focal length is a Camera
  error, not a background-only image.
- raster: a pixel centre exactly on an edge belongs to one triangle only
  (the side the triangle lies on, +x first, then +y), so translucent
  quads no longer blend their diagonal twice.
- raster: under a perspective camera the shade is interpolated with the
  1/z depth key, so it follows the surface rather than the screen.
- vec3: normalize divides by the largest component first, so a cross
  product above ~1e154 (or a subnormal one) still normalizes.
- render docs and README state the near plane: 1/10,000 of the
  eye-to-target distance.

Each new test fails with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 21:41:05 -05:00
KenM76andClaude Opus 5.5 5cc01712c3 Merge feat/prc-reader: the reader's review fixes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 21:36:17 -05:00
KenM76andClaude Opus 5.5 b1a6128dbd M12: review fixes from the renderer PR's review of the reader
- Huffman leaf tables where one code begins another are malformed; the
  shorter leaf would end the longer code early and misalign the payload.
- A model-file end past the input is malformed rather than clipped.
- Each schema entry costs one element, so empty programs are not free.
- tessellations() carries the globals schema's remaining allowance into
  the tessellation reader: one stream, one budget.
- A face behaviour wider than 16 bits is malformed, not zeroed.
- Coordinate arrays that are not whole points are malformed; a partial
  last normal drops the stored normals, as an unusable normal slot does.
- Triangles a TESS_3D's faces emit are charged to the element budget as
  they are made, since faces may reuse one index range.
- TriangleMesh::transformed drops stored normals under a projective
  matrix (last row not [0, 0, 0, w]).
- Placement::tessellation's doc says placements() does not check it
  against the tessellation section.

Each fix has a test, seen failing with the fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 21:36:07 -05:00
KenM76andClaude Opus 5.5 ea6d0c6bfb M12: pdfcraft-prc-render, a headless renderer for PRC models
New L3 crate over pdfcraft-prc: assemble places every product-tree
tessellation into world space with its colours (pure `place` does the
transform), Camera fits a view to the model, and render fills
depth-buffered triangles with headlight or unlit shading into an RGBA
image. Opaque triangles draw first, translucent ones blend over them;
perspective views clip at a near plane.

Never panics: MAX_RENDER_PIXELS and MAX_ASSEMBLED_TRIANGLES cap the
work, every pixel and vertex index is checked, and damage returns
RenderError. Single-threaded.

Tests: 24 unit tests (two drive the product-tree placement path) plus an end-to-end one that builds a synthetic
PRC stream in-test, renders it, and checks the background, the nearer
triangle and the farther one by pixel; every truncation of that stream
parses and assembles without panicking. Reverting the depth test or the
placement transform fails the matching tests.

Adapted from pdfcer (MIT, same author) at 912437c3, trimmed to
the camera, raster and headlight parts and rewritten to the
pdfcraft-prc types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 20:50:36 -05:00
KenM76andClaude Opus 5.5 58fca10da9 M12: Cargo.lock follows the 0.18.0 workspace version
The merge of main kept pdfcraft-prc at 0.17.0 in the lock file, so --locked builds failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 19:57:37 -05:00
KenM76andClaude Opus 5.5 43b95919bb Merge origin/main (9ce6d89f) into feat/prc-reader
ATTRIBUTION.md regenerated with `cargo xtask assets --write` (255 on main + this branch's one asset).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 19:26:39 -05:00
9ce6d89f46 i18n: interface translations from craft-i18n (#6055)
* build(i18n): embed the craft-i18n catalogs from a pinned checkout

- `craft-i18n.rev` pins storytold/craft-i18n at e45ddf7 (the PdfCraft cutover sync of
  pdfcraft@ecdd1e6); `.gitattributes` keeps it LF.
- The `craft-i18n` crate (craft-libs 787a7a0, PdfCraft's existing pin) is a direct dependency
  of the UI crate; it was already in the tree through craft-ui.
- `crates/ui-egui/build.rs` writes `CRAFT_I18N` from `CRAFT_I18N_DIR`: one (locale, common.tsv,
  pdfcraft.tsv) entry per locale with a `pdfcraft.tsv`, paths written with `{:?}` and never
  canonicalized. Unset it is empty (English only); `CRAFT_I18N_REQUIRED=1` makes a missing or
  invalid checkout an error.
- Both flatpak manifests name the craft-i18n commit and `CRAFT_I18N_DIR`; a test keeps both
  commits equal to the trimmed pin.

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

* feat(i18n): look strings up through craft-i18n behind a thin Lang wrapper

`i18n.rs` keeps PdfCraft's API (tr, tr_ctx, tr_id, trn, key_name, fmt,
Lang, LANGUAGES, detection) and hands lookups to the craft-i18n crate:
`get` for key names, `trn_plain` for plurals (no isolates, no digit
grouping), PdfCraft's own `fmt` (no FSI isolates). `@id` rows and the
"Undo {action}" template are read only where PdfCraft had them, so menus
and history labels read exactly as before.

Detection is unchanged (LC_ALL/LC_MESSAGES/LANG, macOS defaults,
sys_locale); the crate's match_tag would drop nb for no/nn, so PdfCraft's
own tag mapping stays, with one parity test per path.

Completeness tests become reports; tests that need catalogs skip with a
note when the build has none. Font coverage tests read the embedded
catalogs (shared and PdfCraft rows).

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

* chore(i18n): remove the in-repo catalogs; craft-i18n holds them

The 34 `crates/ui-egui/src/i18n/*.tsv` catalogs were migrated into
craft-i18n at pdfcraft@ecdd1e6 and are now embedded from the pinned
checkout. ATTRIBUTION.toml drops their 34 entries for one craft-i18n
`[[build_input]]` (craft-i18n's ATTRIBUTION.md credits the translators);
AGENTS.md and docs/localization.md describe the new system and how to
add a language.

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

* test(i18n): skip catalog tests without craft-i18n; drop the catalog hash exemption

Tests that switch to a non-English language skip with a note when the
build embeds no catalogs (CRAFT_I18N_DIR unset), so the English-only
build tests green. `skip_without_catalogs` moves to the integration
test support module.

With no catalogs left in the repository, `cargo xtask assets` no longer
exempts `crates/ui-egui/src/i18n/*.tsv` from hashing: every `[[asset]]`
carries a sha256 again, and a `.tsv` added back would need one.

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

* build(i18n): watch the locales directory before reading it

A CRAFT_I18N_DIR that doesn't exist yet now reruns the build script once it appears (review).

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

* build(i18n): pin craft-i18n dfbe955, the merge of storytold/craft-i18n#129

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

* fix(i18n): skip an @id row that changes the label's placeholders or ellipsis

craft-i18n checks placeholders and the trailing ellipsis on every row whose
source is English text, but not on @id rows, whose source is the command id.
The in-repo catalog test used to fail on such a row; since the cutover it only
prints it, while tr_id and menu_label still showed it. Both now use PdfCraft's
own @id row only when it keeps the label's shape (same_shape, the rule
craft-i18n applies elsewhere), and otherwise show the label's translation, as
bundled_catalogs_are_consistent says.

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

* fix(i18n): reject an @id row with an open brace; list unused @id rows in the gaps report

same_shape skipped a `{` with no `}`, so an @id row such as "Öffnen {name…"
for "Open…" was still shown. It now counts as a changed shape.

bundled_catalogs_are_consistent printed the @id rows PdfCraft doesn't use, but
CI captures test output, so nobody saw them. translation_gaps_report, which
`cargo xtask i18n-report` runs with --nocapture, now lists them
(unused_id_rows).

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

* fix(i18n): reject an @id row with a `}` that closes no placeholder

same_shape counted only the `}` that close a `{`, so "Öffnen name}…" for
"Open…" or "{name}} öffnen" for "Open {name}" still passed. A row whose `}`
count differs from its placeholders now counts as a changed shape.

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

* fix(i18n): no braces in the assert messages of the brace test

assert! reads its message as a format string, so "a `}` without its `{`"
did not compile.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-04 00:47:17 +01:00
Dylan M. Taylor 801fd3aa67 fix(engine): decode signature preview images within a small work allowance (#5981)
* fix(engine): decode signature preview images within a small work allowance

* style: rustfmt
2026-11-03 15:10:02 -05:00
Dylan M. Taylor 76260bb6a5 fix(xfa): scripts come only from the XDP's own packets and quote the date pattern (#5987)
The template (and the saved form state) was found anywhere under the XDP root, so a
template-shaped element inside imported datasets could supply the form and its event
scripts. A date field's pattern went into AFDate_FormatEx/KeystrokeEx string literals
unescaped.
2026-11-03 14:33:41 -05:00
Dylan M. Taylor ecdd1e658c fix(audit): read content one operator at a time and charge hidden text and findings (#5875)
The faux-redaction audit held every operator of a page's content and of each
form XObject in a vector before walking it, so a stream of millions of short
operators (compressed to a few kilobytes) was materialised whole within the
page's decode budget. Each Do and gs also copied the whole XObject or ExtGState
table to pick one entry, and each annotation listed in /Annots was deep-copied
before its subtype was read.

The walk now reads operators one at a time through pdfcraft_content::ops,
borrows resource tables and annotation dictionaries through the resolved Arc,
charges a hidden line's text bytes (padding included) against the comparison
bound once per cover, and charges each finding's record against the output
bound, so a shared annotation list on many pages or withheld text cannot grow
the finding list for free.
2026-11-03 10:30:12 -05:00
AdrianTM 0d3f57e120 M5: keep Fill & Sign editing on Enter and preserve line breaks (#5850) (#5954)
* M5: keep Fill & Sign editing on Enter and preserve line breaks (#5850)

* M5: preserve typewriter whitespace through readers and edits (#5850)

* M5: retain direct-subtype comment listing for Fill & Sign fix (#5850)
2026-11-03 10:24:45 -05:00
3709926f39 Continue Bates numbering across ordered PDFs (#5953)
* feat: add multi-file Bates numbering

* fix(automation): doc_bates_multi rejects a malformed start_number and >> in prefix or suffix, accepts a range ending at u64::MAX, and counts only staged outputs

Addresses CodeRabbit's quick-win notes. Parity is partial until a GUI dialog exists.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:14:23 -05:00
Hamza Naveed cdfdd6b782 Open: measure cached objects once and by what they hold, read plain reals without a copy, and keep no /PieceInfo in inspection (#5873) (#5958) 2026-11-03 10:14:17 -05:00
eb696a3fdf Add CSS-free HTML export (#5938)
* feat(export): add simple HTML mode

* test(automation): reject simple HTML mode for other formats

* test(export): check simple HTML without new parser dev-dependencies

The parser test pulled html5ever, markup5ever_rcdom and seven more crates
into Cargo.lock for one test; plain string checks cover the same markup.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:20:23 -05:00
Hasan N. 1849b38c5a M2: a soft hyphen a substitute font lacks draws its hyphen (#5888) (#5896)
Windows exporters map the hyphen of an Arial they don't embed to U+00AD in
/ToUnicode. The CFF standard substitute has no soft hyphen, so the CID was
taken as its glyph index and drew an unrelated glyph (an Ë, or ! in bold).
ISO 32000-2 Annex D.2 calls the soft hyphen typographically the same as
the hyphen, as patch (30) already reads WinAnsi 0xAD: draw the
substitute's hyphen. The text keeps U+00AD.
2026-11-03 09:20:16 -05:00
.CooCoo 15e45069cc Keep disabled scroll animations in Preferences (#5886) 2026-11-03 09:20:09 -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
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
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
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
KenM76andClaude Opus 5.5 ebd09e6649 M12: PRC reader: map the prc-rs licence for license-check (#1297)
The Double code table's attribution pointed its licence file at the
table's own source. `cargo xtask license-check` requires every asset's
licence file to be a LICENSES.toml document, so the gate failed.

Add prc-rs's LICENSE at the same commit as `licenses/prc-rs-LICENSE.txt`,
fetched unchanged, with its origin and hash in LICENSES.toml, and point
the attribution entry at it. The MIT notice stays in the table's header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 15:03:39 -05:00
KenM76andClaude Opus 5.5 cb317560fe M12: PRC reader: answer review (#1297)
- Schema programs draw on one step allowance per reading pass:
  MAX_SCHEMA_STEPS (2^22) moves into the BitReader's budget next to the
  decoded-element allowance, and the tree walk carries both from reader
  to reader. A per-call ceiling let a cheap entity repeated many times
  run ~2^22 steps each.
- The vertex-colour pre-check charges the minimum encoding (one full
  colour, then one reuse bit each), so a run of reused colours may end a
  section.
- placements() reports unreadable globals when an entity reference, not
  only an item, names a reference coordinate system.
- chunks_exact -> as_chunks for clippy 1.99 (chunks_exact_to_as_chunks).

Tests: programs_on_one_stream_share_the_step_allowance,
one_step_allowance_covers_the_whole_walk,
reused_vertex_colours_may_be_the_last_data,
unreadable_globals_are_an_error_when_a_reference_names_a_coordinate_system;
each fails with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 14:38:13 -05:00
KenM76andClaude Opus 5.5 5eb484c235 M12: PRC reader: a test for each input guard (#1297)
Disabling each input guard in turn left several with no failing test.
Each now has one that pins the guard's own refusal, not a later error:

- a zero Huffman code-length width, and a zero code length;
- a boolean array longer than the data, refused before any bit is read;
- a character array count past the data;
- no file structures, or more than MAX_FILE_STRUCTURES;
- a schema entry count past the data;
- normal angles wider than 16 bits;
- more texture sets than a face may carry (now by message);
- representation sets nested past the ceiling (and at it, accepted);
- a prototype chain past the ceiling;
- the element allowance, at each place it is charged: an entity's
  counted array, uncompressed character and short arrays, a schema
  program, and across one walk's readers (walk_within takes the
  allowance so a test can set it; an allowance that fits one file
  structure does not fit two).

Each fails with its guard disabled (the texture-set one by aborting on
the allocation the guard prevents).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:52:20 -05:00
KenM76andClaude Opus 5.5 a7cc7586d1 M12: PRC reader: cap decoded array elements per reading pass (#1297)
The inflation cap bounds bytes, but one bit can decode to a 4-8 byte
element, so a 512 MiB section could ask for about 16 GiB. Every array
count is now charged against MAX_DECODED_ELEMENTS (2^24), shared by
all readers of one tree walk; over the allowance is TooManyElements.
Huffman pre-sizing is capped at 1024.

decoded_elements_are_charged_against_one_allowance fails with the
charge in BitReader::take_elements removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 13:27:05 -05:00
KenM76andClaude Opus 5.5 c566ca6144 M12: PRC reader: tree markups and views read past (#1297)
CAD 3D files carrying PMI failed model_tree() and placements() with
"tree markups (PMI) is not supported": the tree section has no length
prefix, so MarkupData and views had to be parsed to be skipped.

Now read past, not modelled (new tree/markup.rs):
- MarkupData [WD 7.3.10.4]: markup linked items (204), leaders (503),
  markups (502), annotation items/sets/references (504-506); sets
  nest under the MAX_DEPTH ceiling.
- Views (501) [WD 7.7.3], with their annotation plane (SURF_Plane 86),
  optional scene parameters, and from 8137 their flags, linked items
  and filters.
- Scene lights (731-734) and clipping planes, so
  SceneDisplayParameters no longer refuses them.
ReferenceData and ReferenceOnPRCBase are shared readers now, and skip
their schema-added fields.

Field orders the working draft leaves open follow prc-rs's format
description (MIT); each such reader carries a `prc-rs:` marker.

Tests: one synthetic test per entity kind (each reads to a sentinel
and every truncation errors), nesting ceiling, wrong annotation type,
and a tree with markups and views on a part and an occurrence that
still places its mesh. Each shown failing with its reader reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:44 -05:00
KenM76andClaude Opus 5.5 c571b13469 M12: PRC reader: product tree and styles (#1297)
Walk the PRC product tree into placed, coloured tessellations.

- PrcFile::placements / placements_with(StyleAlpha, EntityOverrides):
  every drawable tessellation reached from the model file's roots, placed
  by its occurrence path's locations and then the representation item's
  local coordinate systems (a 4x4 Matrix) [WD 7.3.10.1, 7.6.3.2].
- Colours: styles and materials from the file structure globals, the
  father/son inheritance bits [WD 7.2.4.2], style transparency combined
  with material alpha by StyleAlpha, per-face styles and topology
  references via Placement::triangle_colours. A prototype's part is
  coloured by its own structure's styles. A colour index counts the colour
  array's doubles (pdf-issues #816).
- Occurrences' entity references: colour, hide or relocate the entity
  they name, in the occurrence's subtree, everywhere, or not at all.
- PrcFile::model_tree: the assembly tree as a model-tree panel lists it,
  with name sources, visibility and each node's placement range.
- TriangleMesh::transformed applies a Matrix: positions, cofactor-mapped
  normals, winding flipped under a mirror.

Never panics: occurrence and prototype cycles are Malformed; depth 64,
2^20 visits, MAX_PLACEMENTS and a face-override ceiling bound the walk;
counts are checked against the remaining data before allocating.
Markups, views, scene lights and clipping planes are Unsupported;
textures are read past.

Field orders the working draft leaves open follow prc-rs (MIT); each is
marked `// prc-rs:`, as are the step-1 container, Double and schema
choices that did.

Adapted from pdfcer (MIT, same author), crates/pdfcer-3d/src/tree and
vec3.rs at 0c9742b5936232af89cd707138ef149dc1f4a660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:44 -05:00
KenM76andClaude Opus 5.5 7f30266790 M12: PRC reader: tessellation and arrays (#1297)
Read the tessellation section of a PRC file structure:
FileStructure::tessellations() returns each entity as a Tessellation.

- TESS_3D becomes a TriangleMesh: positions, triangles from the
  triangle/fan/strip blocks (one-normal and textured variants included),
  one triangle range per face, stored normals and texture coordinates
  with per-corner indices, and per-triangle line-attribute styles.
- TESS_3D_Wire becomes polylines (closing and continuous flags); vertex
  colours are read past, sized by points (closing point included).
- TESS_Markup is read past.
- TESS_3D_Compressed is read field by field to its end so the entities
  after it decode; its triangles are not rebuilt (later step).
- The compressed Character/Short/Integer/Indice arrays and the Huffman
  container they use (WD 11.6-11.9, 12.2) live in arrays.rs; the base
  fields every entity shares (attributes, name, user data) in entity.rs.

Never-panic hardening beyond the source this is adapted from:
- Huffman symbol width must be 1..=16; the leaf table and element count
  are bounded by the payload before anything is allocated.
- A face may carry at most 8 texture-coordinate sets (an unbounded
  count allocated per set and padded per triangle).
- Every count is checked against the remaining bits; indice sums and
  deltas use checked arithmetic.

A few reading choices (Huffman bit order and code-length width, indice
running sums, 64-bit attribute order, three version gates, the
is_segment_color bit, the compressed field order) follow prc-rs (MIT);
each is marked `// prc-rs:` in the source.

Tests are synthetic, written with the crate's test bit writer. Each
fix was reverted and its test seen failing (17 reversions).

Adapted from pdfcer (MIT, same author), pdfcer-3d at
0c9742b5936232af89cd707138ef149dc1f4a660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:43 -05:00
KenM76andClaude Opus 5.5 ab552430a7 M12: PRC reader: the Double code table from prc-rs (MIT) (#1297)
Every PRC Double decodes through a fixed 2077-row prefix-code table
(working draft 11.17.2). The rows come from prc-rs src/double.rs at
287b671bb5b31b05efb5ec1b98f7c359b9e4dd7d under its MIT licence, whose
notice stays in the file header; ATTRIBUTION.toml records it.

The table is a complete prefix code, so the empty-table stub test is
replaced by a truncation test. The exponent lookup masks the 11 exponent
bits, because the last exponent row's upper word is 0x7ff80000.

Revert proof: with the table emptied, eight Double tests fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:43 -05:00
KenM76andClaude Opus 5.5 83b230ae73 M12: PRC reader crate: container, bit primitives, schema (#1297)
New standalone L0 crate pdfcraft-prc: the PRC container walk (file
header, file-structure descriptions, per-section zlib inflation under a
512 MiB ceiling), the bit-level primitives of the compressed sections,
and the schema interpreter a newer producer uses to describe fields it
added (step and nesting ceilings).

The Double prefix-code table is not in this commit: Double reads
return UnknownDoubleCode, and the tests that need the table are
ignored with that reason.

Adapted from pdfcer (MIT, same author), crates/pdfcer-3d at
0c9742b5936232af89cd707138ef149dc1f4a660.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 12:26:43 -05:00
FredApps 168a3cb91e Merge pull request #5740 from Combiler01/fix/panel-resize-scrollbar-5730
fix(ui): prevent panel resize handle collision with scrollbar (#5730)
2026-11-02 17:47:35 +01:00
FredApps 8f1441a3f2 fix(ui): keep pane resize edges right of the border, off the page scrollbar (#5730)
The centred 4 pt strip still covered the right 3 pt of the page's
scrollbar beside a right-docked pane. Register the host's resize edge
again from the border rightwards, on the host's own layer, and leave an
inert edge inert. The test now hovers and drags both scrollbars beside a
right-docked Tools pane (CodeRabbit) and checks the width is unchanged.
2026-11-02 16:59:26 +01:00
FredApps 1997b8ed94 Merge remote-tracking branch 'origin/main' into pr5740 2026-11-02 16:29:14 +01:00
FredApps e261518727 Merge pull request #5569 from Piohrun/feat/jbig2-run-allowance
M2.6: the images of a run, and a page's JBIG2 images, share one allowance of decoding
2026-11-02 15:11:32 +01:00