Commit Graph
120 Commits
Author SHA1 Message Date
aiongg 157cbf3a9d refactor(edit): split text.rs into text/
New modules: editor, interp, blocks, lines, rewrite, fallback. The regions and session modules stay #[path]-declared.
2026-10-26 22:18:50 +00:00
aiongg 692f34fce2 Merge track r3-t04 2026-10-24 06:40:21 +00:00
aiongg 8dda3d50b9 Merge PR #4316: M7: a bold paragraph's stand-in font no longer turns earlier edits bold (#4280) 2026-10-24 06:22:51 +00:00
aiongg 5cb74eeb69 Merge PR #4217: M7: edited text keeps every space typed (#4199, #3797)
crates/edit/src/tests.rs: both sides appended tests at the end; kept both.
Cargo.lock: unchanged from base (the PR's ctutils bump did not apply).
2026-10-24 06:22:45 +00:00
aiongg b9f5ef6662 Merge PR #4198: fix(audit): covered_text reports the words under the cover, not the whole line 2026-10-24 06:22:30 +00:00
aiongg 7c005aed5b Merge PR #3551: Edit text: keep Tr, stroke, line width and gs set inside BT when rewriting a paragraph (#3508, #3509) 2026-10-24 06:21:29 +00:00
aiongg fa3a0221d9 Merge PR #3000: fix(edit): batch styled paragraph fallback glyphs 2026-10-24 06:21:12 +00:00
Lcoding15 0b3b243e9a M7: a bold paragraph's stand-in font no longer turns earlier edits bold (#4280)
Edit text set a paragraph whose font lacks a typed character (an accent
missing from a subset) in a standard font, always under the resource
name PCEdHelv, whatever face it stood for. Rewriting a bold paragraph
after a regular one replaced PCEdHelv with Times-Bold, so every
paragraph set in the earlier stand-in turned bold, and the editor's
draft measurement saw those paragraphs change and grew its frame over
them.

The stand-in is now named after its face (PCEdTimesBold, ...), as the
line and region rewrites already name theirs.

Fixes #4280
2026-10-23 22:46:56 +02: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
Jayesh-Prajapati9 3e9988954a M7: CR and CRLF break rows on the styled rewrite path too (#4199)
A multi-line or multi-style draft is laid out by rewrite_styled_block, which broke rows only at \n: a CRLF left a stray CR before the break and a lone CR broke nothing. The CR of a CRLF is now skipped and a lone CR breaks the row, keeping the draft's character positions, and a draft whose only break is a lone CR takes the same path as one with \n.
2026-10-23 14:15:17 +05:30
luis.pereiraandClaude Opus 5.5 60985e119c Edit text: a dash or line cap partway through an outlined line starts a run (#3508)
Review follow-up for #3551: two ExtGStates at the same opacity are the same
Paint, so a dashed outline after a solid one joined its line and was
rewritten with the first run's selections. Ts tracks the effective dash
pattern and line cap, from d and J and from an ExtGState's /D and /LC, and
same_paint compares them as values for stroked text, so states with the
same settings under other names still continue a line. A dash array longer
than 32 entries, or with an operand that isn't a finite number, and a cap
outside 0-2 are ignored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 09:35:03 +01:00
luis.pereiraandClaude Opus 5.5 9163f29090 Edit text: per-run q/Q, /LW from gs, and a Tr partway through a line starts a run (#3508, #3509)
Review follow-up for #3551, rebased on the gs grouping of #3310:
- The styled rewrite draws each run in its own q/Q from the state where
  the rewrite is drawn: a gs has no inverse, so one run's ExtGState stayed
  in force for the runs after it. Two ExtGStates at the same opacity share
  a paragraph (Paint), but either may set what Paint doesn't compare.
- The gs arm applies an ExtGState's /LW to the line width; paint_ops
  restates the effective width after any replayed gs.
- A run whose visible Tr (or, for stroked text, stroke colour and width)
  differs no longer joins the line before it, so a line is not rewritten
  in its first run's state. Clipping and invisible modes still join, as
  the #3447 refusal reads the whole line; gs changes are already kept
  apart by Paint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 09:27:47 +01:00
cadelassandClaude Opus 5.5 6d767cc6fc Edit text: keep Tr, stroke, line width and gs set inside BT when rewriting a paragraph (#3508, #3509)
A rewritten paragraph is drawn before its text object and restated only the
font, spacing and fill. Anything the text object set before its first show was
lost: `1 Tr 1 0 0 RG 2 w` inside BT turned red outline text into filled black
text, and `/GS1 gs` inside BT made semi-transparent text opaque, even when the
text was unchanged.

The interpreter now tracks stroke colour, line width and the ExtGStates
selected (bounded to 32 distinct names), and the rewrite restates whatever
differs from the state in force where it is drawn. Styled runs restate it per
run, too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 09:27:46 +01:00
Jayesh-Prajapati9 d9115bb90b M7: edited text keeps every space typed (#4199, #3797)
Rewriting a paragraph in one style split its text at white space and
joined the words with one space, so a run of spaces, an indent or a
trailing space typed in the editor was saved as one space or none. The
text is now kept as typed (other white space becomes a space, and CR or
CRLF become line breaks), and the wrapper keeps each run of spaces,
dropping only the spaces where a row wraps.

A justified (stretched) row still drops its indent and the spaces after
its last word, as before, so word spacing reaches only the spaces
between its words; keeping an indent on a justified row is a possible
follow-up. Centred and right-aligned rows, and the editor's draft box,
are measured to the last word, and a paragraph at 0 Tz is not justified.

Fixes #4199
Fixes #3797
2026-10-23 13:50:17 +05:30
Gabriel Luís da Silva cd2b26d492 fix(audit): join covered runs the way the line joins them
TextLine::runs also says whether joining put a space before each run,
and covered_words uses it: a word shown in two runs with nothing
between them (`SE` `CRET`) is reported as one word, not `SE CRET`.
Runs with uncovered runs left out between them are still spaced.
The regression test writes its content streams on one line each.

Review on #4198 (CodeRabbit).
2026-10-23 03:53:31 -03:00
Gabriel Luís da Silva e7dd758d33 fix(audit): covered_text reports the words under the cover, not the whole line
A text line joins runs painted on one baseline, and the audit reported
the line's whole text for a cover over part of it, including words
painted beside the cover that stay visible (`SECRETtail` for a cover
over `SECRET`).

TextLine now keeps each joined run's text with the end of its word
boxes (TextLine::runs), and the audit reports the runs whose words
the cover overlaps. A single run, a line reordered for reading
(right-to-left or visual-order Indic), or a cover that overlaps the
line's box but none of its words still reports the whole line.

Fixes #4164
2026-10-23 03:14:49 -03:00
Dylan M. Taylor 7345e5f5de Merge main into fix/coderabbit-a1-audit
Keep this branch's budgeted audit() and read lines with main's
glyph_text_lines, so /ActualText cannot hide text under a cover.
2026-10-22 22:20:45 -04:00
Dylan M. Taylor 684226713a Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	crates/audit/src/tests.rs
2026-10-22 21:42:41 -04:00
aiongg 82721ed44c Merge #4114 (fix/coderabbit-b-shared): over-budget page content reads as an error 2026-10-22 21:41:53 +00:00
aiongg 4fc56fd303 Reading blocks documents that over-budget page content is an error and truncation covers forms 2026-10-22 21:29:04 +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 b979d4d9c2 fix(audit): audit the glyphs on each page held, not their ActualText
The redaction audit read each page's text through text_lines, which walked
the whole page tree again for every page (quadratic in the page count) and
let /ActualText replace the glyphs, so an empty or substitute ActualText hid
covered text from the audit. Edit gains glyph_text_lines, which reads the
page dictionary it is given and ignores ActualText; the audit uses it.
2026-10-22 21:17:15 +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 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
PerchAnton d323e86706 M7 PR #3000: retain batched styled fallbacks with current main 2026-10-22 20:11:21 +03:00
aiongg c88831381e fix(edit): editor font enumeration shares one decode budget
text_editor_fonts counted only the programs it kept against its 32 MiB
budget, so up to 128 aliases of one unusable program were each inflated
in full. Every decode now spends a shared 64 MiB allowance.
2026-10-22 16:57:55 +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 3e306c22ef fix(audit): compare a cover with the start of a merged text line
A text line joins compatible runs on one baseline, and the audit took
the line's last painting operator as its paint position. Text drawn
after a cover on the same baseline joined the covered words and moved
the line past the cover, so the covered words went unreported. The
audit now compares against the line's first painting operator: any
run painted before the cover is checked, and runs painted after it
in the same low-contrast color are unreadable on it too.
2026-10-22 16:44:38 +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 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 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 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 a456f516d9 Merge PR #2756: M7: a paragraph number keeps naming the paragraph text_paragraphs listed (#2516)
ROADMAP.md: kept both sides' progress-log lines.
crates/edit/src/lib.rs: re-export list is the union (TextSession from #3942; BlockKey, block_overlapping from #2756).
2026-10-22 01:47:18 +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 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
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
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
mtlcl410andClaude Opus 5.5 d85d49a4c6 M10: Export to Word keeps the page background colour (#2992)
A page coloured with Edit a PDF > Background exported to Word as a
white page: export pages carried text and images only, and the Word
section has no page colour.

- pdfcraft-edit `page_background`: the colour of the first thing a
  page paints, when that is an opaque fill (no soft mask, Normal blend)
  in a plain gray, RGB or CMYK colour, of a path with a rectangle
  covering the visible page, not clipped smaller; `None` otherwise.
- `export::Page::background` carries it; the Word writer sets it as
  the document background (`w:background`, plus the
  `w:displayBackgroundShape` setting Word needs to show it) when every
  page shares one non-white colour. Word has one background per
  document, so differing colours are left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
2026-10-20 16:55:20 +08:00
hcchung1 3909bebeec M7: a paragraph number keeps naming the paragraph text_paragraphs listed (#2516)
Paragraph numbers are positions in a list regrouped from geometry after
every edit, so an edit that merged a paragraph into its neighbour made the
next edit by number rewrite another paragraph and lose its text.

After text_paragraphs, text_edit and text_edit_layout find the listed
paragraph again by its text and box, and refuse one that no longer exists on
its own (list the page again). An edit's result names the numbers that are
gone in stale_paragraphs. The inline editor's switch-paragraph click holds
the same key instead of an index. Without a listing a number is a position,
as before. An identity that survives save and reopen is still #2508.
2026-10-20 10:02:06 +08:00
MichaelandClaude Sonnet 5 1ce4965c27 fix: read the last array of a surplus-operand TJ, not the first (#3569)
`[(FIRSTARR)] [(LASTARR)] TJ` is malformed (TJ takes one array
operand), but the operator still shows the one immediately before it,
as the renderer, MuPDF and pdftotext agree. Three readers here counted
operands from the front instead and showed the discarded first array:

- crates/edit/src/text.rs (text_lines/text_paragraphs, and the Office
  export through reading_lines)
- crates/redact/src/interp.rs (search-based redaction's content
  interpreter)
- crates/a11y/src/headings.rs (autotagged heading detection)

So the editor could offer a word the page never draws, a redaction
search could miss it, and a heading could read as text nobody sees.
Take the last array for TJ in all three, matching the `_ =>
op.operands.last()` arm already used for Tj/'/" alongside it.

Added a regression test per crate (`a_tj_with_a_surplus_array_reads_
the_last_one`, `a_tj_surplus_arrays_undrawn_glyphs_are_not_seen_
under_a_mark`, `a_heading_with_a_tj_surplus_array_reads_the_one_the_
page_draws`), confirmed each fails without its fix and passes with it.
cargo test -p pdfcraft-edit (196 passed) -p pdfcraft-redact (81
passed) -p pdfcraft-a11y (44 passed), fmt, clippy -D warnings, xtask
layers and xtask assets all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-20 08:09:59 +08:00