Commit Graph
161 Commits
Author SHA1 Message Date
Ridwan 9b5624afc1 fix: Issue #4017. Flattening removes empty fields it cannot draw (#4033)
* M6: flattening removes empty fields it cannot draw (#4017)

Flattening kept every widget without an appearance so that its value
was not lost with nothing drawn in its place (#1794). A field without a
value has nothing to lose, yet it stayed in /AcroForm /Fields and on the
page, so a flattened file could still be filled in.

A widget without an appearance now stays only when its field has a
value: its /V, or the nearest one inherited through /Parent. A missing
value, an empty string or an empty array is no value. Comments and
valued widgets are unchanged.

Fixes #4017

* M6: flattening keeps a widget whose value is past the parent limit (#4017)

has_value walks at most 64 levels up /Parent. When it ran out it said
"no value", so a widget whose /V sat on the 64th ancestor was removed.
Running out now counts as a value: one that wasn't checked must not be
removed.
2026-10-23 10:42:08 -04:00
Matej VrzalaandBrandon Thomas 75207dc8ed fix(edit): setting a paragraph's width no longer panics on text larger than a page (#4110)
rewrite_block clamps a requested paragraph width between the text's
size in user space (size * k) and the largest page PDF allows
(14 400 pt). f64::clamp panics when its minimum is the larger, so text
set above 14 400 pt crashed the app as soon as its paragraph was given
a width. The size comes from the file (`20000 Tf`, or `1 Tf` scaled up
by `Tm`) or from the request (the automation tools pass `size`
through).

The lower bound is now capped at 14 400 pt too, so such a paragraph
gets the widest width allowed. `f64::min` also turns a NaN size into
the cap.

Test: a 20000 pt Tf, a 1 pt Tf scaled 20000x by Tm, and a requested
size of 20000 pt each take a width of 200 without panicking (all three
panicked before).

Reported-by: BS.ehlas.cz

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:20:35 -04:00
aiongg ba024eb0dd fix(edit): flatten on a copy so a page refused part way changes nothing 2026-10-23 00:02:33 +00:00
aiongg 7a466ff5fc fix(edit): prune a field again when a shorter path reaches it past the depth cutoff 2026-10-22 23:32:11 +00:00
aiongg a52e39a5e2 Merge #4114 (fix/coderabbit-b-shared): shared DecodeBudget
The tests each branch appends to the audit, edit and organize test files are
kept side by side; prune.rs merges cleanly, with the form memo on top of the
budgeted page content.
2026-10-22 21:17:55 +00:00
aiongg fdcfe16b7d fix(edit): bound the text one page's extraction can expand into
Text extraction copies a ToUnicode destination for every glyph showing its
code, and the page's total text was not bounded, so a small page could expand
into gigabytes. One page's extraction (its forms included) stops with an error
past 16 MiB of text.
2026-10-22 21:17:15 +00:00
aiongg dc7a195dda fix(edit): prune each form-field node once after flattening
Removing flattened widgets from the field tree recursed into every /Kids
entry with only a depth limit, so a node listing its child twice per level
(or itself) made the walk exponential. Each node's answer is now remembered;
a node met again while it is still being visited survives, as it did when
the depth limit stopped a cycle.
2026-10-22 21:16:21 +00:00
aiongg f1a89a4842 fix(edit): wrap added text once per write, not once per line
Writing an added text item re-wrapped the whole text inside the loop over its
lines to learn the line count, so multiline text cost time and temporary
memory quadratic in its length. The wrapped lines are computed once.
2026-10-22 21:16:21 +00:00
aiongg f980b34ef3 Text and image reading charge a page's content and its forms to one decode budget 2026-10-22 19:49:44 +00:00
aiongg a37a908f7b fix(edit): page content, forms and image records share per-page limits
The editor's page readers decoded every /Contents entry and every form
entry with a fresh 1 GiB allowance, and the reading-order image
inventory added a record per image draw in every form entered, so a few
shared forms drawing each other produced millions of records. Content
streams and form entries are decoded against one per-page DecodeBudget
(content past it is an error, so no edit rewrites a page from part of
its content; forms past it are reported truncated), and an inventory
past 100,000 image draws is refused.
2026-10-22 16:57:55 +00:00
aiongg 55e32759b1 Integration: subset fallback test names the face the build leads with (#2609 × #1760)
#2609 put the Japanese faces ahead of the bundled Inter face and rewrote
the #1760 test to expect Inter, which holds only without craft-fonts.
With CRAFT_FONTS_DIR, BIZ UDPGothic has ő and draws it. The test now
expects the Japanese sans face when the build has one, Inter otherwise.
2026-10-22 04:15:17 +00:00
aiongg 2c13fd9373 Merge r2 track t16: mixed: automation-mcp, crate:misc, crypt-security, export-convert, optimize, preflight-pdfa, save-io
# Conflicts:
#	ATTRIBUTION.toml
#	contributors/people.toml
#	crates/automation/src/comments.rs
#	crates/edit/src/tests.rs
#	crates/engine/src/tests.rs
#	parity/acrobat-features.toml
2026-10-22 02:23:58 +00:00
aiongg dcf2802f96 Merge r2 track t15: mixed: accessibility-tags, dialogs-prefs, javascript, organize-pages
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/engine/src/tests.rs
2026-10-22 02:23:22 +00:00
aiongg 3cf35888d7 Merge r2 track t12: i18n, i18n: new language catalogues
# Conflicts:
#	ATTRIBUTION.toml
2026-10-22 02:21:42 +00:00
aiongg 15f558aaa8 Merge r2 track t11: redaction, mixed: combine, compare, crate:edit, measure, ocr
# Conflicts:
#	ATTRIBUTION.toml
#	crates/automation/src/tools.rs
#	crates/edit/src/lib.rs
#	crates/ui-egui/tests/compare.rs
#	crates/ui-egui/tests/ui_group/marks.rs
2026-10-22 02:21:38 +00:00
aiongg 69e755c439 Merge r2 track t08: fonts
# Conflicts:
#	crates/automation/src/tools.rs
#	vendor/README.md
2026-10-22 02:20:50 +00:00
aiongg 4d5025c6da Merge r2 track t07: text-extract-select
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/render/src/raster.rs
#	vendor/README.md
2026-10-22 02:20:03 +00:00
aiongg a2c46a9298 Merge r2 track t06: annotations-comments
# Conflicts:
#	crates/automation/tests/automation.rs
#	crates/render/src/raster.rs
2026-10-22 02:19:23 +00:00
aiongg 7bf122eea8 Merge r2 track t02: edit-content
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/edit/src/text.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/edit_text_ui.rs
2026-10-22 02:17:34 +00:00
aiongg 456b96d06b Merge PR #2454: M5: year-first slash dates (yyyy/mm/dd, yyyy/m/d) for Fill & Sign and headers 2026-10-22 01:49:17 +00:00
aiongg 1544fae727 Merge PR #2481: M7: show a shaped run in one text object instead of one per glyph 2026-10-22 01:48:40 +00:00
aiongg 3378e660aa Merge PR #2612: M7: let a list marker open an item and a hanging indent continue it (#2564) 2026-10-22 01:48:38 +00:00
aiongg 41ec3f370e Merge PR #3289: Lay out vertical (Identity-V) text as columns in the edit layer
crates/edit/src/text.rs: interpret takes the page resources (#3310) with #3289's CachedFont cache and Result return; walk/enter return Result and use main's form budget; call sites pass resources and propagate with ?. TextLine and the text state keep both sides' fields (clips, gs; unreadable_mode, vertical, unreadable). The shown run keeps main's tracking: 0.0 with #3289's vertical advance and tm. replace_line checks ensure_editable before main's clipping-text refusal.
crates/edit/src/tests.rs: kept both sides' new tests.
2026-10-22 01:48:38 +00:00
aiongg f58254b57f Merge PR #3310: Keep text rows in other layers, gs states or Tz out of a paragraph
crates/edit/src/text.rs: interpret takes the page resources (#3310) and the named property lists for /ActualText (main's #3301); every call site passes both. In BDC handling, #3310's layer push runs before main's property-list lookup; carry.layers is saved before positioned_tracking.
crates/edit/src/tests.rs: kept both sides' new tests.
2026-10-22 01:47:54 +00:00
aiongg fa1095350a Merge PR #3697: Edit text: keep a line together across subsets of the same font (#3661) 2026-10-22 01:47:18 +00:00
aiongg 3f9b41246a Merge PR #1829: M10: keep dotted-leader contents entries together
ROADMAP.md: progress-log line added at the top
docs/roadmap.md: status line, M10 row note and revision row applied onto main's
parity/acrobat-features.toml: export-docx evidence and notes insertions applied onto main's
crates/edit/src/tests.rs, crates/engine/src/tests.rs: new tests appended after main's
2026-10-22 01:46:30 +00:00
aiongg f4f9e009ad Merge PR #3861: M7: keep a word on its line when its spacing differs by less than a thousandth of an em
crates/edit/src/tests.rs: kept both new tests (#3571's surplus-TJ test and this PR's spacing test).
2026-10-22 01:45:49 +00:00
aiongg 64e49eac48 Merge PR #2487: M7: drop the fallback fonts a text edit leaves unused 2026-10-22 01:45:38 +00:00
aiongg a0e8c2faa2 Merge PR #2609: fix(edit): draw the Cyrillic the Japanese faces lack with the bundled Inter face (#2593)
parity/acrobat-features.toml: edit.edit-text evidence and notes take both sides' additions
2026-10-22 01:45:35 +00:00
aiongg b8d5cdd603 Merge PR #3595: M7: paint a new background over older ones (#3591)
ROADMAP.md: progress-log lines from both sides, newest first.
2026-10-22 01:45:17 +00:00
aiongg da6c3adedc Merge PR #3471: fix(edit): keep a superscript footnote marker in its paragraph's row (#3252) 2026-10-22 01:45:10 +00:00
aiongg c4616b25f7 Merge PR #3621: The image scan skips a path segment before any m instead of panicking (#3581) 2026-10-22 01:45:09 +00:00
aiongg b6fdb80cef Merge PR #3756: feat(edit): Header & Footer can be drawn in Times or Courier (#3754) 2026-10-22 01:45:09 +00:00
aiongg 16af57acac Merge PR #3942: M7: a text editing session for one paragraph 2026-10-22 01:45:01 +00:00
aiongg 9d7ccc9cd2 Merge PR #2723: fix(edit): keep a one-row centred title centred when it grows (#2581) 2026-10-22 01:44:58 +00:00
aiongg d133dfbf75 Merge PR #1989: Edit images: rotate an image by any angle with a knob (#1940) 2026-10-22 01:44:58 +00:00
aiongg 23ad2d6b2b Merge PR #3245: Draw a note without an appearance as a fixed-size icon at the corner of its Rect (#3175) 2026-10-22 01:44:50 +00:00
komo1iddinandClaude Opus 5.5 6495c08afd M7: a text editing session for one paragraph
A TextSession opens a paragraph once: it keeps the document as it was
and the page's lines then. Each draft is laid out on a copy of that
document, so no draft sees another's output, and the commit writes into
the document it was opened on, refusing ("the paragraph changed while it
was being edited") when that document has changed since.

text_edit_layout and rewrite_block now open a session and lay out or
commit once, so there is one path and their output is unchanged. No
caches yet: those come next, each under the same equivalence test.

First step of #2512.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 02:15:47 +08:00
komo1iddinandClaude Opus 5.5 475f375ff4 M7: keep a word on its line when its spacing differs by less than a thousandth of an em
Joining runs into one line compared their character and word spacing to
1e-6. Word documents exported through macOS Quartz write 0.0001 Tc on
some words, so a line like "for company AEROSOL" split into "for
company A" and "EROSOL", two paragraphs that edit separately.

Spacing below a thousandth of an em, the unit glyph widths are given in,
can't be seen. Compare character and word spacing to that; scale and
rise still have to match exactly, and visible spacing still keeps runs
apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 22:41:17 +08:00
Gert Pellin 01149dea77 M7: header and footer can shrink the page content out of their way (#2140)
HeaderFooter::shrink scales the original page content evenly into the
band the header and footer leave free, centred across the page, in
display space so rotated pages shrink the way they are shown. The scale
is a content stream first inside the q/Q wrapper, tagged as part of the
header and footer, so Remove and Update take it away again and
backgrounds behind the page stay full size. Margins that leave no room
are refused before anything is drawn.
2026-10-21 09:02:09 +02:00
Lombardi2003andClaude Opus 5.5 14b3978344 fix(edit): a header reuses a font name only for its very own dictionary
From review of #3756: a /PCTimes naming Times-Roman with more to it (a
/ToUnicode map, /Widths) passed as the same face and was replaced, so
the page's own text could lose its map. A name is reused only when it
holds exactly the dictionary a header writes, and then it is left as it
is; anything else gets a free suffixed name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:56:53 +02:00
Lombardi2003andClaude Opus 5.5 4e8e17c7e6 fix(edit): a header never takes over a font name the page already draws with
From review of #3756: add_resources set /PCTimes (or /PCCour, /PCHelv)
in the page's fonts whatever was there, so a page that already drew
another font by that name would have its own text change face. Headers
and footers now pick the usual name when it is free or already names the
same standard face, otherwise the first free name_1, name_2…, and add
only that face, never replacing an entry. An update finds its own name
again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 01:13:00 +02:00
Lombardi2003andClaude Opus 5.5 fecfc774f2 feat(edit): Header & Footer can be drawn in Times or Courier (#3754)
Headers and footers were Helvetica only. HeaderFooter takes the same
standard-face Family as Add text: the content selects /PCHelv, /PCTimes
or /PCCour, the page gains that font, and centred and right-aligned text
is placed by the chosen face's standard-14 widths. doc_header_footer
takes font, and the dialog's fixed "Font: Helvetica" label becomes a
Helvetica / Times / Courier menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 00:48:48 +02:00
Aman Mehtar 1d76fcbc3b Edit text: tests for re-encoded subsets and five-letter prefixes (#3661)
From @mtlcl410's independent run of this PR:

- a second subset of the same font can be re-encoded with `/Differences`, so
  "-l" has different codes from the first subset's; the line still joins,
  reads in coding order, and editing it replaces every run, including the
  re-encoded subset's;
- `ABCDE+Helvetica-Bold` has a five-letter prefix, not the six-letter subset
  tag of ISO 32000-2 §9.6.4, so it stays a different font from
  `Helvetica-Bold` and the runs stay apart.

The first case (and its edit) fails before the subset fix; the second guards
the tag-length rule against stripping any `+` prefix.
2026-10-21 00:46:01 +05:30
Aman Mehtar 4a618e8dcb Edit text: do not group or join by an empty font name (#3661)
Review follow-up. `Metrics::from_dict` leaves `base_font` empty when a font
has no `/BaseFont`, and a bare subset tag such as `/ABCDEF+` left an empty
name after stripping. Two consequences:

- `same_style` split one paragraph into separate reading blocks for the
  exporters when both names were empty, though they grouped before subset
  joining;
- `/ABCDEF+` and `/GHIJKL+` compared equal after stripping, so unrelated
  malformed fonts could join into one line.

`same_base_font` now strips both tags first and requires a non-empty, equal
name, so a missing name or a bare tag matches nothing. `same_style` keeps
grouping lines whose fonts both lack a name, since the resource or fallback
face `same_font` already matched is their whole identity.

Tests: a page in a font without `/BaseFont` still reads as one block (it
split into two before this change), and a bare tag matches no other name.
2026-10-20 22:28:08 +05:30
Aman Mehtar 5eae8d6e90 Edit text: keep a line together across subsets of the same font (#3661)
Word and other writers split one font into several subsets, each a font
resource of its own with its own six-letter tag: "Two-line" can be drawn as
/F1 (Two) /F2 (-l) /F1 (ine) with F1 and F2 both Helvetica-Bold. `text_lines`
ended a line wherever the resource name changed, so Edit text showed three
lines and Word/HTML export wrote three paragraphs -- out of order when the
subsets' boxes differ by a fraction of a point.

A run now continues a line when its `/BaseFont` names the same font as the
line's, ignoring the subset tag (ISO 32000-2 §9.6.4), as well as when it is
the same resource or another subset of our Japanese/Hebrew fallback face.
Block grouping in `text_regions` uses the same comparison, so the exporters
see one block.

Tests: the issue's two-resource case and Word's two-subset case read as one
line and one block; two different fonts still split. Before the fix the first
gave ["Hel", "lo"] and the second ["ab", "ca", "b"].
2026-10-20 21:41:00 +05:30
NetterSilverandClaude Haiku 5.5 25e273a33d Pair /ActualText correctly past 256 levels of marked content
Fixes #3573

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:47:09 +02:00
Rahul Ranjan 631ca00bdd The image scan skips a path segment before any m instead of panicking (#3581)
A content stream that uses l, c, v, y or h before any m or re has no
subpath to extend. The edit-side image scan (crates/edit/src/images.rs)
handed the operators straight to BezPath's line_to/curve_to/close_path,
which panic on an empty path by contract, so object_list, page_images,
image_hit and the Office export's image walk all crashed on such a page,
while the renderer, text extraction, MuPDF and pdftotext skip the
malformed segment and render the rest.

The scan now skips the segment when the path has no elements, mirroring
the v arm's existing guard and kurbo's own condition (debug_assert on
!is_empty). A path started with m or re extends and closes exactly as
before. A clip set by a stray segment (W n on an empty path) takes a
zero bounding box, which kurbo defines as Rect::default, not a panic.

Regression test: each of the five segment operators before any m, then
a well-formed m l h and a re W n clip, all before an image Do - the
image is still listed in every case.

Verified against kurbo 0.13.1's source (line_to/curve_to/close_path
debug_assert on empty; bounding_box unwrap_or_default) and the issue's
repro; no Rust toolchain on this machine, so cargo test was not run
here - saying so plainly for the reviewer.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
2026-10-20 14:50:30 +05:30
mtlcl410andClaude Opus 5.5 d5002807be M10: a turned fill or clip covers the page only if its corners do (#2992)
Review on #2994: fill rectangles and the clip were kept as their page-space
bounding boxes, so a rectangle turned or sheared by the CTM could pass as
covering the page while leaving its corners out, and a following fill became
Word's page colour. Each rectangle is now kept as its four page-space
corners, the clip as the rectangles it intersects, and the crop box counts as
covered when its corners lie inside each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-20 16:55:20 +08:00
mtlcl410andClaude Opus 5.5 ca0e0b08f3 M10: a fill whose second rectangle cuts a hole is no page background (#2992)
Review follow-ups on #2994: under even-odd any second rectangle, under nonzero
one wound the other way, can leave a hole the Word page colour would cover.
The parity note says the page colour hasn't been opened in Word yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-10-20 16:55:20 +08:00