A line goes to the first level whose style it is in, so the same style
for two levels, or one within the size tolerance of the other, left the
later level empty without a word. The edit now refuses it, for the dialog
and automation alike, naming the two levels (CodeRabbit on #4560).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
Deleting one widget took it out of the field tree through its /Parent,
which a damaged file can leave out or point at another field; the group
kept the widget in its /Kids with no page showing it, and the delete
reported success. The field that owns the widget drops it from its own
/Kids as well (CodeRabbit on #4631).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
# Conflicts:
# crates/automation/src/lib.rs
# crates/engine/src/lib.rs
# crates/ui-egui/src/lib.rs
# crates/ui-egui/src/panels.rs
Main's module layout (#4889) moved what this branch changed: the tools to
automation's dispatch.rs (the levels parser to json.rs), the edit to
engine's edit.rs, edit/labels.rs and edit/run.rs, Document::text_styles
to document.rs, the dialog state to state.rs and app.rs, and the menu
item to panels/right.rs and panels/bookmarks.rs. The dialog module joins
the others in dialogs/. Main's craft-menu bar (#4752) lists its commands
by hand, so bookmark.from_text_styles is added after
bookmark.from_structure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
The pre-signing check matched any folder named Program Files, so it rejected
a valid MSI when Windows resolves Program Files elsewhere, accepted the 64-bit
root for an x86 package and accepted a different app name. Build the expected
folder from the package architecture's resolved root and the policy name.
Review of #4898 (CodeRabbit): a shading clearing in another blend-mode
pass cleared its whole drawing path, so a small mesh triangle erased the
earlier elements of its pass around it. The same happened in the
shading's own pass, and in any knockout group, because a knockout paint
is copied over the path and a mesh texture is transparent outside its
triangles.
A shading in a knockout group now goes through knockout_shading: the
shading at full opacity, composited with Compose::DestOut, clears its
own shape (mesh triangles, /BBox), then in its own pass it is painted
with SrcOver. A shading that paints nothing still clears nothing. The
clear uses the shading without the fill opacity it carries, so a
half-transparent shading still replaces all of what is under it.
Test: it::knockout_backdrop::a_small_mesh_replaces_only_what_is_under_its_triangle
(own pass, another pass, isolated group). Revert-proven per half; keeping
the opacity in the clear fails
raster::tests::knockout_groups_replace_what_their_elements_cover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
When the source radio field is also its widget, the join removed the
field-only keys but kept its whole /AA, so field triggers (K, F, V, C)
stayed on a dictionary that is now only a widget. Split it as
split_merged does for the target: keep the widget triggers, drop the
field's, and remove /AA when nothing is left.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Export > Image spent its time in the PNG, JPEG and TIFF encoders, one
page after another: on a 55-page manual at 150 dpi, drawing every page
took about 0.34 s and encoding it 1.1 s (PNG) to 2.0 s (JPEG).
Exporter::images exports a list of pages, drawing and encoding several
at once on worker threads, each with its own PageRenderer (as
automation's extract_parallel does), and hands the files to a sink in
page order on the calling thread. Workers draw at most one page ahead
of the sink and fewer run when 1 GiB can't hold that many of the
largest page. The first page or sink error stops the export with every
earlier page handed over, as before. A page panic becomes that page's
error, and a panicking sink stops the workers. One page, one core or
wasm keeps the per-page loop. Export > Image and the doc_export_images
tool use it.
Files are byte-identical. One core is left for the interface (at most
8 workers). With examples/export_bench, on a Ryzen with 16 threads
(shared), mean of two medians of 21 runs:
PNG 150 dpi 1874 -> 340 ms (-82%)
JPEG 150 dpi 2106 -> 340 ms (-84%)
TIFF 150 dpi 2032 -> 375 ms (-82%)
PNG 300 dpi 3804 -> 590 ms (-84%)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_props chose /TI inside the field-dictionary update, from the field
read before the edit, so a font size or look changed in the same call
was not counted: at a larger size fewer rows fit and the selection
could end up below the visible rows. The field and widget top rows are
now chosen after every layout change, from the field read again,
before the redraw.
Test: a_list_boxs_top_row_follows_a_font_size_changed_with_its_items
(fails with the old ordering: rows 1..2 shown, selection on row 3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review follow-ups on #4977:
- ui-egui: after a rename, the Properties dialog re-selects the edited
widget by object, not by its old index. A button that joins a group is
appended after the group's own buttons, so the old index selected the
group's first button and a nudge moved the wrong one.
- split_merged (join and Duplicate): the new widget takes /A and the
widget triggers of /AA (E X D U Fo Bl PO PC PV PI); the field keeps
K F V C.
- join: a clashing export value searches at most taken + 1 ChoiceN
names, so one is always free, and the names are chosen before
anything is written.
Tests: a_joined_merged_groups_button_keeps_its_actions,
a_joining_button_gets_a_free_export_value_when_choice_names_run_long,
a_radio_button_joining_a_group_by_name_stays_selected (ui-egui). Each
fails with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review of #4898 (FredApps):
- An image or nested group of another blend mode than the pass's composited
its layer at opacity 0, on the assumption that its shape was cleared first.
`knockout_image` skips images with an `/SMask` and stencil masks, so a
white Difference square stayed under a blue Normal masked image in the
Difference pass and turned it yellow. The layer now composites with
`Compose::DestOut` at full opacity and no soft mask, clearing exactly its
coverage.
- In another pass (and an erase pass), a shading cleared under its path
before the mesh-extent check that makes an off-page mesh paint nothing,
so a Difference `sh` of such a mesh erased the blue under it. The clear
now makes the same decision (`shading_bbox`, `mesh_area`, shared with the
paint path).
Tests: `it::knockout_backdrop` gains a masked image, a stencil and an
off-page mesh case; each fails with its half of the fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Real msiexec runs (per-user and all-users, silent and wizard) found two defects
that the metadata fixtures cannot see:
- "Only for me" published ALLUSERS=2, so FindRelatedProducts treated the run as
per-machine and skipped the existing per-user copy. An interactive upgrade
left two registered products and uninstall left files behind. The page now
leaves ALLUSERS unset for a per-user choice.
- Windows Installer resolves Program Files once, for the context it started in,
so choosing "Everyone" in the wizard installed into the user profile with a
machine-wide registration. The wizard now sets the new-install folder itself
after the choice (Program Files, Program Files (x86) for 32-bit, or the
per-user Programs folder). Silent installs are unchanged.
check-install-policy.ps1 now fails if either regresses.
A widget's own /TI is read before the field's, so each one scrolls by its
own top row and size (ListRows::top_showing). Before, the widgets were only
moved, all to the first widget's new row, when the first widget itself
scrolled: a second widget scrolled past the remaining options, or away from
a new value the first already showed, kept its stale row.
Test each_widget_with_its_own_top_row_scrolls_by_itself covers both
set_props (options shrink) and set_value; restoring the first-widget gate
in either caller makes it fail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
/V is inheritable like /DV (ISO 32000-2 §12.7.4.1). Clearing a text or
choice field's value removed its own /V, so a parent field's /V showed
through: Reset Form on a kid with no default refilled it with the parent's
value. When an ancestor sets /V, an empty value is now written instead of
removing the key. Check boxes and radio buttons already write /Off.
Test clearing_a_kids_value_does_not_bring_back_its_parents covers a text
kid and a list box kid through reset and through set_value; each half of
the fix was switched off in turn and the test failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
/DV is inheritable (ISO 32000-2 §12.7.4.1). set_props removed a cleared
/DV, which brought back a parent field's, so Reset Form restored the
parent's default. When an ancestor sets /DV, a cleared default is now
written empty (/Off for buttons, () otherwise); with none it is removed
as before. New tree::ancestor_sets, cycle-safe and capped.
Adapted from pdfcer (MIT, same author) 6af2773ccf599b416fab379673119c6b37c55a2c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Changing a list box's options could leave /TI (the first visible row)
pointing at a row that moved or no longer exists. A widget's own /TI is
read before the field's, and one past the end draws no options at all.
set_props now runs the same ListRows::top_showing rule set_value uses
(ISO 32000-2 12.7.5.4): /TI stays put while the selected rows are still
visible, otherwise it scrolls to them, clamped to the last full page.
The field's /TI is updated, and so is every widget that carries its own.
The scroll helpers move out of set_value so both paths share them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
set_props rewrote /Opt (an items edit or "Sort items") but left /I, the
selected positions in /Opt (ISO 32000-2 §12.7.5.4), so /I named whatever
now sat at the old positions. It now re-points /I at the selected values'
new positions, by set_value's rule, and removes it when none is offered;
a field /TI past the end of the new list is removed.
Adapted from pdfcer (MIT, same author) 149db0662ecfe5b23a050cdb7513821da918d7cb.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review follow-up (CodeRabbit): the join read the target's /Kids only when
it was a direct array, so an indirect one was replaced by the moved
buttons alone and the group's own buttons were lost. It now reads
through the reference with tree.rs's list_of, as link() does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review follow-up. A clash is checked against the group's own values only,
and each clashing value gets one fresh ChoiceN shared by every joining
button that had it, so a button repeated on another page stays in unison.
The group's value selects a moved button only when one of the group's own
buttons has it. A joining field that is its own widget leaves the
calculation order too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Renaming a radio button to an existing radio group's name was refused as
a duplicate, so buttons placed separately could never become one group.
set_props now moves the renamed field's buttons into the named group
(ISO 32000-2 §12.7.5.2.4: a group is one field whose widgets are its
buttons). A clashing export value is renamed to the first free ChoiceN
in /AP /N, /D and /R; a moved button is on only if its value is the
group's value; merged field+widgets are handled on both sides (the
target split via split_merged, now shared with duplicate_field).
Refused when either group has /Opt, has no button, or the target is
locked.
Adapted from pdfcer (MIT, same author) 79870b56 and 116f48d3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review follow-up: an offset equal to the end of the info header is only
right when no bitfield masks or colour table come after it; otherwise the
pixels were read from the masks or the table. It is now repaired like an
offset inside the headers. Offsets strictly inside the masks or table are
still taken as given (they may come from an overstated biClrUsed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Review follow-up: the repair reads the 40-byte header's compression and
colour count, which a 16-byte OS/2 header does not have. New cases pin
a 12-byte core header (3-byte palette entries) and a biClrUsed of u32::MAX.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
LiveCycle Designer wrote BMPs whose file header gives bfOffBits = 0. image
(0.25) seeks to that offset as given, so the pixels were read from the file
header on and the picture was drawn shifted and wrapped around (#4937).
`pdfcraft_xfa::image::bmp_with_pixel_offset` recomputes the offset from the
info header (its size, BI_BITFIELDS masks in a 40-byte header, the palette)
when the declared one lies inside the headers and the computed one fits in
the file; anything else is passed through unchanged and not copied. XFA
pictures (size and decode) and `create::from_images` both go through it, so
pdfcraft-create gains a dependency on pdfcraft-xfa (L4 -> L3).
Fixes#4937
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9