Commit Graph
6348 Commits
Author SHA1 Message Date
Grzegorz MartensandClaude Opus 5.5 5e2552dcb0 M8: named property lists and their MCIDs are read once for a stream
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
2026-11-10 23:11:09 +01:00
Harrison BoothandFredApps c73a53705b Fix #5211: make calculateNow synchronous (#5304)
* 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>
2026-11-10 22:48:50 +01:00
Grzegorz MartensandClaude Opus 5.5 7c78222612 M8: path points and an MCID by reference are read as the renderer does
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
2026-11-10 22:22:09 +01:00
Grzegorz MartensandClaude Opus 5.5 b16f0c0a77 M8: redaction reads an operator's operands from the end, as the renderer does
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
2026-11-10 22:22:09 +01:00
FredApps 1be1cfa6ed CI: the workspace's tests on macOS each night (and on demand) (#6921)
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.
2026-11-10 22:13:16 +01:00
FredApps 85987baef0 Test: at the minimum width on macOS the main menu may be the folded Menu button (#6920)
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.
2026-11-10 21:51:38 +01:00
Harrison BoothandFredApps 3df6be4eff feat(js): expose document page boxes (#5501)
* 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>
2026-11-10 21:37:10 +01:00
Yusuf MANTAM 1fc9501cf3 fix(platform): preserve file visibility when saving replaced files (#5001) (#5048) 2026-11-10 21:01:27 +01:00
FredApps b10f85262c Test: the non-UTF-8 output-name probe test doesn't run on macOS (#6913)
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.
2026-11-10 20:57:08 +01:00
Perchenko Anton d47adeff89 M4: bind property drafts with edit-session and replay UI scenarios (#4594)
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.
2026-11-10 20:22:58 +01:00
FredApps 8e6ec925c8 macOS: a job that names no paper tray asks for Auto Select (craft-print 1ee01a4c, #3752) (#6905)
* 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.
2026-11-10 20:17:41 +01:00
FredApps 0bffa42fc5 Merge pull request #6889 from Combiler01/fix/continuous-scrolling-default-6780
fix: Continuous Scrolling toggle not working in Preference tab (#6780)
2026-11-10 20:05:21 +01:00
FredApps b16cd32659 Print: a printer's remembered paper wins over its driver's default paper (#6906)
#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.
2026-11-10 20:03:14 +01:00
Harsh Verma 572ee68cb2 Merge branch 'storytold:main' into fix/continuous-scrolling-default-6780 2026-11-11 00:01:24 +05:30
Combiler01 ef18d60cb8 fix(ui): update only changed display fields when defaults preference is already active 2026-11-10 23:23:53 +05:30
Combiler01 91e54eeb37 fix(ui): preserve presentation display state when applying preferences 2026-11-10 23:08:08 +05:30
Kyle KassenandFredApps 92b965dba3 M10: preserve annotation layers in vector print output (#5019)
* M10: preserve annotation layers in vector printing (#5005)

* #5019 test: a Popup is the annotation that never prints; a Link prints since #5028

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 18:26:50 +01:00
FredApps 655aa92b5e Merge pull request #6100 from Combiler01/fix/paper-size-driver-5861
fix(print): auto-detect printer paper size and add page setup sync (fixes #5861)
2026-11-10 18:18:09 +01:00
FredApps e03d5a1c29 Render: an isolated CMYK group of any size blends in CMYK, in bands (#6885)
* 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
2026-11-10 18:11:33 +01:00
Combiler01 6b51ed655a Merge remote-tracking branch 'upstream/main' into fix/continuous-scrolling-default-6780 2026-11-10 22:20:38 +05:30
8559306f77 Do not send duplex to printers without two-sided support (#6673)
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>
2026-11-10 17:48:48 +01:00
FredApps 1c1a5d746e Merge main and retain grayscale spooler conversion 2026-11-10 17:42:46 +01:00
Combiler01 9ec3c48f84 fix(ui): preserve viewport anchor on gaps change and guard set_fit 2026-11-10 22:06:54 +05:30
FredApps 6748830620 test(print): assert idle prerequisites before testing worker wake 2026-11-10 17:36:40 +01:00
FredApps 4bbfd51718 test(print): isolate the options worker wake from printer discovery 2026-11-10 17:35:24 +01:00
FredApps fe70ecce73 Print in grayscale prints on a Windows driver with no grey option (#5806) (#6863)
* 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)
2026-11-10 17:31:12 +01:00
Combiler01 ac46a4a26c fix(ui): handle Fit::None with set_zoom in preferences fit_changed 2026-11-10 21:41:18 +05:30
Kyle KassenandFredApps 919db9bd51 M10: preserve NoRotate appearance geometry in vector printing (#5036)
* M10: preserve annotation placement precision (#4995)

* M10: preserve NoRotate and implicit Text print orientation (#5029)

Keep existing printable appearances upright on rotated source pages, including Text annotations whose NoRotate flag is clear.
Preserve original flags and appearance streams, print eligibility and outer page transforms.

* M10: clarify NoRotate verification docs

---------

Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 17:04:04 +01:00
Grzegorz MartensandClaude Opus 5.5 4f107a6f40 M8: a form /Matrix through a reference to a reference is refused
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
2026-11-10 16:45:34 +01:00
Grzegorz MartensandClaude Opus 5.5 cf26980471 M8: a long form /Matrix is read cheaply, an unreadable one leaves layer removal alone
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
2026-11-10 16:45:34 +01:00
Grzegorz MartensandClaude Opus 5.5 d2c87ae789 M8: the redaction walk reads a form's /Matrix entries by reference
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
2026-11-10 16:45:34 +01:00
FredApps eaff44e4d7 Merge main and retain one printer for paper and duplex discovery 2026-11-10 16:45:09 +01:00
FredApps 257c026c8a fix(print): guard paper discovery and retain explicit custom stock 2026-11-10 16:42:49 +01:00
6cded6a5a3 M10: a booklet's both sides follow a driver that keeps its own two-sided setting (#5772) (#5903)
* 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>
2026-11-10 16:34:48 +01:00
Combiler01 4f16190e52 fix(ui): apply continuous scrolling and view defaults to active and remembered documents (#6780) 2026-11-10 21:01:34 +05:30
0e4b9171c0 fix(ui-egui): keep hotbar picker stationary while resizing (#6513)
* 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>
2026-11-10 16:24:33 +01:00
3bcaacc272 fix(print): keep the localized reset label off the properties panel title (#5550)
* 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>
2026-11-10 16:19:34 +01:00
FredAppsandKyle Kassen afca38ad71 M10: retain printed artwork with indirect annotation metadata (lands #4668) (#6855)
* M10: retain printed artwork with indirect annotation rectangles

* M10: resolve indirect annotation appearance states when printing

* M10: follow bounded annotation reference chains when printing

---------

Co-authored-by: Kyle Kassen <85040622+KyleKassen@users.noreply.github.com>
2026-11-10 16:05:43 +01:00
Grzegorz MartensandClaude Opus 5.5 a7f0074381 M8: doc_hidden_info and the parity note say what the refusal covers
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
2026-11-10 15:55:59 +01:00
Grzegorz MartensandClaude Opus 5.5 f2d64ca136 M8: redaction refuses a page whose content it can't fully parse
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
2026-11-10 15:53:40 +01:00
905e9abf6f fix(forms): ignore print requests from automatic field scripts (#4166)
* fix(forms): ignore print requests from automatic field scripts

* docs(contributors): opt in to GitHub profile name sync

---------

Co-authored-by: enriquemelzer-bit <319226743+enriquemelzer-bit@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 15:50:16 +01:00
Harsh Verma de24a4ddba Merge branch 'storytold:main' into fix/paper-size-driver-5861 2026-11-10 19:58:34 +05:30
Kyle KassenandFredApps debb826f29 M10: preserve annotation placement precision (#4995) (#5002)
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 15:17:15 +01:00
Combiler01 1427a114e5 Merge remote-tracking branch 'upstream/main' into fix/paper-size-driver-5861 2026-11-10 18:56:42 +05:30
Combiler01 ebc9de101b Merge remote-tracking branch 'upstream/main' into fix/paper-size-driver-5861 2026-11-10 18:46:05 +05:30
Grzegorz MartensandClaude Opus 5.5 ac1eba1092 M8: the form and hidden-text callers are tested without a Type 3 font too
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
2026-11-10 14:10:19 +01:00
Grzegorz MartensandClaude Opus 5.5 506502386e M8: the Type 3 cleanup charges the page only when there is a Type 3 font
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
2026-11-10 13:59:52 +01:00
BFlat 1a159c57fc Merge pull request #6820 from storytold/merge/print-snapshot-area-2491
Print a selected area from Take a Snapshot (lands #2491)
2026-11-10 19:58:04 +07:00
BFlat 4ddf52ef90 Merge pull request #6824 from storytold/merge/egui-725a39fc-6699
Bump storytold/egui to 725a39fc (lands #6699)
2026-11-10 19:23:37 +07:00
huppifluppandFredApps 8864903795 M10: Recognize text no longer clips the first and last letters of a line (#5058)
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 19:23:21 +07:00