Commit Graph
5709 Commits
Author SHA1 Message Date
Dylan M. Taylor 785e7e510c fix(forms): finding the first character a simple font cannot hold encodes the whole string once, then each distinct character once (#7047) 2026-11-13 12:23:58 +01:00
Dylan M. Taylor b167422b2e fix(sign): every signature's VRI entry shares the store's evidence arrays instead of copying them (#7038) 2026-11-13 12:22:54 +01:00
Dylan M. Taylor b0607f5cd3 fix(optimize): a destination name given by reference is copied once however many dictionaries name it (#7035)
* fix(optimize): a destination name given by reference is copied once however many dictionaries name it

* fix(optimize): aliases of one destination-name object are copied once
2026-11-13 12:21:26 +01:00
FredApps 0c15e3de71 Merge pull request #7115 from jonathanwxh-cell/fix/measure-compound-number-formats
fix(measure): read compound and fractional measurement number formats (ISO 32000-2 12.9.2)
2026-11-13 10:38:39 +01:00
FredApps 623c53f887 Merge pull request #7073 from mtlcl410/fix/test-moddate-second-boundary
fix(tests): saves compared byte for byte don't fail when the clock crosses a second between them
2026-11-13 10:37:57 +01:00
FredApps eb2a2eddac Merge pull request #7056 from mtlcl410/perf/form-edit-reads-fields-once
M7: a form edit reads the document's fields once (form.fill-text)
2026-11-13 10:37:09 +01:00
FredApps a080e1ff27 Merge pull request #7053 from mtlcl410/fix/lone-sign-token
fix(hayro-syntax): a lone sign in a dictionary or array is unreadable, not a panic
2026-11-13 10:32:49 +01:00
FredApps 6d2c006f02 Merge pull request #7051 from mtlcl410/fix/js-session-test-worker-cap
fix(js): each unit-test thread counts its own document script workers
2026-11-13 10:29:31 +01:00
asadosteaksnlandasadosteaksnl 175992b5a2 fix(ui): Recognize Text's settings scroll in a short window, its buttons stay in reach (#6599) (#6760)
* fix(ui): Recognize Text's settings scroll in a short window, its buttons stay in reach (#6599)

On a small screen or at a large display scale the dialog was taller than the window: it was
centred with its title and its Cancel / Recognize text row off the screen, and nothing scrolled.

The four settings groups (Pages, Settings, Skip, Output folder) now sit in a vertical scroll area
capped to the window's height, as Preferences does (#3557). The title, the engine notice and the
button row stay outside it, so they are always on screen; when the settings fit, nothing changes.

The test opens the dialog in 400 to 700 pt windows and checks the title and both buttons are on
screen, and that the last group scrolls in between the title and the buttons while they stay put.
Without the fix it fails at 400 pt with the title at y = -267.

* contributors: credit asadosteaksnl as Kas

* fix(ui): Recognize Text's settings scroll in as little as 40 pt, so its buttons fit the smallest window (#6599)

A 120 pt floor still pushed the dialog off a window shorter than about 340 pt: at 173 pt (the
smallest window, 520 pt, at the largest interface size, 300%) the title sat at y = -32. The test
now checks 173, 208 and 260 pt windows too, as the Preferences layout test does.

* test(ui): the short-window Recognize Text test clicks Cancel and checks a 640 pt wide window (#6599)

Cancel being on screen isn't proof it can be clicked: the test now clicks it at every height and checks the dialog closes. The window is 640 pt across, a 1280-pixel screen at 200%, and every control is checked to fit across as well as down.

* test(ui): the Recognize Text title stays put while the settings scroll (#6599)

* Revert "contributors: credit asadosteaksnl as Kas"

This reverts commit 52470f66a7.

* docs(ui): document the Recognize Text dialog's body (#6599)

* test(ui): the Recognize Text test also covers tall windows where nothing scrolls (#6599)

The dialog is about 935 pt tall, so it scrolls up to a 900 pt window; at 1100 pt everything fits. Both now run, so the test also pins the case where the settings fit and the layout is unchanged.

---------

Co-authored-by: asadosteaksnl <353856209+asadosteaksnl@users.noreply.github.com>
2026-11-13 02:42:37 +01:00
FredApps 330cde8035 Merge pull request #5559 from vangel76/feat/recent-reveal-5452
Reveal recent files in the system file manager from Home (#5452)
2026-11-13 02:19:00 +01:00
mtlcl410andClaude Opus 5.5 85d4257523 Merge upstream/main into fix/js-session-test-worker-cap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyGTPYerQyLLrnydVDA
2026-11-13 04:35:23 +08:00
mtlcl410andClaude Opus 5.5 cd1d7160b0 Merge upstream/main into perf/form-edit-reads-fields-once
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyGTPYerQyLLrnydVDA
2026-11-13 04:24:08 +08:00
FredApps bffa2d58c0 fix(scripts): a batch the redaction checks refuse discards its script globals
After the edit ran, a batch can still fail the redaction checks: a
deleted marked page alongside added pages, or text still shown under a
mark. Both returned without discard_edit_callbacks, so globals (and now
timers) the batch's field scripts left stayed in the document's realm
while the document rolled back. Every other refusal after the edit
already discards them.

Test: a_batch_a_redaction_check_refuses_discards_callback_globals. A
keystroke script sets a global, and a batch that sets the field and
deletes the marked page while adding one is refused. Without the fix the
global is still there ("number" for "undefined").
2026-11-12 19:27:38 +01:00
FredApps 522fa442f5 Merge main into port-4751
vendor/README.md conflicted: main took hayro-interpret patch 104 (RGB
colours in isolated DeviceCMYK groups). This branch's viewer overrides
become patches 105 (annotation_overrides) and 106 (appearance_overrides)
in the README, in vendor/hayro-interpret/src/interpret/mod.rs, in the
raster test and in docs/acroform-script-workflows.md.
2026-11-12 18:15:19 +01:00
120c62fb82 Auto-tag Apply tags headings by size and weight (#7062) (#7069)
* feat(a11y): auto-tag Apply tags headings by size and weight

Apply now chooses H1-H6 or P per text object instead of P for all.
The body style is the half-point size showing the most characters (bold
when most of those are bold). A short object (at most 3 lines, 200
characters, with a letter) is a heading when it is 1.15x the body, or
bold at 0.9x the body over a regular body in at most 2 lines. Heading
styles rank across the document, larger and bolder first, capped at H6,
and a level never skips past the one before it so heading-nesting
passes.

Size is Tf x Tm x CTM with q/Q tracked; bold is Metrics.bold. Text in
render mode 3 (an OCR layer) is not counted. Font metrics are measured
once per font object per document, at most 256 fonts.
AutoTagResult gains a headings count. Parity, gaps, roadmap and the
apply tool's note say headings are tagged.

Fixes #7062.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(a11y): rank heading styles by map and skip clip-only text

Look up each heading style's rank in a map built once after sorting,
so ranking is linear in the number of text objects. Text in render
mode 7 (clip only) paints nothing, so it is excluded from heading
evidence like mode 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 17:58:31 +01:00
490287fc9d prc: rebuild compressed tessellations whose normals are recalculated (#1297) (#7034)
* prc: rebuild compressed tessellations whose normals are recalculated

A TESS_3D_Compressed (WD 7.8.9.7) was read to its end but never rebuilt,
so prc-render drew nothing for it. Rebuild the form whose normals are
recalculated, with three edge statuses per triangle, into a Mesh grouped
by face and styled by each face's line attribute.

The walk (tess/compressed.rs) takes a seed, then one apex per triangle,
from a reference or the next point in the frame of the edge it continues
across (WD 7.8.9.2), with the WD 12.3 Length, Unitize and MakeOrthoRep
arithmetic bit for bit. Arrays that do not fit, or an edge shared by more
than two triangles, leave the tessellation Compressed. Every triangle,
style and face range is charged to the element allowance first.

Stored-normals meshes, the one-status-per-triangle form, the planar bend
check, the best-fit search and partial salvage are later PRs.

Adapted from pdfcer (MIT, same author) at 0a249a73.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* prc: charge rebuilt compressed meshes after they fit; model_render test

The rebuilt triangles, their styles and the face ranges are now charged
to the element allowance once the arrays fit and before the face ranges
are made. Arrays the walk cannot fit stay Compressed and spend nothing,
so they no longer fail the file over a face count they never use.

Tests:
- a_rebuilt_compressed_mesh_is_charged_its_triangles_and_faces pins the
  exact charge (4 triangles x 3 elements + 2 faces);
- arrays_that_do_not_fit_spend_no_triangles_or_faces;
- rebuilt_triangles_are_grouped_by_face_with_their_styles, a direct
  by_face test with out-of-order faces, empty faces and styles;
- model_render's a_compressed_prc_model_is_rebuilt_and_drawn, end to end
  from a PDF, cited in misc.3d-view; its hex fixture is the prc-render
  builder's output, kept so by the_automation_fixture_is_the_compressed_triangle.

The walk's working tables are documented as bounded by the triangle
count rather than charged. The module docs now say the arithmetic
matches the WD pseudocode as read and that the traversal rules are
inferred, not WD text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* prc: charge compressed styles and face ranges before making them

A rebuilt compressed mesh's per-triangle styles were built before the
budget was charged, and also when the normals were stored and the mesh
was read past. The face attributes are now kept as read, and the styles
are built only for a rebuilt mesh, after the charge.

Each face range is a Range<usize>, two budget elements on 64-bit
targets, so it is charged at its size, not as one element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* prc: bound the compressed rebuild walk by the allowance left

The walk's vertices, triangles, edge counts, edge log and stack were
allocated before anything was charged, and could hold several times the
decoded arrays. The walk now gets the elements the reader has left and
stops once its arrays, by length, would hold more; the tessellation then
stays compressed, as when its arrays do not fit the walk.

A face whose line attribute is 0 but whose behaviour is not now styles
its triangles, so the behaviour bits are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* prc: say the rebuild walk is bounded by the allowance

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* prc: cap the rebuild walk's first reservation by its limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 17:58:14 +01:00
72c607fc47 fix(render): an RGB colour in an isolated DeviceCMYK group blends in CMYK, as its ink (#7031)
* fix(render): an RGB colour in an isolated DeviceCMYK group blends in CMYK, as its ink

Part of #4887. An isolated DeviceCMYK group blended in CMYK only when every
colour in it had process tints (DeviceCMYK, DeviceGray); one RGB fill sent
the whole group back to RGB blending.

Vendored hayro-interpret patch (104): Color::ink_tints gives DeviceRGB,
CalRGB, Lab and three-component ICCBased colours the ink that draws back to
them through the bundled CMYK profile (new cmyk_inverse module: black
0.4*k0^2 + 0.6*k0^8 with k0 = 1 - max(r, g, b), matched to Acrobat on
synthetic pages; then cyan, magenta and yellow by at most 12 Newton steps
against the profile; one-entry per-thread cache). ICCBased CMYK still has no
tints.

Test an_rgb_colour_in_an_isolated_cmyk_group_blends_as_its_ink: 50% grey
under Difference with black within 10 levels of Acrobat's 137 112 106;
in-gamut colours draw as themselves; red within 8 of Acrobat's 238 32 37.
With the additive arms reverted the grey draws 93 97 96 and it fails.
#5228's control now uses an RGB image, which still has no tints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(render): an RGB colour with a non-finite component has no CMYK ink

`to_rgba` turns a non-finite component into some finite colour, so the
inverse could not see it and gave that colour's ink. `ink_from_srgb` now
declines such a colour and the group draws it as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 17:57:26 +01:00
jonathanwxh-cell 4439794769 fix(measure): validate written Y factor and check /F and /D on last format only 2026-11-12 22:05:28 +08:00
jonathanwxh-cell c2355b0b0e fix(measure): nudge whole units by a step of the last unit; keep Y's own units and CYX
A whole-unit nudge measured in the larger unit could exceed a step of the last unit (1 km - 0.9 mm showed as 1 km 0 m). A compound Y in units other than X's was relabelled with X's unit and lost its CYX when written back.
2026-11-12 21:36:03 +08:00
jonathanwxh-cell 87d8deaced fix(measure): read compound and fractional number formats (ISO 32000-2 12.9.2)
Scale::read refused number format arrays of more than one unit (feet and inches) and the /F, /T formats, so measuring inside an Acrobat-calibrated viewport failed and such saved measurements were listed as unsupported. Read them into Scale::formats, label readings with them (whole units, then the rest as decimals, a fraction, rounded or cut off; a last unit that rounds up carries) and write them back when a measurement is added with that scale. Single-unit decimal scales read and label exactly as before.
2026-11-12 21:28:38 +08:00
d749fd8062 Print colour management: the app converts printed pictures into a printer profile (#6269) (#6853)
* Print: app-managed colour converts printed pictures into a printer profile (#6269)

doc_print takes color_management (printer, the default, or app). With app,
color_profile (an ICC file or an installed profile's name), rendering_intent
and black_point_compensation convert each rendered page picture from sRGB
into the printer's RGB or grey profile before it is sent. app prints the
pages as pictures; CMYK and non-destination profiles are refused.
print_color_profiles lists the installed RGB and grey profiles.

The conversion is craft-cms at craft-print's rev, in pdfcraft_print::colour,
hooked into print_raster::rasterize. Raster carries the colour and is Clone
rather than Copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Print colour: look for installed profiles inside the read root when confined

With automation roots, print_color_profiles and doc_print's color_profile
name lookup read the system's colour folders, outside the roots. They now
look in the read root instead (as text_font_files does), check each file
against it before reading it, and are refused without read authority.
Under --root they look in the root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* automation: keep colour profile discovery inside --root, links followed

Under --root alone, print_color_profiles and the color_profile name
lookup listed the root's .icc files without checking each against the
root, so a link inside it could read a profile outside. Each discovered
file now goes through the same --root resolution as any other read
(links followed) before it is opened.

colour_profile_links_out_of_the_root_are_not_read makes such a link and
checks it is neither listed nor found by name; it fails without the
check (the link is listed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* print: as_chunks for the grey fill; area print keeps managed colour

Clippy 1.99 (`chunks_exact_to_as_chunks`) flags the constant-width
`chunks_exact_mut(4)` in the grey conversion; use `as_chunks_mut::<4>()`.

main's print-area path built `Raster` without the new `colour` field;
build it through `PrintDraft::raster()` so a printed area is colour
managed like a printed page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Lock craft-cms at the craft-libs commit craft-print now uses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(print): read only regular files as printer profiles, and no more than the cap

A FIFO or device named `.icc`/`.icm` in a profile folder was listed and
read, and opening a FIFO for reading waits for a writer. The listing now
keeps regular files only (links followed), `PrinterProfile::read` refuses
anything else before opening it, and the read goes through a limit one
byte past `MAX_PROFILE_BYTES`, so a file that grew after it was measured
is refused rather than read whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: KenM76 <KenM76>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 06:28:18 +01:00
mtlcl410 50ed2e55da fix(hayro-syntax): a failed typed read leaves an unreadable array slot in place
FlexArrayIter::next::<T> stepped over a lone-sign slot before finding that
null is not a T, so a caller that then read the slot as another type got
the item after it. The slot is now passed only when null is a T, as a value
of another type is left in place. The sign-token tests also require the
lone-sign fixtures to parse and check raw_iter's null slots directly.
2026-11-12 12:28:05 +08:00
mtlcl410 ae279a27f4 fix(tests): saves compared byte for byte don't fail when the clock crosses a second between them
The three script-refusal tests built on reset_order_session save, run a
refused edit or script, save again and compare the bytes. Each save stamps
/ModDate from the system clock, so the comparison failed whenever the two
saves fell in different seconds. The helper now pins the session clock with
Session::with_clock, as session_with and the other byte-comparing tests do.
2026-11-12 11:43:00 +08:00
mtlcl410 503a19467f M7: a form edit reads the document's fields once (form.fill-text)
The fields of a document are kept with the state they were read from
(pdfcraft_cos::ReadState), and a document that still reads as that state
gets a copy instead of a new walk. One value edit read them four times
on an AcroForm (fill, recalculate, refresh, display) and seven on a
laid-out XFA form (plus the data sync twice and the scripts), mostly with
nothing changed in between; it now walks them once, and twice on an XFA
form (once more after its data is written).
2026-11-12 07:47:54 +08:00
Dylan M. Taylor 1d2af9907b Fix Flatpak updates and bump to 0.21.1 (#7020)
* fix(flatpak): allow release checks and self-update helpers

* test: keep Flatpak permission parsing within strict Clippy rules

* chore: bump version to 0.21.1 for the Flatpak update fix
2026-11-11 18:38:09 -05:00
Dylan M. Taylor 80e26e674d fix(redact): remove a stroked path by the box its stroke paints (#5876)
* fix(redact): remove a stroked path by the box its stroke paints

Path redaction tested a path's construction box against the marks. A
horizontal or vertical stroke has a zero-area box, which overlaps
nothing, so its drawing commands stayed beneath the overlay; a wide
stroke whose points lie beside a mark but whose ink reaches into it
stayed too.

The interpreter now tracks the line width, miter limit and join style
(`w`, `M`, `j`, and `LW`, `ML`, `LJ` through `gs`) and, for a stroking
operator, grows the box by half the width (at least one unit), times
the miter limit when the path has mitred joins, scaled by the CTM's
largest stretch, before the hit test. The bound is conservative: a
stroke near a mark may now go where before it stayed.

* address review: pad hairlines by half a unit, rectangles for their corners, square caps by sqrt 2

* review: test a kept clip against the path as constructed

* address review: a line state set between a path and its stroke no longer lets the stroke skip the redaction test

* address review: line-state operators read the operand before the operator
2026-11-11 18:16:48 -05:00
Dylan M. Taylor 9f763636fd fix(forms,xfa): find every password field before a save skips scrubbing it (#5864)
* fix(forms,xfa): find every password field before a save skips scrubbing it

The quick Password-flag check in pdfcraft-forms stopped silently at its
depth limit, and visited the tree in an order where a field the full
field walk reaches could be cut off on a long branch first. It now
answers true (read every field) whenever it cannot look as far as the
field walk, and also walks below fields adopted from page widgets.

XFA password classification walked AcroForm fields only 32 levels deep,
shallower than editing and dataset synchronization, and read the
datasets packet of an XFA array without the prefixes its preamble
declares, so a typed password could stay in the saved datasets. It now
walks as deep as the field walk, refuses to clear (and so to save) when
the tree is deeper than it can check, and reads the packet the way the
datasets writer does.

* address review: check every page widget's /Parent chain for the password flag

* address review: password scan follows the parents of nameless /Fields entries; field-walk completeness checked before the datasets packet is read

* address review: the field walk's completeness is decided after every root; the subtree scan starts at the last dictionary of the parent chain

* address review: a skipped visit does not replace the reach a field was classified with
2026-11-11 18:16:31 -05:00
mtlcl410 b06bedefa6 fix(hayro-syntax): a lone sign in a dictionary or array is unreadable, not a panic
A lone + or ++ passes the first pass, which skips it as a number, but no
number is read from it (ISO 32000-2 7.3.3). Dict::entries and Dict's Debug
unwrapped the failed read and panicked, Array::raw_iter did too, and
Array::iter stopped there and dropped the items after it.

Vendor patch (46): a dictionary leaves such an entry out, as Dict::get
already did (7.3.7), and an array reads it as null in its slot (7.3.9). A
typed read still ends at it, as at any value of another type.
2026-11-12 07:14:32 +08:00
mtlcl410 795bc1144b fix(js): each unit-test thread counts its own document script workers
Parallel unit tests in one process shared the process-wide count of live
document script workers, so unrelated tests together reached the 16-worker
cap and failed at random with "document script worker limit reached".
Under cfg(test) each test thread now has its own count against the same cap;
a permit releases the count it was taken from. The production cap and its
process-wide count are unchanged.
2026-11-12 07:04:56 +08:00
vangel76andClaude Sonnet 5.5 08bf6ec278 Merge upstream/main into feat/recent-reveal-5452
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-11 22:24:02 +01:00
Dylan M. Taylor 1d7a9f9ffe fix(edit,a11y): font aliases share the metrics built for their font (#5998)
* fix(edit,a11y): font aliases share the metrics built for their font

Text extraction and heading extraction built and kept font metrics per resource name, so
many names for one font object each held a copy of its Unicode and width tables.

* fmt

* address review: font metrics are cached by the object at the end of the reference chain
2026-11-11 15:51:22 -05:00
Dylan M. Taylor 69936f1d1f fix(engine): run the renderer redaction check on redactions applied inside a batch (#5977)
* fix(engine): run the renderer redaction check on redactions applied inside a batch

* Resolve redaction marks by page object and measure the edited document

The renderer check looked marks up by the page index they had when they
were drawn, so a MovePages later in the same batch pointed them at another
page's text, and it measured glyphs with page geometry from before the edit.

Each mark now carries the page object it was placed on, and the geometry is
read back from the document as the batch wrote it.

* review: skip marks whose page the batch deleted

* review: group marks by page once and refuse an ambiguous page tree

* address review: refuse a batch that deletes a marked page while adding pages

* merge main, keep both

* address review: a batch that applies redactions and then copies a page is refused
2026-11-11 15:50:56 -05:00
Dylan M. Taylor abec4e3fee fix(js): bound array text, month parsing, name registration and list work (#5883)
* fix(js): bound array text, month parsing, name registration and list work

- strings() (field value and colour setters, resetForm) copied every array
  entry into its own String with no total: one long string listed a million
  times was copied a million times. The copies together are now held to the
  run's MAX_PAYLOAD byte budget.
- FormCalc parse_date copied and lowercased the rest of the text for every
  month token of the picture, quadratic in both; it reads at most the longest
  month name now.
- XFA global registration checked each child name against a Vec of every
  name defined so far, quadratic in a wide form; it is a HashSet.
- FormCalc gather charged one step for a whole collection and list_push
  charged nothing for a duplicate it dropped, so .all on same-named siblings
  did quadratic work for one step a pair; every node visited costs a step.
- add_instance reserved only the new root's SOM before copying a subtree;
  the copy's nodes, names and SOMs are charged to the byte budget first.

* address review: gather charges each call; check an array entry's length before copying it

* Hold an array entry to its widest escaped size before copying it

The pre-copy check compared the payload budget against a JS string's UTF-16
length, which is shorter than the escaped UTF-8 form it is copied to: a
string of quotes or non-ASCII text could allocate up to six times the
budget before the post-check rejected it.

The check now uses the widest escaped width, so the copy cannot exceed the
budget it is checked against.

* review: measure an array entry's escaped width before refusing it

* address review: count an array entry's escaped width without copying it

* address review: charge retained field text and colours to a run-wide budget

* fmt

* fmt

* merge main, keep both
2026-11-11 15:50:29 -05:00
Dylan M. Taylor 77078864f4 fix(render): charge inspection text (fields, tooltips, layers, bookmarks, blank comments) against shared allowances (#6015)
* fix(render): charge field, tooltip, layer, bookmark and blank-comment text against shared allowances

* address review: charge comment text once, keep JS and field-action flags past the budgets

* review: count an unlisted field's actions through its whole subtree

* review: mark multi-select values the allowance dropped

* review: count only real actions, walk each field once, keep name selections

* address review: revisit shared field descendants from shallower paths, charge value separators, check every widget for actions

* merge main, keep both
2026-11-11 15:50:02 -05:00
Dylan M. Taylor daec898d7c fix(sign): judge a shared signature value once per listing; dedupe DSS evidence by object number (#6041)
* fix(sign): judge a shared signature value once per listing; dedupe DSS evidence by object number

* address review: shared signature values are judged by output index without copying the dictionary; a dead evidence reference does not hide a live one

* address review: repeated evidence references are skipped before they are resolved

* address review: each full evidence reference is tried once
2026-11-11 15:49:35 -05:00
Dylan M. Taylor c73cccfb33 fix(redact): hidden-layer removal reads the default state as viewers do (#5867)
* fix(redact): hidden-layer removal reads the default state as viewers do

hidden_layers ignored an indirect /BaseState and, with /BaseState /OFF,
let /ON override /OFF, so a group both lists named survived as visible.
The OCMD and OCG classifier also read /Type directly, so a membership
dictionary with an indirect /Type was never evaluated and its content kept.

The default state is now /BaseState (resolved), then /ON, then /OFF, and
/Type is resolved before classifying a group or membership dictionary.

* address review: follow chained /Type and /BaseState references, hash-set ON/OFF lists

* address review: a visibility expression naming a group through an alias reference fails closed
2026-11-11 15:37:44 -05:00
Dylan M. Taylor 1e91e8a93e fix(forms): a read-only radio button is not picked or left through set_value (#6038)
* fix(forms): a read-only radio button is not picked or left through set_value

* address review: use /V as the selection in the read-only radio check, keep reset going

* address review: radio read-only checks judge each button by the state it would get, and cover FieldValue::Widget
2026-11-11 14:48:24 -05:00
Dylan M. Taylor ee367a4a1c fix(xfa): bound the name and path bytes data nodes and queued instances copy (#6032)
* fix(xfa): bound the name and path bytes data nodes and queued instances copy

* review: bound the name bytes of a whole data path and report each cap separately

* address review: the queued-instance allowance counts path segment buffers and the dedup copy

* address review: the queued path's spare capacity is counted
2026-11-11 14:48:10 -05:00
Dylan M. Taylor b52bc30328 fix(annot): count ink points as strokes resolve and cap the eraser's samples (#6001)
* fix(annot): count ink points as strokes resolve and cap the eraser's samples

Resizing and erasing a drawing resolved every /InkList entry without a total, so one
stroke array listed many times expanded freely; the eraser densified every segment into
up to 2,000 samples with no overall limit.

* review: refuse an eraser result that exceeds the stored ink point cap

* review: say the erasure would make the drawing too large

* address review: the eraser counts each stroke's final point against its sample total
2026-11-11 14:47:56 -05:00
Dylan M. Taylor 13d6367385 fix(redact): read each Type 3 glyph program once per cleanup (#5993)
clean_type3_font decoded and parsed every surviving glyph program of a
font, for every font. Many small font dictionaries sharing one
compressed CharProc multiplied that work without multiplying the stored
data, with a fresh allowance for each decode.

The resource names each indirect glyph program uses are now cached by
object across fonts (resource-name interpretation stays per font, since
each font prunes its own /Resources), and all decoding spends one
cleanup-wide DecodeBudget; past it the cleanup fails closed.
2026-11-11 14:47:42 -05:00
Dylan M. Taylor 87aebd1b24 fix(preflight): follow every /Next action and bound colour and intent work (#5926)
* fix(preflight): follow every /Next action and bound colour and intent work

Action chains followed an indirect /Next only when it named an array, and
skipped references inside a /Next array, so a forbidden action one indirect
hop behind an allowed one survived both verification and conversion; an
untyped dictionary with /P and /K was exempt from action checks whatever its
/S. Both are now followed and checked, and arrays are borrowed instead of
deep-copied per chain. Conversion treats a /Subtype /Widget without /Type as
an annotation, and takes forbidden annotations off their pages in one pass.

Device-colour inspection decoded each content stream with the 1 GiB
per-stream default and no total; it now shares one 1 GiB DecodeBudget and
reports unread content instead of treating it as clean. Type 3 CharProcs
shared by many fonts are read once, and declared() reads each output intent
once (at most 8).

* address review: no sRGB intent when colour couldn't all be read; count array reads in action chains; declare every output intent

* address review: cut only an over-long identifier; test with a filter that can't be read

* review: leave untyped structure elements out of the forbidden-action check

* merge main, keep both

* address review: a wide direct /Next array is refused before it is queued; queued references are resolved through aliases
2026-11-11 14:47:29 -05:00
Dylan M. Taylor fba22fc738 fix(optimize): the fragment search charges every operator of a group it reads (#6095)
* fix(optimize): the fragment search charges every operator of a group it reads

* address review: conflict markers removed; the fragment search charges every operator it reads to the production allowance

* address review: group reads are charged before a parse failure and count all four operators
2026-11-11 14:47:15 -05:00
Dylan M. Taylor 683ccc9fcc fix(optimize): walk shared resource dictionaries and name-tree kids once (#5956)
* fix(optimize): walk shared resource dictionaries and name-tree kids once

* test(optimize): restore the file around the appended tests

* address review: import ObjRef in the optimize audit module
2026-11-11 14:47:00 -05:00
Dylan M. Taylor d46d9daea5 fix(edit): editor font enumeration reads a face name without building font metrics (#6086) 2026-11-11 14:46:47 -05:00
Dylan M. Taylor 583ae29599 fix(sign): a widget removed from a page's inline Annots is not a comment change (#6013) 2026-11-11 14:46:34 -05:00
FredApps ee081d033e fix(scripts): keep timers past a refused script; page actions before timers
A script refused before it runs (too long, or nested too deeply) left the
realm untouched but reported no timers. The viewer keeps only the timers
a run reports, so a button or console script over the limit stopped every
page timer. The realm now reports the timers it still has on that path.

The viewer built the page actions' run and the timers' run before it
handled either, so a due timer ran before what the page's open action
asked for: on a page that goes on to another, before that page's close
action could stop it. The timers now run after the page actions, and only
if those left the page, the tab and the dialogs as they were; otherwise
they wait a frame. The focused field's editor text is taken before each
run, not once for both.

Tests: a_script_refused_before_it_runs_reports_the_realms_timers (js),
a_script_refused_before_it_runs_keeps_the_pages_timers (engine),
timers_wait_for_the_page_a_page_action_goes_to (ui). Each fails without
its fix.
2026-11-11 19:48:17 +01:00
Dylan M. Taylor f44ea8380d redact: a covered clip-and-paint path leaves an empty clip, not its outline (#6996) 2026-11-11 10:57:36 -05:00
Dylan M. Taylor 0b0519f5df fix(forms): deleting a field removes ancestors it leaves without kids (#6997)
* forms: deleting a field removes ancestors it leaves without kids

* forms test: full-save check, widget only in the sibling case
2026-11-11 10:57:05 -05:00
FredApps 223c1f02d4 fix(scripts): review fixes for page actions and timers
- A page action or timer whose edits are refused shows nothing it did:
  view changes go in only after apply_script_outcome accepts them.
- js_advance runs at most its 6,000-run cap; the last call only moves
  the clock (Session::run_timers_at_most).
- Field callbacks see the document's layers (getOCGs), also after a
  batch rebuilds the runner.
- The viewer's hit test uses what scripts show (shown_by_scripts): a
  frame they showed takes presses, one they hid doesn't.
- Print previews draw without the live annotation and appearance
  overrides.
- An appearance override naming no form (a cleared icon) draws nothing
  instead of the button's own face (hayro patch 105).
- Page actions and timers wait while a dialog or prompt is open.
- A timer editing the focused field refreshes its untouched draft.
2026-11-11 15:53:02 +01:00
Dylan M. Taylor a23b7c7f73 fix(redact): apply marks with an indirect /Subtype and judge inline annotations (#5849)
* fix(redact): apply marks with an indirect /Subtype and judge inline annotations

A Redact annotation whose /Subtype is an indirect reference was passed
over by apply although the audit, which resolves it, reports it as a
mark: the other marks applied and the content under that one stayed.
Annotation dictionaries written in place in /Annots were kept whatever
they covered, and an inline reply or pop-up kept a removed comment
reachable through /IRT or /Parent.

/Subtype is now resolved wherever redaction classifies an annotation.
Inline entries are judged by their rectangle and relationships like
referenced ones, on the marked page and across pages, and the pop-up an
annotation names (/Popup) goes with it.

* address review: an inline widget's /Parent field goes even when no referenced widget names it

* fmt
2026-11-11 09:17:32 -05:00