Commit Graph
4320 Commits
Author SHA1 Message Date
jonathanwxh-cell 644f7ab685 fix(render): clip extracted text to the page CropBox (#2670) 2026-10-16 18:51:55 +08:00
hade0ntemaandClaude Sonnet 5.5 37a9346629 fix(select): clear the picked picture in the edit itself, not on the next frame
The first fix dropped a stale pick while painting the next frame. The window only redraws on
the next input, so after deleting a picture in Edit text & images its highlight stayed on screen
until the user clicked. Clear the pick in apply_edit, in the same step as the edit, and ask for a
repaint. drop_stale stays for the paths that change the document without apply_edit (Undo, Redo).

An edit-level test checks the pick right after apply_edit with no frame in between; it fails
without the change. The earlier one-frame test could not tell, the test harness redraws more
often than the app does.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 17:40:55 +08:00
alehelandClaude Opus 5.5 93df949e3e fix(assets): refresh the catalogue hashes for the backdrop translations
The 22 interface catalogues are registered assets; adding the Add
Background strings changed their SHA-256. Provenance is unchanged
(contributor-original translations).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:40:07 +02:00
alehelandClaude Opus 5.5 b5d34d439f fix(ui): translate the Add Background backdrop messages
The six new dialog strings (the backdrop offer, its note, the scan
warning and "Checking pages…") were missing from every catalogue, which
the *_covers_ui_literals tests require. Languages with several plural
forms use the "label: {n}" pattern so any count reads correctly.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 16:47:49 +08:00
alehelandClaude Opus 5.5 a9b0db12c6 fix(edit): show backgrounds on pages that paint their own white backdrop
Add Background draws behind the page content, so on a page that first
fills itself with opaque white (browsers' Save as PDF, print drivers) the
background was hidden completely and the tool looked broken.

- pdfcraft-edit: new `backdrop` module. A strictly detected backdrop (one
  axis-aligned `re`, opaque white in a device colour space, painted before
  anything else and covering the crop box) is replaced by turning that one
  fill into `n` in a per-page copy of its stream; the original stream is
  left untouched for other pages. Remove background restores it, unless
  the copy was edited since. Page-sized pictures (scans) are reported only.
- Background gains `replace_backdrop` (default on); the default colour is
  a visible light yellow instead of near-white.
- Dialog: offers to replace the backdrop, warns about scanned pages,
  checks pages a few per frame, preview draws text over the colour.
- Automation: `doc_background` takes `replace_backdrop`; new read-only
  `doc_backdrops` lists pages with a backdrop.

Acrobat's behaviour on such pages is unobserved; recorded as our choice
in parity/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:46:02 +02:00
hade0ntemaandClaude Sonnet 5.5 da48b84b8c fix(select): a picked picture that is edited away no longer leaves its highlight behind
With the Select tool a click picks a picture and draws a blue highlight over it. The pick was
only cleared by Escape, a drag or another click. Picking a picture, then entering Edit text &
images and deleting it left the highlight on the empty spot, with nothing left to click to
remove it.

The pick now remembers the document's edit generation and is dropped once the document has been
edited since (a delete, a move, Undo), the same condition under which the copy shortcut already
refused it as stale.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 10:33:24 +02:00
Enrico SitaandClaude Opus 5.5 c66384f92b fix(ui): Read mode hides the quick toolbar (#2646)
Read mode hides the tools, not the navigation panes, as Acrobat's does
and as switch_mode already says, but the floating quick toolbar was
still drawn over the page there. It is now hidden in Read mode, and
entering Read mode puts the current tool down (back to Select), so no
markup tool stays active without a toolbar to show it. The right rail,
the page navigation and the V/H shortcuts are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 10:27:59 +02:00
Ahmad Mujahid 4f3f2fe1d2 M5: batch Comments panel thread and page lookups (#2669) 2026-10-16 12:03:40 +04:00
Enrico SitaandClaude Opus 5.5 26051871d0 M11: remove PDF/A or PDF/X identification (optimize.remove-standards-id)
A file edited after it was made PDF/A or PDF/X can keep claiming a
standard it no longer meets. organize::remove_standard_id removes the
claim: the pdfaid: XMP properties for PDF/A, or GTS_PDFXVersion and
GTS_PDFXConformance in the document information and as pdfxid: XMP
properties for PDF/X. Output intents and other claims (PDF/UA) stay.

The XMP is edited with the packet editor Document Properties already
uses (sync_info and the new remove_properties share its read and write),
so the rest of the packet stays byte for byte and unsafe packets are left
alone. It is one undoable edit, in Standards ▸ PDF/A as a button worded
like its undo label, and the standards_remove_id tool (pdfa or pdfx).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:45:31 +02:00
Ahmad Mujahid e447749109 Fix wrapped revision controls overlapping Binding (#2739) 2026-10-16 11:22:54 +04:00
Ahmad Mujahid 9409265c35 M10: apply component Decode during lossless image export 2026-10-16 11:06:55 +04:00
wstockandClaude Fable 5.1 6e3a16f96a Issue #2368: flattening keeps a NoRotate widget upright on a rotated page
A widget (or any annotation) with the NoRotate flag is shown upright on a
page with /Rotate: its appearance turns with the page about the rectangle's
upper-left corner (ISO 32000-2 12.5.3), which is how MuPDF and Poppler draw it.
Flattening drew the appearance onto the rectangle as it lies in user space
(Algorithm 8.1 alone), so on a /Rotate 90 page the flattened widget turned
sideways and moved while its neighbours without the flag stayed put.

The placement matrix of a NoRotate annotation is now followed by that turn
(`upright`), for 90, 180 and 270.

Regression test: tests::flattening_keeps_a_no_rotate_widget_upright_on_a_rotated_page
(a plain and a NoRotate widget on pages rotated 0/90/180/270; the plain one's
cm is unchanged, the NoRotate one's is the turned matrix). Verified on qpdf's
manual-appearances.pdf page 2 (/Rotate 90): Poppler's render of the flattened
page differs from the original's by 80 pixels at 36 dpi (anti-aliasing),
against 648 before, when the NoRotate widget's box moved.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh
2026-10-16 06:19:15 +00:00
FredApps 10b768ff42 Test idle autosave after a second edit 2026-10-16 08:08:35 +02:00
Fernando Areán MagánandClaude Opus 5 c21178ab16 Merge main and delete the two copies it grew back
Main solved this branch's problem on its own: #1712 replaced the two
duplicated catalog paragraphs with one current paragraph that covers
every shipped language. Two and a half hours later #2320, branched
before that, added its two copies back, so main carries three again.

Keep #1712's paragraph exactly as main wrote it and delete only the two
stale copies, so this branch no longer rewords anything.

Docs only, no code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-16 08:03:20 +02:00
Il Min Brian Moon 8026e85097 contributors: add my own entry (brian-moon)
Claude-Session: https://claude.ai/code/session_01RNjF2Pc6JWeHW1UVPK8uVg
2026-10-15 21:57:33 -07:00
Il Min Brian MoonandClaude Opus 5.5 d6a4602f70 fix(ui): Korean (Hangul) file names and tab titles draw as boxes (#2747)
No embedded interface face has Hangul, and the installed fallbacks covered Arabic, Devanagari,
Telugu, Sinhala, Tamil and (for a Chinese interface) Han only, so a Korean file name in a tab,
the title bar or the bookmarks panel drew as boxes (#2747, #407, #1043).

hangul_fallback() adds the first installed Korean face that has the whole syllable block, like
the Telugu and Tamil fallbacks, and it ends every family next to them. The probe is U+D7A3 (힣),
the last of the 11,172 modern syllables: Droid Sans Fallback has 가 but few other syllables, so
a 가 probe would accept a face that still draws boxes. Candidates: NanumGothic, UnDotum,
Baekmuk Dotum (Linux), Malgun Gothic, Gulim, Batang (Windows), Apple SD Gothic Neo, AppleGothic
(macOS). Noto CJK and Source Han are left out, as for Chinese (AGENTS.md §1.1).
PDFCRAFT_SYSTEM_FONTS=0 turns it off like the others.

Test: system_fallback_covers_hangul (skips on Linux without an installed Korean face).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNjF2Pc6JWeHW1UVPK8uVg
2026-10-15 21:57:33 -07:00
PerchAnton f79ab9ae2d M12: update viewer-warning PR against main (#2589) 2026-10-16 07:48:00 +03:00
PerchAnton d78df1f163 M12: update growable-field PR against main (#2569) 2026-10-16 07:44:37 +03:00
PerchAnton 4d0f2e4dde M12: update print-button PR against main (#2572) 2026-10-16 07:41:57 +03:00
PerchAnton 39e2481d7d M12: update rich-text limit PR against main (#2570) 2026-10-16 07:37:07 +03:00
Marco Arnulfo c736697680 fix(ui): Save asks where to keep an Apple Mail attachment (fixes #2751) 2026-10-16 06:13:19 +02:00
NetterSilverandClaude Haiku 5.5 fdc8099ac2 Keep leading spaces and tabs when wrapping plain text into a PDF
Found while testing crates/fonts/src/lib.rs:79 (wrap_with), called from crates/create/src/lib.rs:set_text

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 04:08:29 +00:00
Dylan M. Taylor cfb71d850e ci: switch to shared nix workflow from craft-common-actions 2026-10-15 23:57:29 -04:00
skylar taylor-barrick 361d0e6ef8 test: refresh regression coverage after CI workflow update
Refs #2067. Incorporate the current main workflow while preserving the qualified test changes.
2026-10-15 21:45:10 -06:00
skylar taylor-barrick d43b9a1f86 test: refresh open regression coverage
Refs #2067. Preserve the original regression commit while incorporating upstream changes.
2026-10-15 21:28:11 -06:00
skylar taylor-barrick f0c9901c39 Merge pull request #2727 from dylanmtaylor/ci/craft-common-actions
ci: add nix check workflow
2026-10-15 21:25:16 -06:00
hcchung1andClaude Sonnet 5.5 a51380e869 docs: record the XFA page area occur fix in parity and the ROADMAP log (#2574)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 10:46:39 +08:00
hcchung1andClaude Sonnet 5.5 31716e7058 fix(xfa): a page area without <occur> has no limit (fixes #2574)
A pageArea with no <occur> was given a subform's default (at most one use). Once the
first body page was out, the layout fell back to the last page area, here one with
occur max="1", and drew its content over every body page, with a copy of its fields
per page. A page area with no <occur> now repeats as long as pages are needed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 10:46:25 +08:00
FredApps f4ff7fe433 Refresh Pages sidebar shortcut after main updates (#2052) 2026-10-16 04:07:56 +02:00
FredApps a99f1ab9cb Refresh form export values against main (#231) 2026-10-16 03:56:32 +02:00
Christoph OrtenburgandClaude Opus 5.5 c0a8a1a10f test(cos): qpdf can't check a prefixed file whose offsets include the prefix
qpdf counts every offset from the header in a file with bytes before it, so
it already reports the input of
`incremental_saves_of_a_file_whose_offsets_count_from_its_first_byte_keep_counting_so`
as damaged ("xref not found") and reconstructs it, before anything is
appended. The qpdf check there could never pass; the test's own reader checks
stay. The header-relative test keeps its qpdf check, which passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:46:45 -04:00
Osvaldo DuranandClaude Opus 5.5 e5c2ea2bff Merge upstream main 5fdd820b into pr/attachments
Conflicts: ATTRIBUTION.toml, crates/ui-egui/src/i18n/es.tsv, crates/ui-egui/src/i18n/vi.tsv. Catalogs keep upstream's rows, then this branch's rows upstream lacks; ATTRIBUTION.toml is upstream's with every sha256 recomputed; progress lines keep both sides, upstream's first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:31:50 -04:00
alehel 8f0b36d66e Merge remote-tracking branch 'origin/main' into feat/all-tools-expand-view
# Conflicts:
#	ATTRIBUTION.toml
#	crates/ui-egui/src/i18n/es.tsv
2026-10-16 03:25:54 +02:00
Dylan M. Taylor 8fa6fecf70 ci: add nix check workflow 2026-10-15 21:17:37 -04:00
Chiranjeevi Uandskylar taylor-barrick 5fdd820bdb fix(ui): validated signature display, snapshot tab reuse (#1895)
* fix(ui): validated signature display, snapshot tab reuse

Signature widgets with layered appearances baked the unverified state
into the page, so a valid signature kept reading 'Signature Not Verified'
while the banner and panel said valid. Validated verdicts are now drawn
into the icon and caption layers in the display copy only: the signed
bytes are untouched, saving keeps the original appearance, and files
without those layers or without a valid verdict render exactly as before.

Other valid signatures still get the verdict mark over their widget,
sharing one style with the Signatures panel, and hide it again once the
appearance itself shows the verdict.

View signed version and View revision opened a new tab on every click,
and every tab showed the document title, so repeats piled up identical
tabs. Repeats now reuse the snapshot tab, snapshot tabs read like their
source plus the suffix in the tab strip, window title and Combine picker,
and carry a notice bar naming the revision with a Close that asks about
saving first.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Address review comments on validated appearances and trust refresh

Count every field when checking shared appearance streams, so a
caption shared with an unvalidated field is never rewritten for the
valid one alone.

Pick the icon slot by drawing instead of length, and only when a
single layer qualifies. Bare rectangles never count as the icon, and
ambiguous or missing slots fall back to the overlay badge.

Revalidate and invalidate together through one helper, so trusting a
certificate always drops rendered pages. Trust alone still marks
nothing dirty.

Tests: mixed-verdict sharing, longer decorative borders, competing
and doubled layers, capture-before preservation, trust invalidation
without zooming, and signed Session round trips with rendering,
saving and passwords.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Prove the icon slot geometrically and cover real layered signatures

A lone line drawing is still not the icon: frames and borders span the
box while the status mark lives in a corner of it. The icon now has to
sit strictly inside the appearance frame and cover at most half of it;
anything else falls back to the overlay badge. Widgets without a
measurable frame veto the rewrite the same way.

Tests: parsed extents and inhabitant checks as units, both reported
border repros plus a frameless widget, and real layered signatures
built with cms::sign_detached through Session open, trust, rendering,
save and reopen, plain and password-protected.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Identify the icon by shape instead of geometry

A lone line drawing is still not the icon, but size and position were
the wrong signals: the real status mark is large, and geometry blocked
it. Frames, borders and panels are rectangles by nature while the mark
is not, so a drawing made only of axis-aligned rectangles is decoration
however it is stroked. Singularity, shared-stream and template checks
are unchanged; anything ambiguous still falls back to the overlay badge.

Tests: rectangularity as a unit, nested line-drawn frames, and a
triangular fixture mark in place of the rectangular one.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Budget the appearance walk and keep the marks with the bytes

The walk that finds a signature's icon and caption capped depth,
layers and fields, but not the work: a dictionary whose entries all
point at the next one is visited once per entry per level, so a small
hostile file never finishes opening. The walk now counts its visits
and gives up, and a field whose tree cannot be read to the end stops
the whole pass, because a layer we never read may be the one two
fields share and a rewritten shared layer puts one field's verdict on
another field's page. The sharing count is exact again with that: two
widgets of one field pointing at the same appearance claim it once,
or the field vetoes its own rewrite.

A mark used to outlive the bytes it described. A failed display write
left the page showing the file's own caption while the verdict still
claimed its state was drawn; a metadata edit dropped the mark while
the page kept drawing the state, so the badge landed on top of it. The
write fallback now drops the marks and logs the loss, and metadata
carries them over.

Tests: a self-referential appearance tree, which never finished before
the budget, two widgets sharing one appearance, a metadata edit that
keeps its mark, a save that goes through the writer instead of
short-circuiting on the opened file, and the snapshot bar's Close
button pressed for real.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* Cite the new regressions in parity, restore main's installer note

The appearance entry now cites the bounded walk and the save through
the writer, and says what a tree that cannot be read to the end does.
The installer entry carried main's older text, which still claims
signed installers are planned; main's newer note is taken verbatim.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: preserve rounded decoration beside raster status glyphs

* M9: place appearance progress beside signature history

* M9: retain appearances with ambiguous curved layers

* M9: keep signature verdicts in viewer overlays

Defer generic appearance rewriting: caption text and a unique drawing do not establish an icon role. Preserve original artwork for saves, image and text exports, and both print modes while retaining snapshot reuse and trust refresh. Cover chamfered borders and mixed signer/caption streams through signed Session round trips.

---------

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-15 19:16:48 -06:00
Krzysztof Cieślik ad9e5d2ba9 fix(edit): keep a one-row centred title centred when it grows (#2581)
- Detect a one-row centred title through a new `is_centred` helper in
  `text_regions.rs`, shared by both rewrite paths, instead of the multi-row
  ragged-centres rule that can never fire on a single row.
- A lone row counts as centred when its centre sits within one font size of
  the page centre and its left edge is not the page's dominant left edge, so a
  full-width left-aligned line at the margin is not mistaken for a title.
- In the single-style path, a centred block with no explicit width or
  alignment no longer has its width stretched to the page margin; the line is
  centred in the box it already sits in, so a grown row keeps its centre and
  every wrapped row shares it.
- Gate the new rule behind main's `upright` flag: `is_centred` measures
  page-space rects, which are not meaningful across a turned baseline; frame
  coordinates (#1443) are the tool for that, not this change.
- Add `a_one_row_title_keeps_its_centre_when_it_grows_and_when_it_wraps`,
  built from the issue's fixture; red before the fix (centre 398.025 vs
  expected 306), green after.
2026-10-16 02:49:20 +02:00
mtlcl410andClaude Opus 5.5 271cb6161f M1: read an xref table numbered from 1 that begins with object 0 (#2676)
Some producers head a classic xref table "1 n" and then write object 0's
free entry first, so every row is one number off and object 1 reads as
free. The renderer coped, but cos did not: printing a scanned invoice
built from cos dropped object 1 (page 1's image) and printed the page
blank. Such a subsection is now read from 0 when its second row locates
an object that is numbered 1, in both xref readers, and the repair is
logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-16 08:35:35 +08:00
Bone NI 4164c0e385 i18n: add Lao (lo) translation support 2026-10-16 07:19:53 +07:00
FredApps a5ef6b76e8 Wake idle windows for pending recovery snapshots (#2663) 2026-10-16 02:17:12 +02:00
FredApps 44d795d9f8 Refresh Pages sidebar select-all against main (#2052) 2026-10-16 02:16:58 +02:00
jonathanwxh-cell 9229e7af36 fix(edit): wrap single turned rows at the page margin (#2578) 2026-10-16 08:13:01 +08:00
Claude 33fe488061 fix(cos): an incremental save of a file with bytes before its header counts from the header
Some files carry bytes before `%PDF-`, and their offsets count from the header rather than from
the first byte. We read those, retrying a section relative to the header when the absolute
offset misses, but the incremental writer then appended a section whose offsets and `startxref`
were absolute while its `/Prev` kept the header-relative value it was given. qpdf reported "xref
not found" and reconstructed the file, so a save that reported success left a damaged-looking
update behind (#2364, found with KillerPDF's reserve3.pdf and a Subject edit).

The reader now records on each `Revision` whether it was found counting from the header, and the
writer counts the new section the same way as the one its `/Prev` points to. A file whose
offsets already include the prefix keeps absolute offsets, which the second test pins so the fix
cannot overcorrect.

Seen red with the writer change reverted: the appended section's `startxref` missed its `xref`.

Fixes storytold/pdfcraft#2364

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsT1y8xBvkXeBxEUQqdDtj
2026-10-15 19:54:17 -04:00
PiohrunandClaude Opus 5.5 f65f733041 fix(render): faster sampled functions, image decodes across zoom levels and patch meshes (vendored hayro-interpret patches 31–33) (#2436)
* fix(render): sampled functions index a flat sample array (vendored hayro-interpret patch 31)

A Type 0 sampled function kept its samples in a HashMap keyed by each point of
its /Size grid (two SmallVecs per key, SipHash), and the renderer builds a
shading's functions again every time it is painted, so every paint hashed one
insert per sample. A 2,097,152-sample ramp painted 30 times took 38 s; the
pdf.js corpus's bug1721218 (InDesign) spent most of a 1.5 s page on it.

The samples now stay in stream order in one Vec, and a grid point is found by
strides (first input fastest). The interpolation keeps the same recursion and
arithmetic, so results are bit for bit the same, and a point the stream ended
before still gives no value. The repeated-paint page draws in well under a
second. The maximum sample value no longer computes 2^32 in a u32 (a panic with
overflow checks on at 32 bits per sample).

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

* fix(render): one decode of an image serves every size it is drawn at (vendored hayro-interpret patch 32)

The decoded-image cache (patch 24) keyed each decode by the size the image is
drawn at, but only the JPX filter decodes a reduced resolution for a size:
every other filter decodes the whole image. So a JPEG or Flate scan missed the
cache at every zoom step, was decoded again, and was kept once per zoom level.

The key now carries the target size only when JPX decodes the image, its
/SMask or its /Mask (read from /Filter or /F, erring towards keeping the
target). `InterpreterCache::decoded_image_decodes` counts the cache's decodes;
the new test draws a Flate image at five sizes and sees one decode, and a
copy whose soft mask is JPX-encoded still decodes per size. Both assertions
fail with the old keying.

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

* fix(render): patch meshes copy their vertices' colours less (vendored hayro-interpret patch 33)

A Coons or tensor-product patch is drawn as 722 triangles over a 20 x 20 grid
of points, and every vertex's colours (a SmallVec) were produced or copied up
to five times: each grid cell interpolated its four corners itself (1,444
interpolations a patch for 400 points), cloned all six vertices of its two
triangles, and the mesh sampler cloned each vertex again to move the triangle
into device space. Profiling page 8 of the annual report in #1584 put a
quarter of its time in those clones.

Each grid point's colours are now interpolated once, a cell's second triangle
takes the vertices its first copied, and the sampler moves a patch's freshly
made triangles instead of copying them (`TriangleSampler::sample_owned`; a
free-form triangle mesh, which the shading keeps, is still copied). The same
inputs give the same values: rendering the first three pages of every pdf.js
corpus file is byte for byte the same. Release build, Linux: report page 8
773 -> 696 ms, personwithdog.pdf 149 -> 129 ms, issue13520.pdf 104 -> 87 ms.

The new test pins the colours near each corner, at the centre and between them
for a Coons and a tensor-product patch (taken before the change); swapping two
vertices' grid points makes it fail.

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

* docs: roadmap entries for faster sampled functions, image decodes and patch meshes

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:50:29 -06:00
Marco Arnulfo b387802f2d fix(comments): Add a text box takes a drag that sizes the box (part of #2669) 2026-10-16 01:43:30 +02:00
Nick Nyssen 2ec501fa74 test(cos): property tests for save fidelity, plus credit entry
Adds crates/cos/tests/proptests.rs (464 lines) - three properties over generated catalogs (edit sequence
vs a plain ordered model; full save in both output styles; incremental save over bases built BOTH ways) -
and a self-credit entry in contributors/people.toml committed under the author's own identity. No product
code changes; total +466 lines across two files: people.toml +2; proptests.rs +464.

Two design decisions that came out of adversarial review, each measured rather than asserted:
  * catalog keys insert in a scrambled order ([5,0,7,2,6,1,4,3] filtered to the drawn keys), because with
    ascending insertion "preserves key order" and "sorted by key" are the same observable.
  * incremental expectations derive from the values written, not from reading the writer's own output back,
    so a consistently-wrong implementation cannot pass by agreeing with itself.

Measured against one-line breaks in crates/cos/src/writer.rs, each reverted and byte-compared afterwards
(verdicts from cargo exit codes; "pre-existing" = lib 48 + writer 10 + encryption 8 + shared_streams 3 +
bounded_writer 4, i.e. every target that existed before this PR):

  sort every dictionary by key              CAUGHT (props 1+2)      pre-existing suite FULLY GREEN
  reverse dictionary emission              CAUGHT (all three props)   pre-existing suite FULLY GREEN
  move /Type to the end of every dict     CAUGHT (prop 1)           pre-existing suite FULLY GREEN
                                            - caught only after catalog_keys() was added to pin the full key
                                              sequence; without it this break was green everywhere
  classic-table incremental startxref +1   CAUGHT (prop 2 alone; props 1+3 green), failure message names
                                            "classic-table", confirming the incremental table path executes
  drop null-valued dictionary entries      CAUGHT (all three)        at the pre-fix commit b51381c this same
                                            break left property 2 green - the self-reference it removed

Known limits, stated not hidden: incremental saves are asserted to add bytes and leave the original prefix
untouched - minimality is NOT claimed; catalog key order pins one fixed scramble rather than drawn
permutations, so 25.4% of cases draw too few keys for order to be meaningful; nested dictionary key order
is deliberately not pinned (canon sorts inside values); /Perms and permissions semantics are out of scope
here.

Gates at this tip (upstream 24f93e6, drift 0): cargo test -p pdfcraft-cos 85 passed / 0 failed / 2
ignored; cargo fmt -p pdfcraft-cos --check clean; clippy exit 0 for this crate with
-D warnings -A clippy::chunks_exact_to_as_chunks (that lint fires only in upstream's own
crates/oracle/src/pixels.rs:189 from f988cf8, tracked by #2342/#2324; flags after `--` forward into every
crate in the build graph, and crates/cos itself emits zero warnings under a forced recompile).
cargo xtask layers OK: 37 crates.

Scope: part of #1187 (items 1-3 partly; the reference-following comparison, undo state machine, password
property and damaged-file splices remain open). The encryption-side properties are PR #2358 by the same
author; earlier drafts of this text mis-cited #2014, which is certificate encryption and still open.
2026-10-15 16:41:32 -07:00
Nick Nyssen cdbce8ac17 crypt: authenticate against encryption numbers we did not compute
Adds crates/crypt/tests/qpdf_literals.rs (305 lines): three tests that hand our security
handler /O, /U, /OE, /UE and /Perms values from outside implementations, then require our
reader to authenticate them and report the intended role. Tests only - no product code, no
new dependency; PDFs stay outside the repository and only quoted hex is committed.

Why outsiders wrote these numbers: create() and open() share one derivation, so a
wrong-but-self-consistent implementation passes every internal test. Two review rounds proved
it by mutation - several key-derivation breaks stayed green in both suites because writer and
reader drifted together. Quoting bytes we did not compute breaks the circle: open() recomputes
/U (or the revision 6 hash chain) and compares it to what was stored, so a divergent derivation
cannot authenticate.

Six cases come from qpdf core 12.2.0 via pikepdf (R2/R3, R4 in both metadata modes, R6 in both).
Revisions 2-4 are deterministic given /ID and passwords, so the generator reproduces their quoted bytes
byte for byte (verified). Revision 6 salts are fresh on every run by design: these literals match the
fixtures as captured, and re-running the generator will produce different - equally valid - revision 6
numbers, so compare R/V/Length structurally there and re-quote if you regenerate. One
case is HAND-DERIVED, labelled as such in the source: an independent re-implementation of ISO
Algorithms 2/3 at n = 5, cross-validated by having that same routine reproduce qpdf's n=16 /O
byte for byte. It exists because no conforming producer emits R3 at 40 bits - qpdf writes 128
there and this crate's create() maps Rc4_40 to revision 2 - yet the derivation loop only runs
for r >= 3, so without a crafted row its truncation is unobservable and mutating it changes
behaviour that nothing catches.

Measured effect, same battery, upstream --lib run separately each time:
  r6-hash-selector      (%3 collapsed to %2)      CAUGHT here; upstream stays green
  r6-sha-arms-swapped   (SHA-384 <-> SHA-512)     CAUGHT here; upstream stays green
  r6-selector-window    (sums 17 bytes not 16)    CAUGHT here; upstream stays green
  r4-key-chain-truncation (loop hashes whole digest) CAUGHT here, via the crafted row;
      upstream stays green. Pristine 3/3 pass, mutated 2 passed / 1 FAILED - verified twice.
Still invisible, named rather than glossed: r6-round-floor (minimum rounds 64 -> 63) - the
terminator does not separate the two floors on these inputs; /OE,/UE length loosening and the
/Perms filler source, both unreached here.

Corrections carried from review: qpdf's advice against a static /ID does not weaken these
revision 6 vectors, because key_r6 never receives the file /ID at all; it matters only to the
revision 2-4 paths, where we quote /ID literally so the numbers stay exact. And no ATTRIBUTION
entry is added, matching crates/crypt/src/tests.rs:237 which already quotes qpdf vectors
unattributed; cargo xtask assets stays ok.

Gates on a clean tree: cargo test -p pdfcraft-crypt 28 passed / 0 failed (17 lib + the 8 properties from #2358, now merged,
plus 3 oracle tests - one
per concern - role authentication across seven dictionaries, metadata-flag key separation, /Perms
decode);
clippy --all-targets -D warnings clean; cargo fmt --check clean; xtask layers OK (35 crates).

Integrity note: the literals in this commit were checked field by field against the PDFs on disk -
27 of 27 fields quoted verbatim, no transcription drift. A regression test for that check belongs in the
generator's output comparison rather than here, since it needs pikepdf.
2026-10-15 16:41:20 -07:00
Krzysztof Cieślik a3cbb85536 M7: let a list marker open an item and a hanging indent continue it (#2564) 2026-10-16 01:30:11 +02:00
jonathanwxh-cell f536ac7a05 fix(sign): fill typed signature outlines with non-zero winding (#1786) 2026-10-16 07:24:33 +08:00