Redaction reads page content and form XObjects strictly (upstream) and
within one page's decode budget (this branch): both now go through
DecodeBudget::decode_strict.
Both checks for text left under applied redaction marks (redaction's own
verify pass and the engine's renderer cross-check) skipped any page
content stream whose dictionary said /PCMark /Redaction, so that the
overlay's label would not count. An input file could put that label on
its own content stream and keep it out of both checks. Applying
redactions now records the overlay streams it adds (stamp returns the
new stream), and only those are skipped.
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.
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: 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).
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
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
On a page with no content of its own, an on-top watermark is the first
stream in /Contents, so it was counted with the behind-content marks and
a watermark added behind later was drawn over it. Nothing records
whether a watermark is behind, so a new behind mark now goes right after
the last background, which is always below any on-top watermark. A newer
behind watermark goes under older ones again, as before #3591.
Marks behind the page content were all inserted at the start of
/Contents, so a second background went under the first and the old
colour stayed. Backgrounds now come first, then behind-content
watermarks, each in the order added. A watermark added after a
background no longer disappears under it.
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.