The Replace text tool saves a StrikeOut with /IT /StrikeOutTextEdit and a caret
reply. The Comments panel named it by subtype only, and tool_for mapped it to the
Strikethrough tool, so Make Current Properties Default on it changed the
Strikethrough tool's defaults. Both now read the intent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
The picker passed no target, so the staleness guard never ran and a file
chosen after switching tabs was attached to the newly active document. Pass
the active document like Import data and the image picker do. A test covers
the tab switch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.
Clicking a paragraph covered it with an opaque box until its textless
background was rendered, then redrew it in the editor's own fonts; a
quick click from one open paragraph into another also painted the first
one's textless page with no editor over it, and framed the second one
with the first one's measured extent.
The textless page now goes into a place kept under the page's later
shapes, and the editor fills it only once the paragraph has changed;
until then the page raster shows its own text and the editor draws its
text in no colour, for the caret and the selection only. The cover is
kept for a changed paragraph whose background isn't ready, decided
after the frame's typing. The frame uses a measured extent only when it
was measured for the open paragraph.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The whole-page raster and the tiles of a page come back separately. Once
the whole-page raster is fresh it has the picture where it went, but the
settled preview still took its pixels from where it was, now empty,
and drew them over it until the last stale tile arrived; a drag begun
in that interval did the same.
Keep blanking the old frame while anything is still stale, but take
the pixels from the old frame only while the whole-page raster itself
is stale (page_raster_out_of_date), else from where the picture went;
a drag in that interval blanks the first edit's frame and samples its
own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* 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>
A 0×0, 0×n or n×0 raster is the right length (no bytes) and already
failed in all three encoders, but through each one's own error (PNG's
"Zero width not allowed", the JPEG encoder's size check, tiff's
dimension check). check_raster now refuses it first, with one message
for every format, so the rule doesn't depend on the encoders' versions.
rasters_of_no_pixels_are_an_error asserts that message for PNG, JPEG and
TIFF; without the check it fails on PNG's own error. From CodeRabbit's
review.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
Edit text & images outlines every picture from the page's inventory,
which still has the dragged one where it was until the release; that
outline trailed behind the picture and caught up after it was let go.
Leave the dragged picture's own outline out while it moves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
encode_jpeg and encode_tiff composited the raster over white with
as_chunks::<4>(), which drops a partial trailing pixel, and handed the
result to encoders that trust its length. Run before this change:
- JPEG: a partial pixel at the end was dropped and the rest written as a
valid image; a buffer a pixel short, a pixel too many, empty, or with a
size whose byte count overflows panicked (image's JpegEncoder checks
the length with assert_eq!).
- TIFF: a partial pixel or a pixel too many was written as a valid
image; a buffer a pixel short panicked (tiff's write_data compares the
buffer with the pixel count, not the sample count, then slices).
The length check encode_png gained in #2425 is now check_raster, and all
three encoders call it first. Rendered pages are always the right size,
so what Export > Image writes is unchanged.
export::raster_tests::rasters_of_the_wrong_length_are_rejected now runs
its seven cases through PNG, JPEG and TIFF; it fails without the fix at
"JPEG, opaque, a partial pixel after". rasters_of_no_pixels_are_an_error
covers 0x0, 0x3 and 3x0, which are the right length and still errors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* 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>
Drawing a half can close its own tab (the snapshot bar's Close), which removed
or shifted the view indexes compare_area kept using: the right half's close
panicked at the scroll sync, the left half's in document_area for the other half.
Each half is now found again by id, and the frame ends when one is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
CodeRabbit: the engine test skipped when the viewer's render was blank, so a
viewer pool that lost the installed-font fallback would have skipped instead of
failing. Whether this machine has a face that can draw the page is now asked of
a renderer with RenderConfig::system_fonts switched on by hand, and the viewer's
render must then draw it. Without the fallback on the viewer's pool the test
fails.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CodeRabbit: Edit refuses to start recognition in Read mode or beneath a dialog,
but a run started a moment earlier could finish after the user had switched to
Read mode or opened a dialog, and poll_ocr applied its result there. The result
of a run Edit started is now left until both are gone; a run the user asked for
completes as before. The unit test uses a made-up result, so it needs no OCR
models, and fails without the check.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The old doc comment was left above the constants added in this change, where it
documents PREFERENCES_BUTTON_ROW wrongly, and its "Returns true to close" no
longer holds for preferences_body, which returns ().
Move its first sentence onto preferences_body, drop the "Returns" sentence, and
leave each constant with only its own comment.
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).
Create a new digital ID rejects a password shorter than
sign::pkcs12::MIN_PASSWORD_CHARS (8) with "The password must have at
least 8 characters.", which the dialog shows through tl!. Italian only
had the older 6-character wording, so the message appeared in English.
Czech, Lithuanian and Vietnamese already carry the current key.
The other 67 keys some catalogs have and Italian lacks are not looked
up by the code: shortcut tooltips the code now fills from {key} rows,
older wordings, plain-string plurals and generic labels only ar/lt/vi
carry. The catalog's SHA-256 in ATTRIBUTION.toml is updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The settings scroll when the window is short, but OK was the last thing inside
the scrolled content, so in a short window it was off screen until you scrolled
to it, and the dialog was cut off partway through the theme cards.
OK is now a row below the scroll area, outside it. The scroll area gets the
height the window leaves (at least 60 pt, so OK stays on screen in the smallest
window, 520 pt, at the largest interface size, 300%) and a scrollbar of its own
instead of the floating one that lay over the right edge of the theme cards.
A scroll area starts from the room its modal offers, and a modal offers more
each frame for the first few, so with OK outside the scroll the dialog opened
small and stretched by about 45 pt a frame for ten frames, and clicks in the
first frames missed. The scroll area is asked for its full height from the start
(min_scrolled_height, which egui applies only when the settings overflow it),
so it opens at full size and does not stretch in a tall window.
- test: OK and the title are on screen at window heights from 173 to 1100; OK
closes the dialog and does not move while the settings scroll, and a far-down
setting scrolls into view between the title and OK; the dialog does not grow
after it opens; a very tall window has no blank gap above OK. Both the
original layout and the stretching variants fail them.
- parity: misc.preferences-dialog evidence and what is still missing.
The field's /TU is read into FormField::tooltip and shown in the Fields
panel, but resting the pointer on the field on the page showed nothing.
page_input now attaches it to the hover, as Acrobat does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R3niW7kVeT8mi7TpWc8eAJ
* 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>
FredApps: CI never fetches the OCR models, so the tests that recognize something
skip there and the recognition paths this PR adds went untested in CI.
The decision is split out of auto_ocr_scanned_page into scan_to_read, which
needs no models (the run itself judges the page's own text), with unit tests
that run everywhere: a picture page is read and a text or blank page is not;
only Edit being on starts it (its tab counts); Read mode and an open dialog
wait; a page already read is not read again and a page found no scan is judged
again once the document changes. Two mutants (no Read/modal guard; a no-scan
page never judged again) each fail one of them. The tests that recognize words
still need the models.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Proposed for the maintainers to accept or drop (separate commit): the page
viewer, on screen only, may use one installed face for a CID font of an Adobe
CJK collection that a PDF does not embed. Exports, printing, OCR and the
automation tools never use it; nothing drawn with it is saved or printed; no
Noto CJK, Source Han or Adobe family is a candidate.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
FredApps: AGENTS.md §1.4 allows reading an installed font at runtime only as
the interface's last fallback and says nothing from it may enter a PDF or
package, while the renderer fallback also served exports and printing.
RenderConfig gains system_fonts, off by default. The engine sets it only on the
pool that draws the viewer (display_config); exports, printing, OCR and the
automation tools render with the document's own configuration, so nothing drawn
with an installed font is saved or printed. Tests: the render of a non-embedded
CNS1, GB1 and Korea1 font is blank without the flag and drawn with it; the
viewer's pool draws such a page and an export of it does not (it fails if the
flag is on everywhere). No Adobe-named font can be a candidate.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The preview was always monochrome, so the Print in grayscale checkbox appeared to do nothing but flicker between the colour thumbnail and the (wrongly) greyed sheet raster.
Move GRAYSCALE_TAG to a free tag bit (1 << 59) so it no longer overlaps PRINT_TAG, and let the colour and grayscale preview requests carry different tags so set_print_grayscale actually re-renders the right one.
Test: print_preview_tag_distinguishes_grayscale_from_colour failed before the change and passes after.
A TextSession opens a paragraph once: it keeps the document as it was
and the page's lines then. Each draft is laid out on a copy of that
document, so no draft sees another's output, and the commit writes into
the document it was opened on, refusing ("the paragraph changed while it
was being edited") when that document has changed since.
text_edit_layout and rewrite_block now open a session and lay out or
commit once, so there is one path and their output is unchanged. No
caches yet: those come next, each under the same equivalence test.
First step of #2512.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two cases kept the old picture on screen after a move or resize:
- A page that had no tiles when the edit applied was taken as drawn
again as soon as a first tile arrived, while its backdrop still had
the picture. Out of date now means its whole-page raster or any tile
still waits for a replacement.
- A second drag before the page was drawn again took the picture's
pixels from its new frame, where the raster doesn't have it, and
blanked only that. It now takes them from where the raster still has
the picture and blanks that, and keeps it as the source on release.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>