A rewrite also takes away copies of its words. With a copy earlier on the
page, the words after it moved up the list, so the next change rewrote the
word after the one it meant: deleting John removed Smith and left John.
Before each change, the words up to the next one that stays must still be
where they were. If they aren't, the edit falls back to the whole-phrase
rewrite.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backspacing over a word leaves the spaces either side of it, and the rule
that sent spacing to the whole-phrase rewrite took that for spacing someone
asked for, so the most ordinary deletion redrew the whole line again.
Spaces beside a change are nothing to draw: the words around it stay where
the scan has them, with the gap already between them. Only what has to be
drawn goes to the whole rewrite: spacing inside the new words, between two
words nothing else changed, or any whitespace other than spaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A draft with the same words (a space typed after the last) changes
nothing and is no undo step. It used to redraw the whole phrase.
- Rewriting words alone is refused when it would move any other text on
the page, visible text too, not only other OCR words.
- A cover stops at the words either side that stay: the padding of a
large word could reach over the ink of a small one beside it.
- Spacing other than one space between words goes to the whole-phrase
rewrite, which keeps it, instead of being dropped.
- A squeeze is relative to the scale the words already have.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deleting one word from a recognised line covered the whole line and
redrew it in Helvetica, though nothing but that word had changed.
A text edit of an OCR phrase now covers and redraws only the words it
changes; the rest stay as the scan shows them, still invisible text. Each
changed stretch is rewritten as a phrase of its own, on a copy of the
document, and the edit falls back to the whole-phrase rewrite when words
can't be told apart or a kept word would move. A deleted word leaves its
gap. A replacement a little too wide is squeezed across to fit, by up to
a quarter; wider than that it takes over the next word. A change of
formatting, or a typed line break, still redraws the whole phrase.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #2354. Thai typed into these tools is shaped and drawn from the Thai face through Add text's writer (now pdfcraft_fonts::shaped_text), searchable and copyable, instead of being written as ?????; a build without a Thai face refuses it with a message.
Refs #5989. Ctrl/Cmd+A selects every object on the page; copied objects paste as page content that draws the same, within a document or into another, with organize's bounded deep copy across documents. Headless: object_copy, object_paste, object_move all.
Brings the Thai word-breaking branch up to date with main. The only conflict was the edit.add-text parity entry: main's evidence and notes (print inks) are kept, with the new Thai test added and the Thai wrapping sentence updated.
A complete program of the source's family in another style, such as one
its descriptor marks bold, stopped the bundled Regular Thai outlines from
completing a Regular subset, and the text was then drawn in that bold
program. Count only a program in the source's style, as the subset
completion itself does.
The resource limit was checked before a form's resources were merged into
the page's, so one form could take the page past it and every later form
was then refused, even one whose resources were all the page's already.
Check the merged dictionary instead.
The first program of the face that could shape a span was chosen even when
its embedding permissions forbid embedding it, and the edit then failed
while a later program of the same face could have been used. Check that a
program may be embedded before choosing it.
A space that no subset of the face encodes was drawn as a quarter-em gap,
narrower than the text-space fallback the source font uses elsewhere.
Keep each subset's source space with it and use that for the gap.
Every resource name of a page's Thai subsets decoded and kept its own copy
of the font program, so many names of one large program could hold about
2 GiB for a single edit. Names of one font object now share its program,
the subsets of an edit are decoded within a shared 64 MiB allowance, and
an edit that needs more is refused with the reason, changing nothing.
Edit text queued Prepare grouped text for every page that came into view, after a full
preparation on a copy of the document on the UI thread to see whether it could. Scrolling cost
that work per page, marked the document changed, added undo steps and inlined shared forms
although nothing was edited.
Now nothing happens while pages are shown. A click (press and release, no drag) on grouped
artwork, routed to the picture by the page router as before, asks once whether that artwork
shows text preparation would open (text::forms::grouped_text_at, which reads the page without
copying or changing it). If it does, the page is prepared as one undoable step and the
paragraph at the point clicked opens for typing. A logo or a group preparation would refuse is
selected as before, with no message and no change. A drag still moves the selected group, and
while a paragraph is open or an edit is waiting the click selects as before. text_prepare is
unchanged.
The docs and the parity entry no longer say text is prepared on entering Edit text.
The repair reads the page's own fonts. It ran only after a group was expanded, so the same
damaged map on text that was never grouped kept reading wrong. Preparation now repairs it
whenever it finds it, recorded in the repair log as before, and PrepareText (text_prepare)
succeeds when it repaired a map even with nothing to expand. A repaired map no longer matches
the damage's fingerprint, so preparing again changes nothing.
Ungroup and text preparation now check the placement in one place, ungroup_ops, which is given
the page's operators and the Do's index:
- The form is looked up by the name the Do writes, byte for byte. Ungroup looked it up by the
picture's display name, a lossy UTF-8 copy, so a form named /F#FF was 'missing'.
- A Do with more than one operand is refused: the picture was read for its first, the renderer
draws its last.
- A Do inside BT…ET or an unfinished path is refused. Inline, the form's text objects would be
nested in the page's, and its clip would take in the page's path. Preparation already refused
these; Ungroup didn't.
A Q with no q before it restores nothing; the renderer ignores it. The check for translucency at
the Do took it as restoring the opaque default, so a transparency group drawn after /H gs Q was
inlined although it is drawn translucent.
A form with resources of its own and no /DefaultRGB (likewise Gray, CMYK) draws its device
colours as they are. Inline, the page's resources apply, and a /DefaultRGB there would remap
them (ISO 32000-2 §8.6.5.6). Only the form having an entry the page lacks was refused; now either
side having one the other doesn't share is.
text/forms/expand.rs was a drifted copy of ungroup.rs. It is gone: text preparation now opens each
placement with crate::ungroup, so Ungroup and preparation share one implementation.
What the copy added is now in ungroup.rs: a Refusal that says whether the form itself or only
this placement is the reason (intrinsic) and whether a rename alone was the problem (renaming),
the shows_text result, merges_without_renaming, refusals for unreadable /Resources and /Group
entries and arrays with a non-number, a cap on resource counts and q nesting, a total size for
pattern copies, and ending an unfinished path with the form. Ungroup's newer behaviour stays:
names are read from the operand the renderer uses, so a surplus operand is renamed correctly
rather than refused; default colour spaces are compared with the page's; and a group blending
in DeviceRGB on a page that names no blending space is drawn inline, as Ungroup does, rather
than refused as the copy did.
What a form draws is now read within one decode allowance shared with the form itself, so a
crafted group can no longer make one Ungroup decode 64 MiB for each of 1024 nested forms. A
group whose content can't be checked is refused; for a form without a group, blend modes inside
don't matter, and content that can't be checked only means preparation can't tell whether it
has text (shows_text is None), which it reports.
A text object can run on from one content stream into the next. When it is still open at a
stream damaged partway, or one that doesn't decode, the text that stream shows starts where the
text before it ends: editing that text moved what the hidden stream shows, its advance changed.
An edit in the text object before such a stream went ahead, as edits before the damage do.
`rewritten_contents` now refuses a change from the start of a text object still open at the
first stream not read whole (`changes_text_open_into_damage`); a change before that text object
goes on. `text_a_stream_not_read_whole_goes_on_from_is_not_edited` (the hidden stream damaged
from its start and one that doesn't decode, between the text and a stream closing it or last on
the page; a line in an earlier text object of the same stream is still edited).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Moving or resizing an image in a stream before one damaged partway, or one that doesn't decode,
goes on, as the edit changes no stream past it. But the image's `q cm … Q` can close past that
stream, and what the stream draws inside the scope is unknown. When the image looked alone there,
the move changed the scope's `cm` in place, and anything the stream draws in it moved too: a
square drawn there in the image's `cm` moved with the image, as on `main`.
A scope still open after such a stream now counts as one that paints something else
(`first_op_after_damage`, read by `image_scopes` and `enclosing_transform`). An operator counts
by its keyword there: a page may split its content between an operator's operands and its
keyword, so a `Q` whose operands come before the hidden stream still closes after it. The move
gives the `Do` a `cm` of its own and leaves the scope's as it was; with a clip on the image, the
clip stays, as one the stream may share. A crop goes around the `Do` alone rather than at the
scope's start, where it would clip what the stream draws. A crop PdfCraft put in such a scope
isn't removed, and the refusal says why rather than that there is no crop. Arrange is
unchanged: it takes out the scope it finds, which changes the stream past it and is refused.
`an_image_whose_scope_closes_past_a_stream_not_read_whole_moves_alone`: a stream that doesn't
decode and one damaged, each also with the square's operands before what is hidden (ending the
stream before, or in the damaged stream before its damage); the image with no clip, a clip, and a
PdfCraft crop; moved, cropped, and its crop removed. A scope that closes in a damaged stream
before its damage still takes the move in its `cm`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Review follow-ups:
- The phrase's median counts each word once: neighbouring words OCR
sized alike share a run, so 15, 9 and 9 pt gave 12 pt, not 9.
- The styled rewrite's width calibration is not clamped again after the
phrase ratio, so a word sized at more than twice the phrase keeps the
width the scan shows it at.
- The single-style rewrite (a new family or bold) scales the first
word's Tz by its own size over the phrase's, so the line is as wide
whatever size OCR guessed for that word.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recognize text sizes each word from its own box and a guess at how much
of the em its letters fill, so one phrase's words come out several
points apart (9 to 15.6 pt on a LaTeX scan). Editing a phrase drew and
saved every word at its own size.
An invisible block's size is now the median of its words' sizes
(TextBlock::phrase_size). The styled rewrite draws every run at it and
scales each word's width calibration by its own size over the phrase's,
so words keep the width the scan shows them at; the single-style
rewrite (a new family or bold) uses it in place of the first word's
size; and the inline editor lays each word out at it while typing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An edit was refused after a content stream damaged partway, since what the damage hides can
change how the later streams read, the graphics state they draw in among it. A stream that
doesn't decode at all hides all of its operators, and an edit after one went ahead. It now stops
there too: text lines and blocks, and images (arrange, crop, transform, move), as selecting
objects on `main` already does for a page with such a stream. An edit in a stream before it goes
on, and the stream is kept as stored.
A file may give a stream's /Filter by reference, which isn't read here, so the case needs no
damage: `content_after_a_stream_that_does_not_decode_is_not_edited` has such a stream move the
text and image after it with a `cm`. `a_content_stream_that_does_not_decode_is_kept_and_edited_around`
becomes `..._kept_and_edited_before`, its edit now in the stream before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A page's content streams are read as one stream, so what a stream
damaged partway hides past the damage is unknown to the streams after
it too. A string or comment the damage leaves open runs on into the
next stream, and operators the damage hides can change the graphics
state the later streams draw in. Marking a tag's content as an artifact
took what read as a BDC in a later stream for the tag's own, and could
rewrite bytes inside a string. Moving an image after the damage worked
the move out without a cm the damage may hide.
Now an edit refuses a change to any stream after one damaged partway
(rewritten_contents), and an image isn't arranged into a new stream
placed after one. Marking an artifact or a decorative figure refuses
content that reads as the tag's in a stream after the damage. Edits and
marking before the damage go on as before.
A content stream that doesn't decode at all was read as empty and
whole. A styled edit whose paragraph marker closes past the page's last
operator appended the close to it, so the edit replaced that stream
with one holding only the close. Edits now refuse a change to such a
stream, and still go on around it. Marking an artifact or a decorative
figure whose content isn't found takes it as possibly in such a stream,
as it does for content past the damage, and refuses rather than take it
for absent.
Tests: an_edit_never_rewrites_a_content_stream_damaged_partway (an edit
after the damage refused, one before it made),
text_a_damaged_stream_runs_on_into_is_not_edited,
images_after_a_content_stream_damaged_partway_are_not_moved,
a_content_stream_that_does_not_decode_is_never_written_over,
a_tags_content_after_a_damaged_stream_is_not_rewritten,
a_tags_content_not_found_beside_a_stream_that_does_not_decode_is_not_taken_for_absent,
and a_tags_content_in_a_stream_damaged_partway_is_not_rewritten now
checks the damaged stream itself and marks content before the damage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
`DecodeBudget::decode_strict` read a stream's `/DecodeParms` only when
direct, so content behind a predictor whose parameters are an indirect
object decoded with the predictor's tag bytes left in. Redaction then found
no glyph under a mark, left the text in the file and reported the mark
applied. ISO 32000-2 lets those parameters be indirect.
`decode_strict` takes the document and reads through `decoding_view`, as
`decode_with_damage` does. Its other readers read the parameters too:
Remove Hidden Information's hidden text and layers, the audit's covered-text
check, and edit's backdrop scan.
The accessibility_mark_tag_artifact description says that content which is,
or may be, in a stream damaged partway is refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
`decode_with_damage` and `decode_contents_with_damage` read a stream's
`/DecodeParms` only when direct, as `Stream::decoded` does, so content behind
a predictor whose parameters, or a number in them, are an indirect object
decoded with the predictor's tag bytes left in, and was taken as whole. Edit
then listed and wrote back the wrong bytes, and pruning dropped an image such
a form draws. ISO 32000-2 lets those values be indirect.
- `decode_with_damage` takes the document and reads the parameters through
it, and so does `decode_contents_with_damage`, and `decode_contents` with
it: printing, accessibility's reading of a page and a page copied as a form
read them too. Code that calls `decode` on a stream itself doesn't yet.
- It reads through `Document::decoding_view`: `with_direct_parms`, but a copy
of the stream holds only its `/Filter` and the parameters, its data shared,
so a stream a page's `/Contents` lists again and again isn't copied whole
for each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A stream that doesn't decode at all is not damage to write over: edit and
accessibility keep it as stored and go on with the page's other streams,
and only running past the page's decode allowance refuses. Nothing pinned
either, nor that selecting objects reads a stream of a few MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
A Flate stream damaged partway decodes to what came before the damage, and
`DecodeBudget::decode` gave that back as the stream's data: the decoder's
partial flag was dropped in `Stream::decoded_within_counted`. Editing a text
line, a block, an image or an added item in such a stream wrote it again from
that part, so everything after the damage was lost on save, though other
readers may still read it.
- `DecodeBudget::decode_with_damage` decodes as `decode_within` does and says
whether the stream is damaged partway. A missing checksum or end-of-data
marker is no damage, as before.
- Edit reads the page's streams with it (`ContentStream`), and
`rewritten_contents` refuses, before adding anything, a change to a damaged
stream. Changes in the page's other streams go on, and the damaged one stays
as stored.
- Selecting objects (`editable_objects`, moving added items) refuses a page
with a damaged stream, as it refuses one that doesn't decode: what follows
the damage could change every later item's graphics state.
- Marking a tag as an artifact or a figure as decorative refuses when the
tag's content is in a damaged stream (`ArtifactError::Damaged`,
`AltError::Damaged`), and changes nothing, the structure tree included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY