A predictor whose parameters are an indirect object left its row tags in
the decoded font program and glyph map, so the Thai map repair, the glyph
codes and the shaper read the wrong bytes. Resolve the parameters as the
other font readers do, keeping the decode budgets.
The hybrid update test now checks that the update's page and catalog
win and that the repair log says why, and that the linked file keeps
both revisions; the save test checks that the reopened file keeps the
update's page and catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
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.
- Pdf::new_with_password reads the table the last startxref names first: a wrong password is
refused through it, and the rebuilt table is taken only when it is encrypted exactly when
that table is.
- Tests: an encrypted file with a cut-off update needs its password; data after %%EOF holding
an object header reads as the document model reads it (both take its page).
- vendor/README.md lists the split tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
- `has_signatures` treats a /Contents of only zero bytes as unsigned and finds
signature dictionaries in one pass over the file (field /V values, plus
/ByteRange dictionaries and document timestamps) instead of two.
- Before the first signature, a field's own appearance counts as drawing text
only if it or a form XObject it paints shows text; a background-only XObject
no longer keeps an appearance that would leave the field blank. The look
inside opens at most 16 streams; past that it counts as text, as before.
- Tests: zero-filled placeholders (hex and literal), document timestamps, a
widget without /AP, background-only and text-drawing XObject appearances
with an unencodable character.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests only: a complete update, trailing data without objects and a short cut-off update each
have their own test, and the trailing-data case asserts the model's page count as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
page_modules_read_the_pointer_only_through_page_pointer treats any file
that mentions `.router.` as a page module, and failed on
workflows/ocr_web.rs: its `self.router` is the OCR pool's
ocr_route::Router, which hands each page's text to the run that sent it,
not the pages' PageRouter. Adding the file to PAGE_MODULES would put
the workers under the page-input checks, so the field is renamed
`runs` instead.
Tests: the page_pointer tests and all 808 pdfcraft-ui-egui lib tests
pass, and cargo check for wasm32-unknown-unknown builds.
In the web build, Verify's Run OCR and its page/region re-reads never
started the model download. On a fresh page they failed at once with the
desktop's "set PDFCRAFT_MODELS" message, and worked only after Recognize
Text had downloaded the models.
Both now start the download (web_models_missing), say "Downloading the
text recognition models…" and remember what was asked; poll_models runs
it once the models land, or shows "Couldn't download the text
recognition models: {e}" when the download fails. Nothing runs if the
screen was closed meanwhile, and an engine the browser can never run
says why instead of waiting.
Tests: models_landed starts a waiting run and a waiting re-read, reports
a failed download without touching the review, and runs nothing for a
closed screen. The first three fail without the fix. Checked by hand in
Chrome: a fresh page, Verify > Run OCR downloads the models and reads
the page (47 words on a real scan); with the models missing it shows
the HTTP 404 and a second click after restoring them works.
Hosting notes for the models and workers, the platform parity text, the
ocr.ocr-web parity entry (now citing the ported tests, with the known
limits: the page runs the cleaning steps, the Verify screen re-reads on
the page, ocrs only), and the roadmap logs.