Commit Graph
4320 Commits
Author SHA1 Message Date
KenM76andClaude Opus 5.5 801976a574 feat(organize): export, import and copy bookmark trees (#6018)
bookmark_export returns an outline, or one bookmark's subtree, as JSON:
titles, nesting, open state, colour, bold/italic and destinations as page
plus view in PDF user space (ISO 32000-2 12.3.2.2), optionally written to
a file. bookmark_import takes it back, inline or from that file, under any
bookmark at any position or in place of the outline, as one undoable edit;
bookmark_copy does the same from another open document by page number.

A bookmark whose page the target lacks keeps its place without a
destination and is reported in `unmapped`. Every node is checked before
anything is written (title, page, colour, view type and arity, 33 levels,
100,000 bookmarks), and the error names the node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-04 03:37:55 -05:00
FredApps 69fda9959e Merge storytold/main into fix/scroll-regression-1230 2026-11-04 08:56:42 +01:00
FredApps 9bbcde4699 Merge pull request #6063 from dotCooCoo/fix/continuous-scroll-prefetch
fix(ui): continuous view renders the pages ahead of the scroll
2026-11-04 08:56:30 +01:00
FredApps d21abe9881 test(ui): keep the eviction-order test over the window-sized allowance 2026-11-04 07:42:59 +01:00
FredApps 933a92fad0 Merge storytold/main into fix/continuous-scroll-prefetch 2026-11-04 07:42:58 +01:00
FredApps 1f8cb45353 Merge pull request #6065 from dotCooCoo/fix/window-sized-page-budget
fix(ui): size the whole-page raster allowance to the pages on screen
2026-11-04 07:42:41 +01:00
FredApps bde72b6f26 test(ui): pin the whole-page allowance ceiling per target 2026-11-04 07:03:52 +01:00
KenM76 6fbc1eb9d6 Merge remote-tracking branch 'fork/feat/prc-render' into feat/model3d-render 2026-11-04 00:03:33 -05:00
KenM76andClaude Opus 5.5 c336c59cfb M12: prc-render: scale-safe lengths, a render work ceiling, fit fixes
- vec3: length divides by the largest component before squaring, so
  a camera 1e200 away keeps a finite near plane; unit_cross normalizes
  each edge first, so edges of very different sizes keep their normal.
- render: MAX_RENDER_WORK (2^31) caps the pixel centres one render
  tests, charged per triangle's clipped bounding box before scanning;
  past it, RenderError::TooMuchWork.
- Bounds::centre halves each end before adding, finite for any finite
  box. Bounds::of and fit_meshes use only corners of triangles render
  draws. Camera::frame builds its basis from the direction, not from
  points near the model, so a small model far from the origin fits.

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-04 00:03:28 -05:00
KenM76 f59297454f Merge remote-tracking branch 'fork/feat/prc-reader' into feat/prc-render 2026-11-03 23:58:15 -05:00
KenM76andClaude Opus 5.5 b4ba4edc7f M12: PRC reader: charge copied blocks, refuse overflowing schema expressions
Review fixes from #6087:

- Picture and uncompressed blocks are charged to the parse ceiling before
  they are copied. File-structure headers may overlap, so each structure's
  copy counts. The ceiling test now includes the 3-byte picture.
- Schema expressions use checked multiply, add and subtract; an overflow is
  Malformed rather than a wrapped loop count.
- Each face's triangles are counted from its entity sizes and charged before
  it is triangulated.
- Matrix::mirrors divides each row by its largest entry before taking the
  determinants, so a mirror whose determinant underflows still reverses the
  winding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 23:58:08 -05:00
KenM76andClaude Opus 5.5 d3821f3574 M12: model_render premultiplies through as_chunks_mut
CI's clippy (1.99) flags chunks_exact_mut with a constant size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 23:47:10 -05:00
KenM76 66e32f2be5 Merge remote-tracking branch 'fork/feat/prc-render' into feat/model3d-render 2026-11-03 23:08:04 -05:00
KenM76andClaude Opus 5.5 ca21d4fb7b M12: prc-render: scale a triangle's edges before their cross product
normalize already scales by the largest component, but the face normal
was the normalized cross product of two edges, and edges near 1e160
overflow that product to infinity first, so the triangle was dropped.
unit_cross divides both edges by their largest component before the
cross product. a_huge_square_is_drawn fails with the old line (white
instead of [190,192,200]).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 23:06:54 -05:00
KenM76 4e2e77a986 Merge branch 'feat/prc-reader' into feat/prc-render 2026-11-03 22:56:15 -05:00
KenM76andClaude Opus 5.5 d0286a745a M12: PRC reader: winding by the whole determinant; walk charges each schema once
A negative w (uniform or projective) turns mapped triangles inside out
though the 3x3 block does not mirror; the flip and the normal sign now
follow the 4x4 determinant, falling back to the 3x3 block when the last
row is zero.

The tree walk read each structure's globals schema with its own full
allowance, then again for the globals. It now reads it once, charged to
the walk, and continues from there for the globals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 22:56:09 -05:00
KenM76 875d33506d Merge remote-tracking branch 'fork/feat/prc-render' into feat/model3d-render 2026-11-03 22:55:25 -05:00
KenM76andClaude Opus 5.5 fb05b733f9 M12: model_render, and a 3D reference's stream under /3D
A 3D reference dictionary holds the shared stream under /3D (ISO
32000-2 §13.6.3), not /3DD; it is followed once, and the caller refuses
anything that is not a stream, so a chain or cycle of references ends.
The tool is renamed model_render, since MCP tool names here are lowercase
letters and underscores only, and to match model_add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 22:54:28 -05:00
KenM76 a3dbbee20b Merge branch 'feat/prc-reader' into feat/prc-render 2026-11-03 22:20:52 -05:00
KenM76andClaude Opus 5.5 941612d8ab M12: Cargo.lock follows the 0.18.1 workspace version
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 22:20:50 -05:00
KenM76 f837fa652e Merge remote-tracking branch 'origin/main' into feat/prc-render
# Conflicts:
#	Cargo.toml
2026-11-03 22:20:39 -05:00
KenM76 03eb90fd43 Merge remote-tracking branch 'origin/main' into feat/prc-reader 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
KenM76 d40d270dae Merge remote-tracking branch 'fork/feat/prc-render' into feat/model3d-render 2026-11-03 21:44:55 -05:00
KenM76andClaude Opus 5.5 ae1b28cf1d M12: model3d_render, a 3D annotation's PRC model drawn to a PNG
pdfcraft-annot reads a page's 3D annotations and the model each shows:
the /3DD stream, or the stream a /3DRef names (at most four deep, so a
cycle ends), decoded within 256 MB and typed PRC or U3D by /Subtype.
pdfcraft-engine exposes both on Document.

The automation tool model3d_render picks a page's Nth 3D model, parses
PRC with pdfcraft-prc, assembles and frames it with pdfcraft-prc-render
from a chosen direction (default an isometric-style view, Z up), and
returns the image as a PNG. U3D gives an error until a decoder lands.
parity misc.3d-view moves to partial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 21:44:55 -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
Grzegorz MartensandClaude Opus 5.5 07f4403884 M2: a JBIG2 refinement of the page refines only its area
A refinement of the page refines "the current contents of the page
buffer, restricted to the area of the page buffer specified by this
segment's region segment information field" (ISO/IEC 14492 7.4.7.4), at
an offset of 0 (Table 35), and reads 0 outside that area (6.3.5.2).
hayro-jbig2 read the whole page at the refinement's location, the
pixels around the area too, so a refinement of the page decoded to
other pixels than were coded, and one wider or taller than the page was
refused.

Vendored patch 16: Bitmap::area copies the page's area, 0 off the page
and past its width whatever its rows' padding holds, and both handlers
refine that. Every refinement region now reads its reference at offset
0, so the per-page offset #6016 kept goes.

And RefinementContextGatherer::start_row loads the reference's words a
column left of where a row starts in it, as the reload would, not from
column 0. With the reference offset 32 or more left of a refinement (a
refined text instance from a narrower bitmap or an RDX below -31, a
refined symbol from such an RDX), each row's first context read 0 for
the reference column it starts at, and from 33 left for the one before
it too. That half was left out of #6048: alone
it read the page column left of a page refinement's area, which the
area now keeps at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 23:16:10 +01:00
dotCooCoo a262171999 fix(ui): continuous view renders the pages ahead of the scroll
In continuous view the only demand off screen was the pages near the
window. A fast wheel spin crosses them faster than a heavy page renders,
so pages came into view blank.

The view now keeps the direction it last moved and also asks for the
next two rows that way (four pages in two-up). A zoom or a layout change
clears the direction. Scrolling the other way drops those pages from the
queue, which cancels their renders. Pages on screen stay at the head of
the queue in their usual order, and the byte and count limits bound the
prefetch. The web build renders on the UI thread and does not prefetch.

A raster that arrives over the allowance now evicts the cached pages the
frame does not ask for first, then those behind the scroll, so it no
longer evicts a prefetched page that the next frame asks for again.

A prefetched page that is tiled at the current zoom keeps a cached
whole-page raster sharper than its backdrop, as a tiled page on screen
does. This also applies to the prefetch of single-page and two-page view.
2026-11-03 13:53:52 -08:00
dotCooCoo 1a2104d428 fix(ui): size the whole-page raster allowance to the pages on screen
Whole-page rasters shared a fixed 128 MiB allowance. In a 3440 x 1440
window a Letter page at fit width is a 2,980 x 3,850 raster (43.8 MiB),
so two pages on screen left no room for another, and scrolling back
over five pages rendered all five again. Zooming in on a smaller window
has the same result.

The allowance now holds eight rasters the size of the largest whole
page on screen, between 128 MiB and 384 MiB (256 MiB in the web build).
The size comes from the page's request at the current scale, or from
the raster held for it while a zoom hold leaves the request out. A
tiled page is on screen as a small backdrop and does not raise the
allowance. Queue admission, keeping pages already rendered, and
texture admission use the same allowance. Tiles keep their own
128 MiB allowance.
2026-11-03 13:36:13 -08:00
dotCooCoo 292f9c75ff Scroll: smooth wheel bursts and preserve precise movement 2026-11-03 12:12:11 -08: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
Grzegorz MartensandClaude Opus 5.5 858ce9608b M2: a test pins which bitmap a JBIG2 refinement refines
A region and its refinement, directly and through an intermediate
refinement, decode otherwise than the same refinements of the page, and
at 2, 3 to exactly the pixels they decode to at 0, 0, moved. A handler
that refines the page instead, or reads the region at the page's
offsets, fails it, as does dropping intermediate refinements; no test
caught those. The test's helpers move to the module. A comment still
said a refinement of the page reads "its own area"; it reads the page at
its own location. And only a location within a region's size of 2^32
came back onto the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 20:08:11 +01:00
Grzegorz MartensandClaude Opus 5.5 b9e068aa36 M2: a JBIG2 refinement of a region refines it where it is
"GRREFERENCEDX 0, GRREFERENCEDY 0" (ISO/IEC 14492 Table 35): a refinement
region segment that refers to another region refines that region where
it is. hayro-jbig2 offset the reference by the difference of the two
locations, which 7.4.7.5 says must be equal; where a stream has them
differ, the refinement decoded against a shifted reference, or, the
region past i32::MAX, against none. It now decodes as coded, and only a
refinement of the page takes an offset, from its own location.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 19:32:29 +01:00
Grzegorz MartensandClaude Opus 5.5 01d3479f26 M2: a JBIG2 region located past i32::MAX is off the page
A region's location is unsigned (ISO/IEC 14492 5.4.4), and hayro-jbig2
took it for a signed one. A region at 0xFFFFFFFC was drawn back onto the
page four pixels left of it, and a refinement anywhere past i32::MAX, of
the page or of a region at the same place (7.4.7.5), was refused as an
overflow, and the page with it.

Vendored patch 14 (13 is #5999): Bitmap::combine_region keeps such a
location past the page, for the four kinds of region drawn onto it, and
a refinement's offset to its reference is taken in 64 bits and held to
an i32, which leaves every reference pixel it reads outside the
reference, as the exact offset does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 19:14:08 +01:00
KenM76andClaude Opus 5.5 ace92f3f2f Merge origin/main
Tests only: both sides added tests at the same place; both kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 11:09:49 -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