Conflicts:
- crates/redact/src/lib.rs: main added the run's ImageBudget to Applying
and Scope::new; kept both sides (the removed-text fields and the image
budget), with main's refused-images check before the destructuring.
- crates/redact/src/tests.rs (auto-merged): this PR's two tests that build
a Scope or an Applying now pass main's ImageBudget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyG234xeYkJfr41E6iP
`ImageBudget::decode` is now the one public way to decode an image's
CCITT fax or JBIG2 data: `Document::decoded_image` and
`decoded_image_within`, which decoded with no limit on the work, are
test-only in pdfcraft-cos, and pdfcraft-filters exports only
`decode_ccitt_within` and `decode_jbig2_within`, which spend from a
caller's allowance (`decode_jbig2` is gone, `decode_ccitt` private).
So an operation over many images can't decode them without sharing an
allowance; the compiler holds new callers to it. Doctests that must not
compile check both crates; with the old entry points public they fail.
Tests outside the crates decode through an unlimited ImageBudget.
Streams in general (`Stream::decoded`) are left for a follow-up.
- Add Freehand-Highlight tool with 100pt line width support, Lucide highlighter icon, and PDF Multiply blending.
- Add customizable floating and docked hotbar with command picker, size slider, full-side strip chrome integration, and settings gear icon anchored at the bar end.
- Add left/right docking options for the navigation rail with toggle button and preferences setting.
The test timed the 100-field form three times, then the 800-field form
three times, and compared the best of each. A busy spell on a shared
runner that fell on the second half slowed only the large form, and the
ratio went past 16x (16.6x on a gate run) though it is about 7x.
Each of five rounds now opens the small form and then the large one, and
the best round's ratio is checked against the same 16x, after one
untimed opening of each. With the values filled one by one again (the
quadratic code before #4638) it still fails, at about 48x.
#2539 review: the scrub removes the catalog's XMP packet whole when it holds
the removed text, and with it the file's PDF/A and PDF/UA claims, which
Report didn't say. The identification (pdfaid part, conformance, amd, rev;
pdfuaid part, rev) is now read by namespace (roxmltree, already in the build
through pdfcraft-xfa), each value kept only when it is the short code it must
be and doesn't hold the removed text (one-character codes always), and
written back in a packet holding nothing else, with a fixed pdfuaid extension
schema under PDF/A-1 to -3 (left out, with the PDF/UA claim, if the redacted
words are among its words). Report::identification says, per standard,
None / Kept / Lost; the engine passes it on as
EditNotice::RedactionIdentification, the Apply notification says it after the
count, and redact_apply returns identification { pdfa, pdfua }.
Tests (redact): the_identification_of_a_removed_packet_is_written_back,
a_packet_without_identification_is_only_removed,
an_identification_that_cannot_be_kept_is_reported_lost,
unsearched_text_keeps_only_one_character_codes,
the_extension_schema_is_left_out_when_it_uses_a_redacted_word; engine
applied_redactions_report_the_pdfa_identification_written_back; automation
redact_apply_reports_the_identification_written_back; ui-egui
applying_says_the_pdfa_identification_was_written_back. With the write-back
replaced by "nothing kept", the redact tests fail (Kept expected, None
found); with the notice not pushed, the engine, automation and UI tests fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Main's `EditNotice` is "something the last edit did that the user should hear
about", set by `Session::apply` and cleared by the next edit, undo and redo,
which is what the PR's separate `redaction_elsewhere` field did by hand. It is
now `EditNotice::RedactionElsewhere(Elsewhere)`, pushed when a place is counted
or the text couldn't be searched for: tools list it in `warnings` with the
other notices, the interface says it through the same path, and
`Document::redaction_elsewhere()` gives `redact_apply` and the Apply dialog the
counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Conflict in crates/engine/src/document.rs: main turned `edit_notices` into
`Vec<EditNotice>`; the PR's `redaction_elsewhere` field is kept beside it in
this merge, and the next commit makes it an `EditNotice` variant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Document the existing policy: mixed plain and target-script text normalizes to that script, then to plain text, rather than reconstructing the original mixture. Undo restores exact prior text. Cover both scripts in engine and headless tool tests, including document-byte restoration, redo and save/reopen, and record the evidence in parity. The new tests pass against unchanged conversion logic; this is policy documentation and coverage, not a product behavior fix. Scoped Linux native, wasm, clippy, formatting, layers, assets and parity validation passed.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Reject finite but singular or overflowing determinants at the engine boundary. Promote added images for arbitrary transforms, as RotateBy already does, while preserving editable quarter turns and reflections. Regressions verify transformed placement, promotion, undo and refusal without mutation.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Each `Do` copied the page's `/XObject` dictionary, and each form its `/Resources`, `/Properties`,
`/Font` and `/ExtGState`, or the resources it was drawn with if it had none. So Office export of one
page took time in the square of what it drew: 31 s for 10,000 pictures, about 70 ms of it decoding, 30
s for 10,000 forms with no `/Resources`, and 16 minutes for one form with 100,000 keys in them drawn
50,000 times, in a test build.
`FormCall` keeps the form and `resource_dict` reads a dictionary where it is, with an object of its
own held. 10,000 pictures take about 0.15 s now, 10,000 forms 79 ms, and that form drawn 5,000 times 40 ms. The test holds to 10,000 pictures, not the 2,000 it could afford.
A `gs` reads a direct graphics state in place too, in the text walk and in `page_background`, and a
value of the wrong type isn't copied to find that out. A form that sets a font still reads the font each time it is drawn, as the fonts read
are kept only for that draw: 3.5 ms a draw for one of 100,000 widths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
An explicit size commit normalizes mixed-size selections even when the value matches the displayed size. Caret-only script commands format the whole draft. Mathematical minus uses reversible Unicode forms and ASCII hyphen-minus remains unchanged. Correct the applied-selection comment and document the character policy in parity and the tool.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
An original image and an added item may share an XObject without sharing editable metadata. Require the selected drawing stream to own the added item instead of falling back to any matching resource. The regression actually moved the added sibling before the fix; scoped native and wasm gates now pass.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Resolve explicitly configured font roots while skipping descendant links; retain all accepted fonts within the existing bounded scan. Allow an explicit Reload without duplicate workers or an app restart. Three actual reverted-fix regressions fail; scoped engine/UI clippy, 2400 tests, wasm, formatting, layers and assets pass on AI02.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Reuse the existing subset-tag parser through the engine facade and share the fallback formatting state.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
The four character match blocks become two const tables in a new engine
module, text_script, keeping the minus sign's mapping. One toggle_script in
Edit text replaces the three copies (paragraph editor, added-text draft and
the Format text button state).
A range is now 'already in the script' when at least one character is and
none of the others has a form in it. Before, one character with no Unicode
form (a space, q, H in H2O) kept the range from ever counting as converted,
so the second click converted again, changed nothing, and the text could not
be toggled back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restore main's text where the PR changed things it did not need to:
intra-doc links turned into plain code spans (engine js.rs and
camera_edges.rs, forms raster.rs, fonts pdf.rs, sign keys.rs and
pkcs12.rs, ui-egui motion.rs and web_print.rs), the #[allow(dead_code)]
on render's byte_capacity and ui-egui's unit_for_tag (both are used on
every native target, as on main), the loosened combine_grid timings, the
forms test case rename (scratch() already numbers its folders), and the
computed xref size in a_recovered_page_tree_survives_a_full_save (always
5 there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
discover charged each candidate's size against the 64 MiB budget before
reading it, so proprietary or broken files early in a fonts folder could
use the budget up and hide openly licensed fonts after them. Charge only
fonts that pass validation and are not duplicates. Reads are still
bounded: every file read counts against four times the budget.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- page_image_at: PageImage::contains already requires the point inside
visible_rect (rect clipped by the placement clip), so the extra bounds
test before it decided nothing.
- image_dimensions: the 256 MB input-length guard repeated the caller's;
the decoder's max_alloc limit still caps what decoding allocates. Its
test no longer allocates 256 MB to hit that guard.
- font_content_key hashed every font file twice; one 64-bit hash plus the
length is enough to tell duplicate files apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_edit_page_image carried its own inverse, bbox and multiply; Matrix
already has invert, bbox and then with the same formulas and the same
degenerate-determinant guard. Also drop the second finite check on Move
(checked on entry), match the added item with two finds instead of a
four-way chain, and toggle flips in one place for both the parallel and
perpendicular cases. No behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The document keeps the last apply's Report::still_holding as redaction_elsewhere (re-exported as
RedactionElsewhere), cleared by the next edit, undo or redo as edit_notices is. redact_apply
returns it as still_holding with the four counts, their total and unsearched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
Brings the branch up to date with main (47f42ba0), which moved 37 commits since
the merge base, including page-edge shadows (#2088), full rewrite after page
delete (#4279) and the licence-check stage (#2212). No conflicts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The allowance's sharing was tested on a few images. This runs a whole page through the export: the
pictures past it are left out and counted, and the page is listed, which is what the warning goes
by. A few large pictures all come through. The README gives the numbers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Storing LZW streams with Flate decoded image streams outside the allowance,
with a limit for each stream and no total, and nothing asked whether to go
on: a 593 KB file of 100 images took 113 s with none refused, and images
refused by the image pass were still rewritten. The pass now pays for what
it decodes and for storing and checking it, at 8 steps a byte since Flate
takes up to 78 ns a byte, and streams past that stay LZW.
An image is counted as refused only when it stayed as it was: one packed
before the allowance ran out on its second read was rewritten. doc_reduce
reports images_refused as doc_optimize does, and the app's action says so
in its log. A refused image no longer reads as an unsupported filter:
FilterError::Spent says what happened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
- [P1] Arrange: track and replay inherited non-ExtGState drawing state (colors, line widths, etc.) across q/Q scopes for stencil and form artwork
- [P2] Flips: route ImageEdit::Flip through coordinate-aware transform logic to swap axes when rotated at odd quarter turns (90°/270°)
- [P2] Rotation knob: measure clockwise angle delta from drag start relative to the rotated image center to prevent reapplying existing angle
- [P2] Format panel: connect superscript/subscript controls to line editor and content draft selections, and update parity evidence
From a review of who reads a refusal.
The rule is to leave it out and say so, and it held on some paths only. An image whose JBIG2 mask is
refused no longer comes out opaque. The first image past the allowance reads as that and not as
corrupt. A page reports what it left out. The Office warning stopped blaming forms. Redaction refuses
the run, leaving the document as it was: removing a readable image whole would take what no mark
covers with it, unreported.
The 2 GiB an export holds is twice the document if that is more, so a large honest file loses nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
One image has its own limit, about 6 s to 17 s, but nothing bounded a file of thousands of images
made to reach it: an operation took that long for each, and an image sharing /JBIG2Globals decodes
them again.
Image::decode_within (hayro-jbig2 patch 11) spends from what a run may still do, starting with its
page's pixels, and charge_reading charges reading a stream and its globals, 4 steps a byte, before
anything is parsed, so an image that fails has paid too. pdfcraft_filters::decode_jbig2_within takes
it, and ImageBudget in cos is that allowance: 2^34 steps, about 1,000 scanned pages, and 2^13 more for
each byte of the file, so a long scan is read whole. An image past it spends all that's left, and
later ones aren't read, globals included.
Optimize leaves such an image as it is and counts it in Report::images_refused. Image export leaves it
out, with the reason, and stops at 2 GiB of files held. Office export shares one allowance, leaves the
picture out and says the page was truncated. Redaction removes it whole, as any image it can't read.
Rendering gives a page's images 2^30 steps between them, inside the watchdog, and each page starts
afresh in Context::new as its other budgets do (hayro-syntax patch 31, hayro-interpret patch 73).
It's a thread-local, unbounded until a page sets it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
- Resolve all 13 merge conflicts between upstream/main and main
- Preserve full image editing suite: OBB, crop, proportional resize, arrange, and context menu actions
- Preserve text editing formatting tools: bullet/numbered lists, subscript/superscript, and line spacing
- Cleanly integrate upstream's canvas modularization, font updates, and styling changes
- Refactor arrange_image and run_edit_page_image helpers to satisfy clippy cognitive complexity limits
- All engine, edit, and UI integration test suites passing cleanly
From CodeRabbit's review of the stack:
- Every JBIG2 image read its /JBIG2Globals afresh, within 32 MiB each but with no
bound across an export or a redaction. A globals stream of the file, not edited since
it was read and with its /DecodeParms direct, now has its filters undone once a
document and is kept for the images that share it (in every clone, which share the
file), up to 64 MiB in all. An edited one, one added in memory, or one with
/DecodeParms by reference (another object, which can change under it) is read each
time as before. This saves the filters only: the decoder reads the segments afresh for
every image, so a real symbol dictionary still costs its decoding once an image.
- view.copy-image dropped "JBIG2 inside other filters" from its missing list on the
strength of Export's test; Copy Image now has its own: a JBIG2 picture behind
ASCIIHexDecode is found under the pointer and copied as its pixels and as a PNG, and
one whose page information is in its globals is read through them.
A JBIG2 image's /Width and /Height are read as given directly; one by reference is an
error rather than an image of no rows.
A /JBIG2Globals reference to no object still reads as no globals: it is a reference
to null (ISO 32000-2 §7.3.10), which an entry may hold for being absent (§7.3.7), and
the renderer draws the page so. An image that needed those globals then mostly fails
to decode; in a probe of twelve pictures that decoded with them, eleven were refused
and one decoded to other pixels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Includes #4325#1952#4180#2529#4320#4805, preserving their original histories.
Keep paragraph alignment and hard/soft wrap metadata together; retain CCITT/downsampling/RGB ownership guards with bounded JBIG2 export and exact-copy symbol encoding.
Combined interaction regressions added. Validation pending.
doc_reduce, the engine's reduced_bytes doc, the optimize README's
presets and the optimize.reduce parity note still said every image
above 225 ppi goes to 150 ppi; black-and-white images go to 300 ppi
above 450 ppi, as CCITT Group 4 when smaller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Redaction doesn't decode CCITT, so a mark over part of an image the
optimizer stored as Group 4 removes the whole image (nothing leaks),
and Export ▸ Images refuses it. The optimize README and the
mono-compression parity note say so. Later PRs of this stack add both
(#4335 export, #4384 redaction).
- doc_reduce's description, Engine::reduced_bytes and docs/cli-batch.md
now list the black-and-white step; docs/target-app-parity.md's
optimize row gives the measured counts and no longer lists CCITT as
missing; the M11 milestone row lists it as done.
From reviews with the pdfcraft-review skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
No conflicts. main's changes to the files this branch touches (the page
cursor router in canvas.rs, hover tips in tools/comments, new tools and
document methods) don't meet this branch's hunks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moving a rectangle, an oval or any other comment showed only its blue
frame going, the shape staying where it was until release. The comment is
now rendered alone once as the drag starts (Document::comment_layer: a
read-only copy with only that comment, no page content) and laid over the
page where it goes; the page beneath keeps showing it where it was until
the move is applied, as a CAD program's move does. Rendering the page
without it would cost a whole page's render at every drag on a heavy plan.
Stamps and Fill & Sign marks keep their own layers. annot: isolate_annotations
for several comments of a page, so a group can be drawn together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>