crates/render/src/raster.rs: main's tests plus the PR's #2970 test after the knockout test
vendor/README.md: main's rows with the PR's rewording of hayro patch 18
crates/render/src/raster.rs: both new tests (#2975 stencil masks, #2971 knockout groups)
vendor/hayro/src/renderer.rs: set_paint returns main's PaintSetup; the PR's knockout erase early return wraps its clip in PaintSetup::Clip
vendor/README.md: main's rows plus the PR's hayro-interpret patch 39 and hayro patch numbered 22 (19 is #3296's), with tests
vendor/hayro-interpret/src/pattern.rs: ShadingPattern::new takes the default colour spaces (#3018) and no opacity (this PR); without_opacity (#2977) and set_alpha both kept
vendor/hayro/src/renderer.rs: main's PaintSetup::Clip match with the PR's fill paint (is_stroke false) and comment
vendor/README.md: main's row plus the PR's hayro-interpret patch and test, numbered 46 (35 is #3296's)
vendor/hayro-interpret/src/encode.rs: both sides; a pixel sampled by either test (corner held, or centre covered) widens the mesh extent from #3296
vendor/README.md: main's row plus the PR's patch 36 and its test
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: calculation order: read, check, fix and set the order fields calculate in
A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.
Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: Prepare a form ▸ Calculation order: a page to reorder, check and fix the order fields calculate in
The calculated fields in the order they calculate (/CO), each with its
formula and the value it shows, moved with Up/Down arrows (as Acrobat's Set
Field Calculation Order), Alt+Up/Down or by dragging. A card says which
fields read one calculated later, which read each other in a loop and which
/CO leaves out, and holds Fix order; a loop's card opens one of its
formulas. On the page, arrows show what the field in focus reads and what
reads it, with each linked field's step. The order is a page of the Prepare
a form panel, opened from its Fields tools, with a back arrow to Prepare a
form; opening it ends Preview and closes the page grid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form.calculation_order runs form_calculation_order headlessly
The command had no tool linked, so command_list showed none for it and
command_run refused it. form_calculation_order is now its tool: run without
a change, it reads the order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Pin third-party actions to full-length commit SHAs (with version comments)
instead of mutable tags/branches to prevent supply-chain tampering via
retagged releases. Resolves SonarCloud rule githubactions:S7637.
Retain the calculation-order update and inline-image repair entry.
Position the repair entry away from concurrent top-of-log additions;
parser and render regression code remain unchanged.
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: calculation order: read, check, fix and set the order fields calculate in
A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.
Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled civil_from_days/days_from_civil copies and the
leap-year/month math around them (log timestamp, PDF /Info dates, ASN.1
UTCTime/GeneralizedTime, FormCalc Date/Num2Date, the Combine modified-time
stamp, the form date picker, and the demo-PDF timestamps) with chrono as a
workspace dependency. The engine's existing chrono (clock) is lifted to the
workspace. Every output format is unchanged and pinned by tests. CLAUDE.md now
says time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Guard chrono date paths: no panic on overflow, no '+' on 5-digit years
Systemic chrono follow-ups to the UTC/calendar PR:
- logging::timestamp and combine_ui::utc_stamp dropped chrono's
From<SystemTime> (which unwraps internally and panics on a clock past
year 262143) for the fallible DateTime::from_timestamp, reading an
out-of-range clock as the epoch, as the old clamping code did.
- timestamp, utc_stamp, pdf_date and the xtask Stamp now write the year
with {:04} instead of %Y, which prefixes a '+' before years past 9999;
the old hand-rolled code wrote the plain zero-padded year.
- forms_ui calendar navigation and length, and formcalc days_in, use
checked_add for the December year successor so y + 1 can't overflow at
i32::MAX / i64::MAX (reachable from a parsed or persisted date).
Docstrings added to the chrono-touched civil_from_days and Time::from_unix
for the docstring-coverage gate. Tests cover the out-of-range-epoch and
10000-01-01 cases for each.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* Keep FormCalc dates exact past chrono's range
MAX_DAYS accepts day numbers up to 1e8 (years past 270000), but NaiveDate
stops near ±262143, so Num2Date(99000000) and Format fell back to
1900-01-01. days_from_civil, civil_from_days and days_in now have chrono
convert within the date's 400-year Gregorian cycle and add whole cycles
back (via i128), which gives the pre-chrono results for every year. Tests
cover accepted inputs past chrono's range on both sides of the epoch,
through Num2Date and Format, with values from the old formula.
* Bump rustls-webpki and webpki-root-certs for RUSTSEC-2026-0382/0383
rustls-webpki 0.103.15 -> 0.103.16 and webpki-root-certs 1.0.9 -> 1.0.10
(both semver-compatible, lockfile only) drop the distrusted emSign
intermediates and root. Only these two entries change; cargo deny check
advisories passes. Upstream main fails the same check.
* logging/cos/sign/combine/demo: keep extended-year dates, no epoch fallback
For consistency with the stricter no-epoch-fallback rule now agreed across the sibling
chrono PRs (wordcraft#3427, filmcraft#2662): `DateTime::from_timestamp(..).unwrap_or_default()`
changes a timestamp the old code converted past chrono's year 262143 range — it collapses
to 1970-01-01 instead of the real proleptic-Gregorian date. The FormCalc Num2Date path was
already fixed (FredApps approved); this applies the same fix to every remaining conversion.
Each site uses the 400-year Gregorian cycle (146_097 days) with chrono as the only calendar
logic: chrono converts the day within one cycle, whole cycles carry in the year (i128,
saturated into i64). PDF/ASN.1 format-specific year handling is kept exactly as the old code
had it. Verified the cycle result equals the old formula across a wide range and at the extremes.
- apps/pdfcraft/src/logging.rs `timestamp`: cycle `civil_from_days(u64)`; pre-1970 epoch clamp
and ms/time-of-day format kept. Pins: +10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
(was 1970); checked_add u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch).
- crates/cos/src/writer.rs `pdf_date` (i64): cycle converter, no year clamp (old code wrote `y`
as-is, so a negative year keeps its sign). Pins: pdf_date(10_000_000_000_000)="D:3188570520174640Z",
pdf_date(-10_000_000_000_000)="D:-3149180813061320Z".
- crates/sign/src/der.rs `Time::from_unix` (i64): cycle converter; the `year` field is a u32, so
the year is cast `as u32` (wrapping) exactly as the old code did — ASN.1 year width preserved
byte-for-byte. Pins: from_unix(10e12).year=318857, from_unix(-10e12).year=4_294_652_378,
from_unix(i64::MAX).year=219_250_468, from_unix(i64::MIN).year=4_075_720_767.
- crates/ui-egui/src/combine_ui.rs `utc_stamp` (u64): cycle converter; pre-1970 epoch clamp kept.
Pins: utc_stamp(10e12)="318857-05-20 17:46 UTC", u64::MAX/2 s="292277026596-12-04 15:30 UTC".
- xtask/src/demo_pdf/pdf.rs `Stamp::from_unix` (i64): cycle converter, no year clamp. Pins:
from_unix(10e12).iso="318857-05-20T17:46:40Z"/.pdf="D:3188570520174640Z",
from_unix(-10e12).iso="-314918-08-13T06:13:20Z".
cos/sign/xtask built, tested (pdf_dates_are_utc_and_byte_identical, der::tests::times,
stamps_round_trip), fmt and clippy clean. pdfcraft-ui-egui and the pdfcraft app can't link
here (unrelated LLVM intrinsic bug in the rten-gemm dependency, 1.98.1), so their logging and
combine_ui functions + tests were copied verbatim into a scratch crate with chrono 0.4.45 and
pass there; fmt --check clean on all five packages. CI builds these on all platforms.
* Don't build a year-318857 SystemTime unchecked in the logging test
Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
Fall back only when missing sample or terminator bytes cannot fit the
content-window budget, including retained header and separator overhead.
Preserve available deterministic boundaries and ordinary large streams.
Add oversized, exact-budget, overflow and large-stream regressions.
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* M7: lay out Format text as Acrobat's panel
Format text was a row of separate combos, a Load font button, an
Installed fonts combo, a swatch grid and labelled drag values. It now
follows Acrobat's arrangement: the font across the panel (the page's
fonts, the standard families, Load font… and Installed fonts in one
list, showing the paragraph's own font by name); size and a colour
swatch, then lists and an alignment flyout; bold, italic, underline,
superscript and subscript; line and paragraph spacing; horizontal scale
and character spacing. One panel serves an edited paragraph and added
text. What PdfCraft can't apply yet (lists, superscript and subscript,
paragraph spacing, spacing for added text) is shown disabled and says
it is planned.
Icons stay Lucide's: superscript, subscript, pilcrow and
move-horizontal are added from lucide-static 1.49.0 with attribution.
Eight new strings are translated in every catalogue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: parity notes for the Format text panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(vi): Superscript and Subscript for Format text
The Vietnamese catalogue came from main without the Format text panel's
Superscript and Subscript labels; vietnamese_covers_ui_literals lists them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: Format text takes any horizontal scale and character spacing again
Review of #2249: the panel's preset lists narrowed what could be set to
50-200 % and -1..5 pt, where the old fields took 10-400 % and -5..50 pt.
Each now has a number field over the old range again, with the list of
common values beside it.
Test: the_format_panel_takes_any_scale_and_character_spacing types values
between the listed ones and beyond them, saves and reopens, and compares
the paragraph with the engine's own rewrite. It fails without the field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: Format text's rows line their controls up and fit the panel
Raised in #3037: the size list sat lower than the colour swatch and the
alignment button beside it. egui lays a list out from the default control
height and grows it downward, so a row's shorter controls ended up off its
middle. Each row is now one control height and centres what it holds, at
a list's own height.
The spacing rows' columns are measured from what they hold, so the second
list no longer runs past the panel; the number fields' lists open from a
small arrow, which leaves the field the room its value needs.
Test: format_text_rows_line_up_and_fit_the_panel checks the size list is
centred with the swatch and alignment button and as tall as the font list,
that each spacing row shares one line, and that nothing passes the panel.
It fails without the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format text: choosing the family a font name reads as still changes the font
CodeRabbit on #2249: a Calibri paragraph is shown as Helvetica, so picking Helvetica left
look.family equal to look0.family and no family change was sent. The pick is now tracked
(LineEditor::choose_family) and counts as a change in style(), font_shown(), has_changes()
and the editor preview; Keep source fonts and an applied edit clear it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Format text: the reusable text style follows a selection only when its edit was applied
A refused UpdateContent used to leave the new style in app.text_style, so the next
added text inherited formatting the selected text never received.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jk4iYSh3mEd145niec77LF
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A "Tools" tab sat over the tool panel although nothing else shared its
group; Acrobat's tool pane has no such strip. Tools docked on its own is
now drawn bare. Grouped with another panel, or floating, it keeps its
tab so it can be dragged away again, and its side stays a preference
and a dock command. The docking test groups Inspector with Tools by
command, since there is no tab to drop on.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
The grid's size was kept per document view and reset to 100% for every document and every
start. Acrobat keeps the size last chosen, observed in its Organize Pages. It is now one value
for the app, saved with the other settings like Combine's zoom, and --grid-zoom sets it too.
A damaged saved value is ignored or clamped to 50-300%.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
* fix(render): read each object stream's index once (vendored hayro-syntax patch 22)
`XRef::get_with` parsed an object stream's whole index of `objnum offset`
pairs again for every object it fetched from that stream, so reading all of a
stream of N objects took N² steps. Firefox keeps every object of a page in one
stream: the pdf.js corpus file bug1978317.pdf (65,542 objects in one stream)
took 37 s to draw and 40 s to extract its text, and was a known timeout in the
robustness baseline.
The parsed index is now kept per stream, beside the decoded stream data it
indexes (`ObjectStreamIndexes`), and never cleared, as that data isn't. A
malformed index is remembered as malformed; `/N` or `/First` that can't be
read (a reference that can't be followed while another stream loads) is not
remembered. An offset past `usize` is malformed instead of wrapping. A poisoned
lock is still used: entries are inserted whole.
Release build, Linux: bug1978317.pdf draws in 0.5 s (was 37 s) and extracts
its text in 1.9 s (was 40 s), and leaves the baseline's known timeouts;
40,000 pages in one stream open in 0.2 s (was 54 s).
`raster::tests::objects_in_one_large_object_stream_are_read_in_linear_time`
times out at 20 s with the fix reverted and passes in 0.2 s with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* test(render): an object stream offset past usize is malformed, not a panic
Review follow-up on #2427. Mutation testing by mtlcl410 found that replacing
patch (22)'s `checked_add` in `read_object_stream_index` with a plain `+`
survived every test, although in a debug build that `+` panics on the very
input it guards against (and wraps in a release build). Their test pins it:
it fails with "attempt to add with overflow" under that mutant and passes on
this branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: the mode bar has no Read tab, as in Acrobat
Acrobat's mode bar is All tools, Edit, Convert and E-Sign: reading is what
the viewer does with no tool in use. Read mode (the panes hidden) stays a
View command and a default workspace. Tests that clicked the Read tab as an
ordinary mode-bar button use Convert instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: parity notes for the mode bar without a Read tab
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: the Read-mode tests enter through the View command
The mode bar no longer has a Read tab, so `read_mode_hides_the_quick_toolbar`
(ui.rs) and `switching_modes_keeps_the_current_page_at_fixed_zoom` (view.rs)
clicked a button that is gone, failing the test jobs on ubuntu-latest and
windows-latest. Both run `view.read_mode` now, as the other read-mode tests in
this PR do; the second is the test the `view.read-mode` parity entry cites.
`cargo test -p pdfcraft-ui-egui --test ui --test view`: 56 and 55 passed.
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): PNG export writes opaque pages as RGB at deflate level 3
`encode_png` wrote every raster as RGBA at the png crate's default level
(zlib level 6). A rendered page is always opaque, so the alpha channel only
made the file bigger and slower to write. An opaque raster is now written as
RGB, at deflate level 3 with adaptive filtering; a raster with translucent
pixels is still written as RGBA with straight alpha.
The files come out the same size, 3-5x faster (release build, Linux):
300-dpi scan drawn at 220 dpi 805 -> 167 ms 4.46 -> 4.47 MB
text page at 220 dpi 70 -> 40 ms 0.85 -> 0.89 MB
annual-report page at 150 dpi 179 -> 64 ms 1.41 -> 1.38 MB
This is the encoder behind Export > Image (PNG), `pdfcraft-cli render` and
Print as Image; the automation tools (`page_render` and the other page
previews, MCP included) had a copy of it and now call it instead, so png is
only a dev-dependency of pdfcraft-automation. Two automation tests that read
`page_render` output as RGBA now accept the RGB a page renders as.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
* fix(engine): PNG export rejects a raster of the wrong length
encode_png now checks that the buffer is exactly width × height × 4 bytes
(checked arithmetic) before choosing RGB or RGBA. The opaque path's
as_chunks::<4>() dropped a partial trailing pixel, so encode_png(1, 1,
&[10, 20, 30, 255, 0]) wrote a valid 1×1 PNG where the old RGBA encoder
returned an error.
Regression test export::png_tests::rasters_of_the_wrong_length_are_rejected
covers opaque and translucent buffers with a partial pixel, a pixel short,
a pixel too many, an empty buffer and a size whose byte count overflows; it
fails on the previous head at the reviewer's case.
Review: https://github.com/storytold/pdfcraft/pull/2425#discussion_r4281603405
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoEx9Nj8KC4WKDcf5YaokQ
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M7: the Edit tab turns on Edit text & images, as in Acrobat
Entering Edit boxes every paragraph and image on the page at once, so text
is editable with one click instead of first finding Edit text & images in
the tool list. The floating toolbar gets an Edit text & images button while
Edit is open or the tool is in use. Leaving Edit (or turning the tool off
there) applies a paragraph still open in the editor, as clicking away from
it does; left open, its page kept showing the paragraph's text removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: parity notes for the Edit tab turning on Edit text & images
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: the panel-reveal test looks for the Edit panel's own row
With Edit text & images on, its name is on the floating toolbar as well as in the panel, so
the label alone matched twice and no longer showed that the Tools tab was revealed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: lay out the Edit panel as Acrobat's
Under the Edit tab the panel is titled Edit, with Preferences and Close;
Modify page is a row of icon buttons (rotate, insert, delete, extract) and
Organize pages; Add content lists Text, Image, Header and footer, Watermark
and Link, with the rest under More; Other options links Combine files,
Redact a PDF and Prepare a form. The milestone banner and Ready chips are
gone; a planned item is muted and says when it ships. Every row still
comes from the catalogue. Edit text & images is on the floating toolbar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: Organize pages in the Edit panel opens the Organize workspace
It ran page.organize, which showed the page grid behind the Edit panel;
Acrobat replaces the panel with Organize pages' own. It now selects the
Organize tool, so its panel and the grid open together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: translate the Edit panel's Less and Other options
Every catalogue gets the two strings the Acrobat-style Edit panel adds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(i18n): refresh the catalogue hashes in ATTRIBUTION.toml
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: no idle selection hint above Format text in the Edit panel
"Shift-click to select several objects. Drag empty space to select an
area." sat at the top of the Edit panel whenever Edit text & images was
on, pushing Format text down; Acrobat's panel has no such line. The
selection count and how to move it still show while objects are
selected. Tests that read the hint as "nothing selected" check that no
selection count shows instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: let auto-deref find the Edit panel's buttons (clippy)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: the Edit panel keeps main's Add / Update / Remove menu
Main gave Header and footer, Watermark and Background a menu of Add,
Update and Remove in the tool panel (#1857). This branch draws the Edit
group in its own panel, so those rows ran only Add and Update / Remove
were unreachable there. Both panels now share one helper for the caret
and the menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Edit panel: Modify page buttons follow command enablement; Organize pages with no document asks for a file
CodeRabbit on #2245: the four Modify page buttons were enabled from the catalogue
(always Ready) while execute() refuses them on a document that forbids page assembly;
they now use command_enabled. Organize pages with no document open selected a panel
with no grid to show; it now runs page.organize, which asks for a file first.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Calls storytold/craft-common-actions/.github/workflows/sonarcloud.yml@main
with secrets: inherit. Adds SonarCloud badge to README where missing and
removes the redundant sonar-project.properties (config now passed by the
reusable workflow).
* Release: PdfCraft v0.11.0
- Bump the workspace to 0.11.0.
- Fix main's red CI (cargo-deny): update rustls-webpki 0.103.15 -> 0.103.16
(RUSTSEC-2026-0382) and webpki-root-certs 1.0.9 -> 1.0.10 (RUSTSEC-2026-0383),
both reached through winprint's sigstore build dependency.
- `cargo xtask version set` now moves the workspace crates' pins in
[workspace.dependencies] with the version; before, `cargo update --workspace`
refused the bump and the pins had to be edited by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: a healthy private retry may answer during the watchdog pass that scheduled it
shared_timeouts_retry_healthy_pages_privately_before_blacklisting failed on
CI (PR #3893): the try_recv that runs the watchdog spawns the private retries,
and page 1's retry has no gate, so on a loaded runner its healthy result
arrived before that same call read the channel. Accept that early answer
(page 1, no error) and use it as the healthy result; an error still fails.
Reproduced by sleeping 300 ms after the watchdog in try_recv: the old test
fails every time, the new one passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(redact): evaluate OCMD visibility during sanitization
Fixes#3499 by evaluating OCMD policies and bounded visibility expressions against the default optional-content configuration before removing hidden groups. Unknown or malformed visibility data is handled conservatively.
* fix(redact): preserve OCMD visibility during sanitization
* fix(redact): count hidden XObjects during verification
* test(redact): cover hidden form depth limit
is_nearly_equal allows a 256th of a path unit, and stroke_path_impl
applies the matrix after path_as_rect, so a cm of 10,000 turns that
tolerance into tens of device units: a path that only nearly closes was
painted as the rectangle, with a side it does not have, and an edge off
true by that much counted as axis-aligned.
The fast path now compares coordinates exactly. Nothing that is a
rectangle loses it: a rectangle's coordinates are equal in the content
stream, not approximately equal. A path that misses takes the general
route and is painted as written, which is the safe direction.
I could not make a rendered case that distinguishes the two: this
renderer paints a 10,000x cm as a solid page whatever the line width, so
there are no pixels to compare at the scale where the tolerance shows.
The change rests on the argument above, and the three existing tests
still pin the behaviour that is measurable.
Reported by CodeRabbit on #3327.
FredApps's note on #3327: five points that are all on the bounding box's
corners still need not trace the rectangle. Three sides and a step back
along the third --
10 10 m 30 10 l 30 30 l 10 30 l 30 30 l S
-- visits every corner, so it was taken as the box and stroked with a
left side the path does not have.
path_as_rect now asks what a rectangle actually is: four distinct
points, each step from one to the next along a single axis, and the last
point back at the first (written as the fourth LineTo or as ClosePath).
That is a rectangle and nothing else is, so the corner test and its
off_corner flag are gone -- four points of an axis-aligned traversal are
necessarily the corners of their own bounding box. A degenerate path,
whose points are not distinct, now takes the general route as well,
where it painted a zero-width or zero-height rectangle before.
The vendored-patch table has the row this change was missing: without
it a re-vendor of hayro-interpret would drop the patch silently, which
is the other half of FredApps's review.
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>