* 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.
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>
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.
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.
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.
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.
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.
#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.
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.
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.
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
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>
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>
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.
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>
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>
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>
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.
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.
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"].
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>
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
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