The tags cleanup resolved the page's `/Properties` at every named `BDC`,
on main too: 20,000 of them with 5,000 entries took 28 s, as measured in
review (optimized), against 0.07 s with one entry. And the second commit's
`MCID` by reference was read again at every `BDC`: the document keeps an
object larger than its cache only until it reads another, so reading the
`MCID` evicted a property list that large, which was parsed again, all of
it, at the next `BDC` (50 `BDC` took 12.3 s against 0.44 s with the
`MCID` given directly). Now the named property lists are read once for
the stream, in place when they are direct, and each name's `MCID` once.
Tests (`crates/redact/tests/operator_work.rs`, a binary of its own whose
tests run one at a time), each the least of three runs against one entry
or operator, allowing ten times as long plus a second:
`named_property_lists_are_read_once_for_a_page` (5,000 `BDC`, 2,000
entries) and `an_oversized_property_list_is_read_once_for_a_stream` (100
`BDC` naming a list too large to keep).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* Fix#5211: make calculateNow synchronous
* Bound and cache calculation execution
* fix(engine): invalidate field calculation cache
* perf(js): build calculation scripts lazily
* Refresh calculation cache after page insertion
* Load calculations for document script helpers
* Assert imported calculation result
* Initialize formatted field-change state
* Update formatted calculation test expectations
* Include the referenced field in calculation test
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Review of the operand reads. Path construction was still read from the
front, in the walk (`construct_path`) and in the proof (`extend_path`):
after a surplus number, `9 100 100 m 9 140 100 l ...` placed the square at
x = 9, so a mark over it removed nothing, and an even surplus placed a
square beside a mark under it, which was removed. `m` and `l` now take
their last two numbers, `c` six, `v` and `y` four, and the proof's `re`
four, as the walk's already did.
The tags cleanup read an `MCID` with `Dict::int`, so one given by
reference in a named property list, or in a structure element's `/MCR`,
was missed and the element kept the removed text in `/ActualText` and
`/Alt`. It is read through the reference now.
Tests: `a_paths_points_after_a_surplus_number_are_where_the_renderer_draws_them`
(lines, and curves by `c`, `v` and `y`),
`an_mcid_given_by_reference_loses_what_redaction_removed` (and an `/MCR` of a
form's stream that stays), and
`the_proof_reads_a_paths_points_from_the_end` (lines, `re`, and each of
`c`, `v` and `y` from a point outside the region).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
An operator takes its operands from the end, and the renderer ignores a
surplus before them: `(x) /OC /MC0 BDC` marks a layer's block, and
`(x) /GS1 gs` selects `/GS1`. Redaction read some of them from the front,
so a surplus stood in for them and what they named went unseen. Found in
review of #6882, where Remove Hidden Information left the text of a
hidden layer in `(x) /OC /MC0 BDC (LAYERSECRET) Tj EMC`, which both the
app's renderer and Poppler hide.
In what Apply Redactions and Remove Hidden Information decide:
- the layers walk read a block's tag and property list from the front,
and kept a hidden layer's block;
- the cleanup of `/ActualText`, `/Alt`, `/E` and `/T` in a block whose
paint was removed read (and wrote) the property list from the front,
and kept the name that still read the removed word;
- the tags cleanup read a block's `MCID` from the front, and left the
removed text in its structure element's `/ActualText` and `/Alt`; and a
`TJ`'s array, so a block that still drew was taken for empty and its
structure element lost the content the page still shows;
- the property cleanups read the name of a `BDC` or `DP` property list
from the front, and retired a property list the page still draws with;
- the walk read the ExtGState a `gs` selects from the front, so a font set
only that way was not set, and `cs`/`CS` the same for a pattern space;
- the Type 3 cleanup's reads of `cs`, `gs`, `Tf`, `BDC` and `DP`, which
decide which resources and fonts are still used.
The redaction proof read `Tc`, `Tw`, `Tz`, `TL`, `Ts`, `Tf`, `"`, `TJ` and
`Do` from the front too. `pdfcraft_content::Op` gains `back`, `name_back`
and `num_back`: operand `k` counted from the end.
Tests (`crates/redact/src/tests/operands_from_the_end.rs`): a hidden
layer's block, `/ActualText` on removed paint, a tag's `MCID`, a tagged
`TJ` that still draws, a property list still drawn with, a font from an
ExtGState, the pattern a `/Pattern cs` keeps, and a Type 3 font with the
ExtGState, colour space, property list and font its glyph uses, each
after a surplus operand. In `tests_verify.rs`,
`the_proof_reads_operands_from_the_end` (`Tf`, `TJ`, and `Do` of a form
and of images), `the_proof_reads_the_text_states_operands_from_the_end`
(`Tc`, `Tw`, `Tz`, `TL`, `Ts` and `"`) and
`an_image_a_type3_glyph_draws_after_a_surplus_operand_is_found`. And
`operands_are_counted_from_the_end` in the content crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Pull requests and main run Linux and Windows only; release.yml builds and signs the macOS app
without running the tests, so macOS-only failures went unseen. On 10 Nov 2026 a hand-started run
found three (two print tests and a bates test, fixed in #6905 and #6913) plus one more
(min_window, fixed separately). One macos-latest leg a night, through the shared gate.
The menu bar folds titles that don't fit into "⋯", or all of them into one "Menu" when not even
the first fits. On hosted macOS, at MIN_WINDOW_SIZE, the bar is the Menu button (the system font is
wider), so the test's File menu item wasn't there and it failed; the main menu is still reachable.
A real macOS window uses the native menu bar.
* feat(js): expose document page boxes
* Normalize page boxes for getPageBox
* Correct page box test expectations and formatting
* Use local page helper in box test
---------
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
output_probe_paths_preserve_non_utf8_names creates files named with bytes that aren't UTF-8.
APFS keeps only UTF-8 file names, so on macOS creating the probe fails before the check runs and
the test fails; no output can have such a name there. It runs on the other unix systems as before.
Found running the automation tests on hosted macOS.
Binds Properties dialog drafts to the vendored edit-session crate (byte-identical to PerchAnton/edit-session@033f7c3a, ledger in vendor/README) and adds replayable UI scenarios. Includes a merge of main and a test covering the clean Properties inspection-tab live refresh.
* macOS: a job that names no paper tray asks for Auto Select (craft-print 1ee01a4c, #3752)
craft-print 1ee01a4c (storytold/craft-libs#232) sends `AP_D_InputSlot=` on macOS for a job whose
options name no tray, as the system Print dialog's Paper Feed: Auto Select does. Without it CUPS
used the PPD's default tray, and a printer whose default tray is empty held every job while CUPS
reported it completed (the Xerox AltaLink in #3752).
- The Print dialog's driver options take the queue's defaults through craft-print's
apply_queue_defaults: option and choice names match as CUPS matches them, and on macOS the
paper tray starts on Auto Select, which is what the job sends.
- The two tests that pin the exact `lp` arguments expect the Auto Select option on macOS.
* Tests: on macOS a CUPS queue on its driver's default tray shows Auto Select (craft-libs#232)
instances_read_the_queue_ppd_and_their_own_defaults and
a_queue_that_never_answers_is_given_up_at_the_deadline expected the PPD's default tray; on macOS
apply_queue_defaults now offers Auto Select first and as the default there, which is what a job
naming no tray sends. Found by running the print tests on hosted macOS.
#5861 (#6100) reads the chosen printer's papers and default paper in the background, and the
default replaces the dialog's paper unless it was chosen in the dialog (paper_overridden). The
paper a printer's setup restores (#2001, #6779) didn't count as chosen, so on Windows the driver's
default replaced it each time the dialog opened: an office printer last printed on A3 came back
on A4.
apply_setup now marks the paper it restores as chosen. A test drives the dialog with a printer
remembered on A3 whose driver's default is A4.
* Render: an isolated CMYK group of any size blends in CMYK, in bands
hayro patch (27) blended an isolated DeviceCMYK transparency group in CMYK only up to 2^24
device pixels (a 4096 x 4096 square); a larger one was blended in RGB. Print as image renders a
whole page at once, up to 8192 px on the long side, so a page enlarged to 11 x 17 in at 600 dpi
(about 43 MP) blended any group covering most of it in RGB, and a print test file's CMYK blend
patches drew their X's. The same applied to exporting a large page as an image.
A larger group is now blended in bands of whole rows, each within the old limit: both ink passes
and the tint conversion run per band, so their memory stays as before, and only the finished
group (4 bytes a pixel) is kept whole. Every band is blended before any is drawn, so a group a
pass has no tints for is still drawn as before, whole.
The regression test lowers the band to 7 rows (set_cmyk_band_pixels, hidden, per thread like
set_skip_offscreen_paths) and checks the banded group matches the whole one across an
anti-aliased edge, and is CMYK's green where magenta is under black with Difference.
* Test: CMYK green through the profile is (0, 166, 81); check green against RGB's magenta
On Windows the printer list reports whether a driver supports two-sided printing, but the job still asked for it anyway, so duplex jobs printed single-sided or misaligned on printers such as the reported Brother model.
Leave duplex unset for a printer reported as not two-sided, so that printer keeps its own setting, while printers that support two-sided or report no capability keep the chosen setting.
Test: duplex_is_not_sent_to_a_printer_without_two_sided failed before the change and passes after.
Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Print in grayscale prints on a Windows driver with no grey option (#5806)
Windows refused Print in grayscale ("the printer's driver did not retain the requested
grayscale; nothing was sent") on drivers that list no grey option the ticket can name:
Microsoft Print to PDF, and an HP Photosmart driver whose grey is its own vendor setting.
- On Windows, a grayscale job's copy for the spooler is made grey first with Convert colours
(all pages, comments and form fields, black kept black, pictures re-encoded where they can
be), and the job says so (Job::grey_pages). The driver is still asked for grey where it has
it; one without prints the grey pages instead of refusing them. A copy that can't be read
goes as it is and asks the driver strictly, as before.
- craft-print 970644b7 (storytold/craft-libs#774) adds Job::grey_pages and asks a driver that
lists only psk:Grayscale for that, not psk:Monochrome.
- Both the Print dialog and doc_print go through spooler_copy, which now takes the job.
* craft-print 1146cdc4: craft-libs main with #774 (grey pages) and #773 (the CUPS printer list waits at most 15 s, storytold/pdfcraft#6761)
Review of the walk's read of a form's `/Matrix`. Through `Document::resolve`
it followed a reference to a reference, for the matrix and for each entry,
as the app's renderer does; Poppler doesn't, and reads such a matrix as
the identity and such an entry as 0. With
`/Matrix [1 0 0 1 0 8 0 R]`, `8 0 obj 9 0 R` and `9 0 obj 200`, a mark
where Poppler shows the text applied with success and left it in the file,
where `main`, reading any entry by reference as the identity, removed it.
`matrix_of` now follows each reference one hop, and a reference to a
reference fails as a malformed matrix: where the content draws can't be
told, so Apply refuses the page, as for a matrix that isn't six numbers.
The refusal now says "isn't six numbers given directly or through one
reference", and `matrix_of`'s doc names the appearances it reads, not
patterns. The proof, which reads the matrix of forms and appearances
with it, treats such a matrix as malformed too.
Test: `a_form_whose_matrix_goes_through_a_reference_to_a_reference_is_refused`
(an entry and the whole matrix, with a mark where either reader draws the
text).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Review of the walk's read of a form's `/Matrix`.
Removing hidden layers alone never asks where content lands, but a form
whose `/Matrix` isn't six numbers was counted in its scan and refused, in
a document without layers too. Such a form is now walked as it stands
there; Apply Redactions, and Remove Hidden Information with hidden text,
which tells hidden text by where it lands, still refuse it.
`matrix_of` resolved and copied every entry before checking there were
six, and the walk reads it at every `Do`. Measured in review (debug
build), 20,000 draws of a form whose
`/Matrix` is 20,000 entries long took 9 s by reference (16 ms before the
walk read entries) and 12 s written directly (2.6 s before, the array
being copied by `resolve`). It now reads an array written directly in
place, and checks the length before resolving any entry.
Tests: `a_form_whose_matrix_is_not_six_numbers_leaves_removing_hidden_layers_alone`,
and `tests/matrix_work.rs` (a binary of its own), whose
`a_long_form_matrix_costs_no_more_than_a_short_one` times 4,000 draws of
a form with a 4,000-entry `/Matrix` against a 7-entry one, written
directly and by reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The walk read a form's `/Matrix` with `Matrix::from_operands` on the array
as written, so an entry given by reference made it the identity. That is
legal: an array's entries may each be an indirect reference (ISO 32000-2
§7.3.10), and Arlington's `XObjectFormType1` allows the matrix itself to
be one. A form moved onto a mark by such a matrix was then tested against
the wrong place, never walked, and Apply Redactions reported success with
its text still in the file: `/Matrix [1 0 0 1 0 8 0 R]` with `8 0 obj 200`
left SECRET, which Poppler's `pdftotext` places under the mark. Remove
Hidden Information missed text such a matrix moves off the page the same
way. Found in review of the unparsed-bytes refusal.
The walk now reads it with the proof's `matrix_of` (`verify/extract.rs`),
which resolves the array and each entry. A `/Matrix` that isn't six
numbers was the identity too; now where the form draws can't be told, so
Apply refuses the page and the scans count it, as for a form past the
page's budget. `matrix_of` now reads a null `/Matrix`, given directly or
by reference, as the identity: a null entry is no entry (ISO 32000-2
§7.3.9).
Tests (`crates/redact/src/tests/form_matrix.rs`): a form moved onto the
mark by a matrix written direct, as a reference, and with an entry by
reference, redacted; a null matrix, the identity; a matrix that isn't six
numbers, refused, and counted by the verify pass and Remove Hidden
Information's scan; and hidden text a matrix entry by reference moves off
the page, removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
* M10: a booklet's both sides follow a driver that keeps its own two-sided setting (#5772)
Booklet ▸ Both sides always asked for two-sided printing on the short
edge (#2707). A driver that doesn't take two-sided settings from other
programs (no two-sided printing, manual two-sided only in its
Properties, Microsoft Print to PDF) then made craft-print refuse the
whole job. The reporter's Epson ET-2870 offers only OneSided, and its
manual two-sided setting is what prints their normal two-sided jobs.
print::booklet_job_duplex asks such a driver for nothing on both
sides, so its own setting applies, as for ordinary printing (#2422)
and as Acrobat enables two-sided for a booklet only "if available".
Unknown support still asks for the short edge; one side on its own
stays one-sided. The Print dialog takes the chosen printer's support
from the printer list (PrintDraft::printer_two_sided, also behind its
"its own setting applies" note), and doc_print reads it only for a
booklet's both sides.
The dialog's booklet test used the machine's printers, and on Windows
the default, Microsoft Print to PDF, now gives the other answer: it
sets its own printers and checks the note too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: doc_print's lookup of a booklet printer's two-sided support is tested (#5772)
The booklet test fed job_duplex the printer's answer directly, so a
lookup that missed the default printer or matched the wrong one would
have gone unnoticed. The search over the printer list is now its own
function, two_sided_in, tested with a list of named, default and
unknown printers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M10: a booklet printed to "default" goes to the printer whose driver was asked (#5772)
doc_print decides a booklet's both sides from the printer's two-sided
support. For "default", the job named no printer, so the spooler picked
whichever printer was the default when the job was submitted, after the
document was rendered. The printer found in the list is now the job's
printer, by name, and the lookup test covers a one-sided default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #5903: the booklet test lists its printers through a stand-in source
It set the dialog's printer list by hand while the real list was still loading. On a machine with
no printers (CI's Linux runner) the list that arrived replaced them and reset the printer, so the
dialog the test clicked through was not the one it set up. The printers now come from the
dialog's own source, drivers unasked as #3554 lists them, with each driver's two-sided answer in
the map.
---------
Co-authored-by: austin62jake <340374338+austin62jake@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* fix(ui-egui): keep hotbar picker stationary while resizing
* fix(ui-egui): keep hotbar picker pinned during keyboard resize
Pin the Hotbar tools picker while the Toolbar size slider is adjusted with the arrow keys, until the slider loses focus. Widen the regression test to both drag directions and every layout, add a keyboard test, and document the new helpers.
* fix(ui-egui): closing the hotbar picker ends a size adjustment
close_picker() also clears picker_drag_pos and keyboard_sizing. Main's
close_hidden_picker() closes the picker on passes that don't draw the
hotbar, where show() can't clear them, and the next opening would start
at the held position. Adds closing_the_picker_ends_a_size_adjustment, a
doc comment for picker_drag_pos, and says "picker" where the comment
meant it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(print): keep the localized reset label off the properties panel title
The header of Print ▸ Properties… laid the title out first and gave what
was left of the row to Done and Reset. When the translated "Reset to
printer defaults" needs more room than that, egui keeps placing the two
buttons from the right edge of the child leftwards and paints them back
over the title: in Italian the label is 51 characters against 25 in
English, and both strings end up unreadable (#5545).
Laid out with egui::Sides::shrink_left().truncate instead: the buttons
take their room first and the title takes what is left of the row, cut
with an ellipsis. Nothing moves in English, where the title still fits.
* test(print): pin the header against every catalog and the title's tooltip
The geometry tests behind #5545 tried English, Italian and German at four
widths. They now walk every catalogued language at the row the settings column
really gives the header (470 px less the frame's 10 px margins), so the longest
translation of "Reset to printer defaults" is covered by the run instead of by a
guess at which locale is the worst one.
Two more tests: hovering the elided title opens a tooltip with the full string,
while a title that fits whole opens none; and a row too narrow to give the title
even an ellipsis leaves it out instead of painting it back over the buttons.
That last case is why driver_options_header returns Option<Response>. Bulgarian
(370 px of button), Spanish (365) and Ukrainian (361) fill the row with the two
buttons alone at 450 px: the ellipsis is 12.4 px wide and the clearance was
0.1 px, so a pixel less and the label was painted over the buttons again.
---------
Co-authored-by: Jarvis (Hermes) <jarvis@localhost>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Review of the refusal. `doc_hidden_info` said it lists what Remove Hidden
Information would remove. A page that can't be read in full is counted as
one item of hidden text and of hidden layers, so that the removal runs and
refuses it: since the refusal before this, that includes a page holding
bytes the parser can't read or an inline image whose end is a guess, not
only content streams that don't decode. The description now says so, and
that with form fields removed too, a field's appearance is read the same
way though it isn't counted. The parity note on Apply Redactions lists the
new refusal beside the inline image's.
Removing hidden layers alone refuses such a page in a document without
`/OCProperties` too, which a review asked to narrow: a membership
dictionary there (`/AllOff` of a layer that is on) still hides what it
marks, for the walk and for the app's renderer. The comment at the
refusal in `sanitize.rs` says only what the bytes are shown to hide: text
in a broken `TJ`.
Tests: `bytes_the_parser_cannot_read_refuse_removing_hidden_layers_alone`;
`hidden_text_beside_bytes_the_parser_cannot_read_in_a_field_appearance_is_refused`
(removing form fields with hidden text, and Sanitize Document, which went
on and left it); and `hidden_text_beside_bytes_the_parser_cannot_read_is_refused`
gains hidden text in a broken `TJ` inside a form. The form-away test's doc
says the check after the walk would still refuse a form without resources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
The redaction walk reads past bytes the content parser can't read: a
stray `)` or `}` in a `TJ` array, say. Text they hide is neither found nor
removed, while other readers draw it: Poppler's `pdftotext` extracts
`[(TAIL) ) (SECRET)] TJ` as TAILSECRET. The only refusal was a side effect
of the reads after the walk, which run only when the walk rewrote
something, read what it rewrote as rewritten, and in the font cleanup's
case only when the resources list a font. So Apply Redactions reported
success and left the text where the walk rewrites nothing (the only text
under the mark in such a `TJ`, or one in a form with resources of its
own), in a stream it rewrites (it wrote the array out as
`(TAIL) (SECRET) TJ`, without the stray byte or the brackets), and on a
page whose font is set only through an ExtGState. Remove Hidden
Information never counted hidden text in such a `TJ`: it found nothing to
remove, or left it beside what it removed.
The walk now records that it met such bytes (`Output::unparsed`, from
`Parsed::skipped`, nested forms included), and Apply Redactions refuses
the page with `RedactError::Unreadable`, as it does an inline image whose
end is a guess. Remove Hidden Information counts such a page in its scan,
so the removal runs and refuses it, as it does content that doesn't
decode; it refuses an inline image whose end is a guess the same way,
which it took on trust before. Apply walks only the forms that reach a
mark, so bytes in a form away from the marks don't make the walk refuse
the page; in a form without resources of its own, the check after the
walk still refuses them when it runs, as before.
Tests (`crates/redact/src/tests/unparsed.rs`): text under a mark in four
broken `TJ` arrays, alone and beside other text; the same in a stream the
walk rewrites; in a form under the mark with resources of its own; on a
page without font resources; hidden text in such a `TJ`, alone and beside
other hidden text, through the scan, Remove Hidden Information and
Sanitize Document; hidden text beside an inline image the renderer can't
end; and a form away from the marks, which is still redacted around. Two
tests in `tests.rs` now expect the walk's refusal; the check after the
walk keeps a case of its own, a form without resources away from the
mark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Review of the allowance change. Only Apply's page caller had a row with no
Type 3 font listed. `the_cleanups_of_a_rewritten_form_spend_the_pages_allowance`
and `removing_hidden_text_spends_what_the_content_walk_left_of_the_pages_allowance`
now have one too: at 100 MiB the redaction and the removal go on.
Doc comments that said every cleanup of a page spends the page's allowance
(`prune_type3_from_resources`, `resource_ops`, `prune`) now say the font
cleanup has one of its own when no Type 3 font is listed. The doc of
`text_beside_bytes_the_parser_cannot_read_refuses_the_redaction` says the
cleanups run only when the walk rewrites some of the page, and read what it
rewrote as rewritten. The comment in `prune_type3_from_resources` no longer
calls its read the only check for text: `prune_form_resources` (for a
rewritten form), `prune` (when an image or form is replaced) and
`prune_properties` make the same read when they run, and `unlink_type3`
reads every page when the document still lists a Type 3 font.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
CodeRabbit found this on #6805, which landed #6341 on main.
`prune_type3_from_resources` reads what the content draws through its
resources (`resource_ops`) whenever the resources list any font, and only
then looks for a Type 3 font, the only kind it prunes. Since #6341 that read
is charged to what the content walk left of the page's allowance. So a page
with only TrueType fonts and a large form without resources of its own
could be refused, by Apply Redactions or Remove Hidden Information, for a
read that prunes nothing: a 100 MiB form crossing the mark is read by the
walk, the page's check and this read, 300 MiB against the page's 256.
Without a Type 3 font, the read is now charged to an allowance of a page of
its own; with one, to the page's, as before. The read is still made with
any font listed, as CodeRabbit's suggested early return would not: it
refuses bytes the parser can't read in the streams and forms it reads, and
an early return would drop that refusal on every page without a Type 3
font. The walk reads past such bytes, so where nothing else reads the same
content, text a stray `)` hides in a `TJ` array under the mark would stay
while Apply Redactions and Remove Hidden Information reported success
(found in review). The refusal covers only what the cleanups read, so such
bytes still go by elsewhere, here as on main. The read and the
pruning tell a Type 3 font the same way (`is_type3`).
Tests:
- `the_cleanups_after_the_content_walk_spend_what_it_left_of_the_pages_allowance`
now also redacts the 100 MiB page with no Type 3 font, which goes on; it
and the two tests like it (a rewritten form's cleanups, Remove Hidden
Information) list an unused Type 3 font where their font cleanup runs,
so that it still charges the page's allowance (`list_type3`).
- `text_beside_bytes_the_parser_cannot_read_refuses_the_redaction`: four
such `TJ` arrays in a page's second stream, one in a form without
resources, and hidden text beside one, on pages listing only a TrueType
font, are refused by Apply Redactions and Remove Hidden Information.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY