Commit Graph

1250 Commits

Author SHA1 Message Date
mtlcl410 ecffce69ee fix(engine): Save writes a repaired file in full even without edits (#1937) (#2029)
* fix(engine): Save writes a repaired file in full even without edits (#1937)

A damaged file is repaired on open (its cross-reference index rebuilt from
object headers, as Document Properties reports), but Save wrote it back as it
was: `Session::save_bytes` returned the bytes as opened whenever nothing was
edited, and a repair is not an edit. The "repaired" file on disk was still the
damaged one.

`save_bytes` now writes a reconstructed document in full even with no edits.
`Document::reconstructed()` in cos names the condition (no revision chain:
rebuilt, or built from scratch), and `write_incremental` uses it too, so the
writer and the engine share one definition.

Tests: `engine::tests::a_repaired_file_is_rewritten_by_save_even_without_edits`
(table offsets shifted; no table at all) and
`automation::saving_a_repaired_file_puts_the_repair_on_disk` (doc_open,
doc_info, doc_save over the same path, reopen). Both fail with the condition
removed: the saved bytes equal the damaged file. parity: core.repair-broken-files
cites them and notes that a repair still does not count as an unsaved change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

* fix(engine): a signed repaired file keeps its bytes on Save without edits (#1937)

Review follow-up: writing a reconstructed file in full on Save puts the
repair on disk, but for a signed file it would also invalidate its
signatures without warning. Save now skips the early return only when the
document is reconstructed and not signed. A regression test signs a
document, appends a `startxref` that points nowhere (the signed bytes stay
intact, the cross-reference data must be rebuilt) and checks that Save
without edits returns those bytes.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-18 10:47:28 -04:00
enes yıldız 1952a88801 fix(cli): use laid-out pages for dynamic XFA forms (#2813)
Fixes #1645. For a dynamic XFA form, `info` reported the placeholder
page and field counts, and `text` and `render` read the placeholder
pages, while the app and the automation tools lay the template out.

The CLI now opens dynamic XFA forms through the engine session and uses
its laid-out document for page and field metadata, text and pixels.
`info` keeps the input's file size and also reports the original counts
as `pdf_pages` and `acroform_fields`; `text` checks the copy permission
before extracting. The input file is never written.

Checks run (rustc 1.99.0): cargo fmt --check; cargo clippy -p
pdfcraft-cli --all-targets -- -D warnings; cargo test -p pdfcraft-cli;
cargo xtask layers; cargo xtask assets.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:54:00 -04:00
Benjamin Michel 6525140955 M3: type an exact zoom into the rail's zoom field (#1755)
* M3: type an exact zoom into the rail's zoom field

The rail's zoom percentage was a read-only label. It is now a text field:
Enter applies a typed percentage (150, 150%, 62.5 %), Escape or unparsable
input keeps the current zoom. The control channel's zoom option uses the
same parser.

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

* M3: document parse_zoom_percent

* i18n: Vietnamese for the zoom field's tooltip

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:53:24 -04:00
Marco Arnulfo b68b3c1119 M3: the Pages panel follows the current page (#2785)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:52:40 -04:00
Yoann LAISSUS 2c4989413f M7: the Edit text caret is as tall as the row it is on (#2462)
In a region that mixes sizes (a 24 pt title over 12 pt lines), the
inline editor's caret on a small row was as tall as the region's first
size and reached into the row below. egui draws the caret at least one
row of the TextEdit's font tall, and that font had the paragraph's first
size. Each row already carries its source row's height, so the
TextEdit's font is now half the region's smallest size and only sets
that floor.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:51:52 -04:00
Andrew Barnes 6ce3bc198b fix(gpu): request adapter-supported device limits (#2383) (#2433)
Clamp the surface device descriptor to the adapter's capabilities while
preserving its maximum 2D texture extent. Cover V3D shader variables,
lower GL capabilities, alignment ordering and capable-device defaults.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:50:42 -04:00
Ian Holloway 1581cd1edd M10: keep the OCR scan fixture at its original page size (#2399)
Assisted-by: GPT-6

Signed-off-by: IanHollow <72767437+IanHollow@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:50:16 -04:00
hms5232 a50ddd4de1 fix(i18n): zh-hant catalog uses Taiwan vocabulary consistently (#2856)
Several entries carried mainland-Chinese computing terms in a catalog
written in the vocabulary used in Taiwan:

- 丟失 → 遺失 (unsaved-changes warning)
- 郵政編碼 → 郵遞區號 (Zip Code field formats)
- 自簽名 → 自我簽署 (self-signed digital IDs, three entries)
- 灰度 → 灰階 (Grayscale Images, Print in grayscale; the rest of the
  file already used 灰階)
- 匹配 → 相符 (ten search/match entries; 符合 stays for the
  fit/conform sense: 符合頁面, 符合 PDF/A)

Translation column only — source keys, placeholders and ellipses
unchanged. ATTRIBUTION.toml hash re-pinned.

Generated with [Devin](https://devin.ai)

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:49:36 -04:00
NetterSilver 2eb1639097 Find credit card numbers followed by an expiry, year or CVV (#2969)
* Find credit card numbers followed by an expiry, year or CVV

Found while testing crates/redact/src/patterns.rs:180-190 (credit_card, via digit_run at :53)

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

* Redact: a card followed by a number drops at most four trailing digits

A longer grouped number (an IBAN's digits) no longer matches on a Luhn-valid prefix; the run is
read far enough to see the tail and refused when it goes on past that.

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:49:13 -04:00
MarekSpodBiedry 25495b959b Keep a subscript or superscript in its line when extracting text (#3174) (#3244)
* Keep a subscript or superscript in its line when extracting text (#3174)

* Text extraction: only a glyph right beside the previous one gets the sub/superscript centre limit

A large negative gap (a small glyph drawn back under a large one) kept the wider limit.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 09:48:49 -04:00
mtlcl410 2bd6d1357d M5: flattening comments leaves 3D, multimedia and print-production annotations (#3087) (#3089)
doc_flatten with comments chose every non-widget annotation except Link
and Popup, so a comments-only flatten baked in and deleted 3D, Screen,
Movie, RichMedia, PrinterMark, TrapNet and Watermark annotations with
their data (a 3D annotation's U3D model and default view). The comments
branch now uses pdfcraft_annot::is_comment_subtype, the classifier the
comment list uses; widgets follow `fields` only.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:48:26 -04:00
mtlcl410 f0c275669a M7: never encode typed text with CID 0 (#3223) (#3235)
Word's Type 0 subsets map CID 0 (.notdef) as well as the real space glyph
to U+0020 in ToUnicode and list a width for CID 0. encode keeps the lowest
code for each character, so an edited line wrote every space as <0000> and
drew a box where each space was. CID 0 is .notdef whatever ToUnicode says,
so has_glyph now refuses it and the space takes the real CID.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 09:48:14 -04:00
NetterSilver 7e3f413f52 Redact email addresses whose local part starts with punctuation (#2948)
Found while testing crates/redact/src/patterns.rs:141-146 (email)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 09:48:02 -04:00
Marco Arnulfo 50f261d70c M6: detect a row of comb boxes as one comb text field (#2111)
* M6: detect a row of comb boxes as one comb text field (fixes #1887)

* M6: keep empty tables of wide cells out of comb detection (#1887)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:50:00 -04:00
Enrico eeadc79d15 M4: reverse page order (organize.reverse-pages) (#1720)
Organize pages ▸ More ▸ Reverse pages (and the command palette) reverses
the selected pages among their own positions, or the whole document when
fewer than two pages are selected. It is one undoable step, needs the
assemble permission like the other page edits, and is available headless
as the page_reverse automation tool (pages optional, default all).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:48:47 -04:00
Marco Arnulfo bd0b3fdb65 Save As to another file writes a clean copy without earlier revisions (fixes #2165) (#2174) 2026-10-18 03:48:35 -04:00
wstock 72dac33f8e Issue #2367: an object stream whose entry cannot place it is found by its header (#2398)
* Issue #2367: an object stream whose entry cannot place it is found by its header

qpdf's xref-compressed-in-compressed.pdf marks object stream 69 as itself
compressed inside stream 81, which the format forbids and which does not
exist. objstm() loaded the container through its cross-reference entry only,
so every object inside (the outlines, the page labels) read as null and a
full save wrote `/Outlines null /PageLabels null`, while an incremental save
(which copies the original bytes) kept them, and MuPDF recovers them.

The container is now found by its own `N G obj` header when its entry cannot
load it (scan_for, the same last resort a plain object with a wrong offset has).

Regression test: document::tests::an_object_stream_marked_as_compressed_is_found_by_its_header.
Verified on the fixture: objects 70 and 82 load (Count 6; 12 page-label
ranges) and a full save keeps both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011K1peyrh5MdZ6ttHkVq8Gh

The fallback is for file entries only: once the stream is freed or replaced in the overlay,
its objects must not come back from the file's copy (sign's
object_stream_changes_cannot_reuse_old_candidate_membership caught this after the rebase);
a cos test covers both cases.

* cos: a container the saved table marks free is not found by its header either

Freeing an object stream, saving incrementally and reopening left the
overlay empty and the table saying the stream is free; the header
fallback then rescanned the file and the stream's objects came back on
the next full save. The fallback now also stops at an on-disk free
entry.

* M1: put the #2367 test's description on the test, not its fixture

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:48:23 -04:00
Ridwan 5827afdcde M8: measure fonts without /Widths with their embedded program (#2130) (#2232)
Find and the renderer place glyphs of a simple font that has no /Widths
with the embedded font program's own advances, but redaction guessed the
widths from the font's name (a standard-14 face, Helvetica by default).
When the guess was wider than the real font, the end of a line under a
mark seemed to lie past it and was kept in the content stream, and the
verify pass, using the same guess, agreed.

Metrics now fills the width table from an embedded sfnt program
(FontFile2, or OpenType FontFile3) when /Widths is missing, looking the
glyph up as the renderer does. A subset's unmapped codes get width 0 so
has_glyph stays false for them. Fonts with /Widths, and bare CFF or
Type 1 programs, are unchanged.

Fixes #2130
2026-10-18 03:48:00 -04:00
mtlcl410 94f1a0d157 fix(engine): a new or recovered document stays unsaved through undo (#2346)
* fix(engine): a new or recovered document stays unsaved through undo

#1435 made undo back to the last saved bytes clean. For a document
created by Combine, Extract or Create, or restored from recovery, those
bytes were never written where the document belongs, so edit then Undo
made it clean: Close no longer asked to save and autosave stopped keeping
it. Editor::unsaved_file, set by open_new and mark_recovered and cleared
by mark_saved, keeps such a document unsaved until it is saved; Revert
follows the same rule.

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

* fix(engine): test that doc_close refuses a recovered or new document after undo

Review follow-up on #2346: a recovered document and a new one, rotated
and undone, are refused by doc_close without discard_changes, and close
with it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:47:43 -04:00
komo1iddin 36d239d2b1 M7: fold repeated group moves of a cropped image into one clip cm (#2476)
Moving an image whose crop is its own (`q re W n cm Do Q`) as part of a
group put a fresh `cm` before the clip path on every move, so twenty
moves left twenty `cm` operators in front of one `re`. It never nested
deeper, but the content stream grew with every drag.

When a `cm` already sits right before that clip (one an earlier move
added, or the file's own), `translate_images` now replaces it with the
product of the new and old matrices. The CTM at the clip is
`c · prev · outer` either way (ISO 32000-1 8.3.4, 8.4.4). A previous
`cm` that is not finite, or a product larger than 1e12, keeps the old
behaviour and inserts a separate `cm`.

The new test moves an image cropped smaller than itself twenty times and
checks that the image and its visible crop both moved by the full offset,
that the operator count stays at its value after the first move, and that
the caption after it is untouched. It fails without this change
(32 operators against 13).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:47:08 -04:00
komo1iddin 7c8ca0bde8 M7: keep the text blocks cache after the document is edited (#2504)
The engine caches a page's text blocks for opening a paragraph and
applying its edit, but only while the document is unmodified: it was
keyed by the source identity, which edits keep. After the first edit the
cache never hit again, and the page was grouped again on every call (the
inline editor and the panel each call it after an apply).

pdfcraft_cos::Document::read_state() returns what the document reads as:
its source identity, its trailer and every changed object, compared as
allocations that the state keeps alive. A clone or a snapshot restored by
undo has the same state; any set, add, free or trailer change makes a
different one. The cache is keyed by it, so it hits after edits and never
returns another state's blocks.

On a page of 120 rows, text_blocks after an edit took 0.215 ms per call
and now 0.006 ms (release build, best of three; a throwaway timing test).

Tests: a read state matches a clone and not a document changed by set,
free or a trailer change, nor another document; after an edit the engine
groups the page once and undo and redo get their own blocks. The engine
test fails without this change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 03:46:56 -04:00
Marco Arnulfo 946c6fdfc4 Comments: an attachment comment's menu saves or opens its file (fixes #2565) (#2585) 2026-10-18 03:46:10 -04:00
Marco Arnulfo cef2255da5 Edit text: a loaded font is applied to the paragraph being edited (part of #2591) (#2599) 2026-10-18 03:45:58 -04:00
mtlcl410 2c4b84f6af M8: redaction removes the replies of the annotations it removes (#2659) (#2697)
* M8: redaction removes the replies of the annotations it removes (#2659)

A reply (/IRT) placed away from the mark survived, and its /IRT kept the
removed annotation - its comment text and the QuadPoints of the redacted
words - in the saved file. Replies to a removed annotation, and replies to
those, now go with it.

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

* M8: test replies listed before their parent, with a pop-up on a reply (#2659)

Review of #2697: the replies precede the annotation they answer in
/Annots, deepest first, and a reply has a pop-up. A single pass in
/Annots order would keep them; the test fails with the loop cut to one
pass, which the first test does not notice.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:45:46 -04:00
NetterSilver 5377a223e9 Extract RTL lines with numbers and punctuation in logical order (#2736)
Fixes #2667

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:14:51 -04:00
mtlcl410 e827a2a17c M6: a box drawn with points along its sides is detected as a box (#2942) (#2947)
Field detection read a path as a rectangle only when it had exactly four
corners, and gave up on a subpath once it reached five points. The French
income tax form 2042 draws its letter cells with a midpoint on each
vertical side (six points), so most of its cells were not found.

A subpath now keeps up to 16 points, and a point lying on a straight side
between its neighbours is dropped before the four-corner test. A point
that turns back along a side is not between its neighbours and stays, so
a path that doubles back is still not a box.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:14:36 -04:00
NetterSilver 43f8ddf264 Apply JPEG EXIF orientation when creating PDFs from images (#2951)
Fixes #2944

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:14:25 -04:00
NetterSilver 17ddc8707d Fix overflow panic for i64::MIN index in content_update/delete (#2979)
Found while testing crates/automation/src/content.rs:141

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 03:14:13 -04:00
Kurtulus Gemici 70d7df548e fix(i18n): the web build's Auto language follows the browser (#2954) (#2998)
On wasm, detect_system_lang() always returned English, so Auto showed
English whatever the browser's language. It now reads
navigator.languages in order, then navigator.language, and takes the
first tag with a catalog, as the desktop builds do with the system
languages and as VectorCraft and FilmCraft do in their browser builds.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:14:01 -04:00
Guillem Hermida 233767d181 fix(ui): the Hand tool moves the page with the pointer (#924) (#3017)
The Hand tool scrolled by the page widget's drag delta from inside the
page view's ScrollArea. egui applies that delta when the area ends, after
the frame's pages are laid out and drawn, so the page showed each move a
frame late and trailed behind the pointer. (Until the page view stopped
animating its scrolls, the delta was also eased in over several frames,
which is the stutter #924 and #1992 describe.) On top of that, the points
the pointer moved before egui tells a drag from a click were dropped, so
the page never caught up with the grab point.

Pan before the area lays the pages out instead: when the page widget is
being dragged with the Hand tool, set the scroll offset from the last
offset minus this frame's pointer delta, or minus everything moved since
the press on the frame the drag is recognised. The page now moves point
for point with the pointer in the frame it moves, and still stops where
it is let go (no fling). Other scrolls (go to page, links, zoom anchors,
compare sync) are untouched and still win when asked for in the same
frame.
2026-10-18 03:13:49 -04:00
skylar taylor-barrick 7cce65917d Control channel: don't lose the refusal reply to a TCP reset (fixes deadline test flake) (#3165)
* Control channel: don't lose the refusal reply to a TCP reset

When the control channel refused an unauthenticated connection (deadline
passed, overlong line, bad first request) it wrote the error line and
closed the socket at once. If the client had sent input the server had
not read, Linux answers that close with a reset instead of a FIN, and the
reset can discard the reply before the client reads it. That is the
flake in an_unauthenticated_connection_is_closed_at_the_deadline: the
dripping client's bytes were still unread when the server closed.

Now the server shuts down its write side after the reply and reads and
discards input until the client closes or a 1 s linger passes, and only
then closes. The new test a_client_refused_with_input_unread_gets_the_reason
pins it deterministically.

* Control channel: refused connections linger outside the auth places

A connection refused before auth used to keep its place among
max_unauthenticated while it lingered (up to 1 s), so refusals could
keep real clients out. Now `refuse` gives that place back and takes one
of a separate, smaller set of lingering places (Limits::max_lingering,
8) before the reply is written. When none is free the connection is
closed right after the FIN instead of lingering, so threads stay
bounded as #1806 intended.

Tests: a_lingering_connection_holds_no_place_to_authenticate and
only_so_many_refused_connections_linger.

* Control channel tests: a 5 s linger for the two linger tests

Their timing checks (the auth round trips finishing within the linger,
and a non-lingering connection failing a write within half of it) had
only 1 s and 500 ms of margin with TEST_LIMITS. Each now serves with its
own Limits and a 5 s linger; the assertions are unchanged and neither
test waits for the linger on its passing path.
2026-10-18 03:13:07 -04:00
mtlcl410 15af2860f1 fix(ocr): the text layer restores the rendering mode it sets (#3029) (#3033)
text_layer wrote '/OCR BMC BT 3 Tr /PCHelv 1 Tf ... ET EMC'. Tr and the
font are graphics state, and ET doesn't reset them, so every content
stream after the layer on the page inherited 3 Tr: a header or footer
added after OCR was in the file but drew invisibly, in PdfCraft and in
MuPDF. The layer is now wrapped in q ... Q inside its marked content.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:12:37 -04:00
mtlcl410 4ff53a8d8e fix(engine): printing keeps layers hidden in the Layers panel hidden (#3046) (#3050)
The print-ready PDF copied the document's /OCProperties as they are, so it
printed the layers in their default state: a layer hidden in the Layers
panel disappeared on screen and came back on paper. Print as image already
used the view's layer choices (it renders with the document's config).

Once the layers have been changed, the copy imposed for printing now has
its default configuration (/D) set to them: /BaseState /ON, /ON the
visible layers, /OFF the hidden ones, its other entries kept. With the
layers untouched, the document's own configuration prints as before.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:12:26 -04:00
ogzhnklvz 13c86b40e8 i18n: Turkish section titles and in-sentence labels keep İ and ı (#3114)
Section titles are drawn upper case through to_uppercase(), and tool names
inside sentences lowercased through to_lowercase(). Rust's case mapping knows
no language, so in Turkish "Gelişmiş" became "GELIŞMIŞ" and "İmza" became
"i̇mza" (i plus a combining dot). Turkish maps i ↔ İ and ı ↔ I.

upper_label() translates and upper-cases a title with the Turkish rule when
the label has a Turkish row (an untranslated label is still English and keeps
the English rule); in_sentence() lowercases with it. Other languages are
unchanged. The test failed before the fix ("GELIŞMIŞ").
2026-10-18 03:12:05 -04:00
PerchenkoAnton f8d7eb1352 M3: prevent oversized page textures after replacement (#3149) (#3182) 2026-10-18 03:11:54 -04:00
Jules Kreuer d8b2705825 Clear replacement text (#1350)
* M8: Clear marked-content replacement text

* M8: Move roadmap note

* M8: Keep inherited resource guard clean under Rust 1.96

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
2026-10-17 22:10:22 -06:00
Guillem Hermida 12fcc64115 Tests: make timing-dependent render and UI tests deterministic (#2812)
Seven tests failed at random on slow or busy machines, main included
(FreeBSD run 38834256439 at 5fdd820b). They now count the work or wait
for the state they check; no assertion is loosened and nothing retries.

- spot_colour_images_run_their_tint_transform_once_per_sample_value
  counts tint-transform runs (exactly 256, not one per pixel) through
  vendored hayro-interpret patch (34), color::tint_transform_runs, a
  thread-local counter, instead of a 10 s deadline.
- shadings_with_large_sampled_functions_paint_quickly: no counter tells
  the flat table from the hash map (both read every sample per build),
  so the build each paint does is timed against reading its samples on
  the same thread, fastest of 8 rounds each, and must stay under 10x
  (flat table 1.1x, hash map 109x), instead of a 10 s deadline.
- page_contents_and_nested_forms_share_one_budget: the budget is
  checked by its warnings; the deadline is only a hang guard, now 600 s.
- The pen-stroke tests hold finished renders back from the view
  (DocView::hold_rasters, for tests) while they look at the settling
  stroke, then wait by state for its raster.
- files_and_quit_from_the_operating_system waits for the tab before
  deleting the file the open worker reads.
- image_pixels_follow_the_frame_while_a_corner_is_dragged waits for the
  page raster on screen before each drag.

Product fix (#1858): a released drawing stayed painted only until any
raster of its page arrived. In full screen its edit is applied a frame
later, so a raster already under way could end the preview first. A
settling drawing is now marked committed when its AddAnnotation lands,
only rasters after that end its preview, and a failed edit drops it.
2026-10-17 20:53:39 -06:00
Brandon Thomas 645016addc Release 0.9.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.9.0
2026-10-17 21:43:00 -04:00
skylar taylor-barrick 3f5a784c2c Merge pull request #3102 from skylartaylor/fix/content-boundaries-and-mark-isolation
Read stream-boundary comments as the renderer does, and isolate new marks from appended graphics state
2026-10-17 18:03:38 -06:00
skylar taylor-barrick 4f6d16380a Merge pull request #2267 from MMexicann/fix/flate-fallback-output-limit
fix(filters): preserve output-limit errors in raw Flate fallback
2026-10-17 17:11:26 -06:00
skylar taylor-barrick 9b4af15b21 fix(edit): read marked content that spans content streams in one pass (#3081)
reading_lines grouped the joined operators by the stream their first token
is in and walked each group on its own. A marked-content sequence opened in
one stream (its /ActualText dictionary, or the whole BDC) and closed in the
next lost its replacement text: the export read "Glyphs" where every reader
of the joined streams reads "Replacement text".

The page's operators are now walked once. Each shown text still counts as
the stream its operator starts in, and text from another stream still starts
a new line, as before.
2026-10-18 00:27:50 +02:00
skylar taylor-barrick 7b39552ff1 Merge remote-tracking branch 'origin/main' into pr2267
# Conflicts:
#	ROADMAP.md
#	docs/roadmap.md
2026-10-17 15:58:13 -06:00
skylar taylor-barrick 0ac9b8403b Merge pull request #1917 from MMexicann/fix/xfdf-preserve-attribute-whitespace
M6: preserve whitespace in XML and XFDF attributes
2026-10-17 15:45:36 -06:00
skylar taylor-barrick 046ca7b947 Merge remote-tracking branch 'origin/main' into pr1917
# Conflicts:
#	ROADMAP.md
#	crates/xfdf/README.md
#	docs/roadmap.md
#	parity/acrobat-features.toml
2026-10-17 14:38:13 -06:00
skylar taylor-barrick 6d087c2d19 Merge remote-tracking branch 'origin/main' into pr2267
# Conflicts:
#	ROADMAP.md
#	docs/roadmap.md
2026-10-17 14:37:12 -06:00
skylar taylor-barrick be0254d708 Merge pull request #3091 from skylartaylor/deny-zbus-advisories
deny: acknowledge the new zbus unmaintained advisories
2026-10-17 14:26:40 -06:00
skylar taylor-barrick 54a45422c9 Merge pull request #2686 from MMexicann/fix/xml-form-text-nodes
M6: preserve complete plain XML form values
2026-10-17 14:19:05 -06:00
skylar taylor-barrick 43bb98eb88 fix(edit): isolate new marks from state left by content after an earlier wrapper (#3084)
place_tagged wrapped the page's content in q/Q only when no Wrap stream
existed yet. Content appended after an earlier wrapper (by another tool, or
an older OCR layer) that left a cm, a clip or text state set then moved,
hid or distorted every later header, Bates number, watermark, added text,
flattened annotation and redaction cover.

Placing a mark on top now reads the page's actual operators instead of the
/PCMark tags. When the content parses cleanly with balanced q/Q and closed
text objects and paths, the stretch from the first stream that leaves state
set is wrapped, once, at a stream boundary no comment runs into. Content
that is already self-contained gets no new wrapper, so repeated marks don't
pile up.

Content that can't be read that precisely (stray bytes, unbalanced q/Q, an
unfinished text object or path, too large) is wrapped whole as before,
rather than refusing: redaction stamps its covers after removing the text
and can't back out. A q that content leaves open is closed after it, where
closing it changes nothing already drawn.

The wrapper's close now starts with a newline, so a comment at the end of
the content before it can't swallow the Q. Removing marks strips only
wrapper pairs that end the list, keeping one that later content still
relies on, and recognises the older close bytes too.
2026-10-17 21:50:58 +02:00
skylar taylor-barrick 392406eb4a fix(content): read a comment at the end of a content stream as the renderer does (#3081)
Pieces::join separated a page's content streams with a newline, which ended
a comment left open at the end of one stream. hayro, MuPDF and Poppler join
them with a space, so the comment runs on into the next stream and hides its
first line. Edit text, object_list, the redaction audit, redaction and the
Office/HTML export therefore saw text and boxes that nothing draws, and
redacting unrelated text on the page made the hidden content visible: the
rewritten stream dropped the comment.

- Pieces::join uses a space, so every reader of the joined streams agrees
  with rendering.
- The parser records, per piece, whether a top-level comment runs into it
  and out of it (boundaries only, not every comment). A redaction rewrite
  of a piece ends an incoming comment before its first operator and leaves
  an outgoing one open, so hidden content stays hidden.
- Reading for export parses the streams joined, instead of one by one.
- The redaction tag check compares the joined streams the same way.
- Pieces::piece_of uses a binary search.
2026-10-17 21:50:58 +02:00
skylar taylor-barrick cee48385b7 deny: acknowledge the new zbus unmaintained advisories
RUSTSEC-2026-0370/0371/0372 were published today for zvariant, zbus_names and
zvariant_derive. All three are unmaintained notices rather than vulnerabilities,
and none has a fixed release to move to. They reach the tree as the D-Bus stack
behind accesskit_unix -> atspi (Linux accessibility) and as a dev-dependency of
pdfcraft-platform, so nothing ships in the other targets' binaries.

This is why main went red today; no dependency version changed.
2026-10-17 12:57:57 -06:00