Includes #4325#1952#4180#2529#4320#4805, preserving their original histories.
Keep paragraph alignment and hard/soft wrap metadata together; retain CCITT/downsampling/RGB ownership guards with bounded JBIG2 export and exact-copy symbol encoding.
Combined interaction regressions added. Validation pending.
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#b70228, its red, from the icon (#5245)). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).
craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/edit/src/images.rs
- crates/forms/src/appearance.rs
The PDF Optimizer and Reduce File Size downsampled colour and grey images
but not black-and-white ones. Settings::mono is now MonoSettings {
downsample, target_ppi, above_ppi, compression }, 300 ppi above 450 ppi
and CCITT Group 4 by default (Monochrome Images ▸ Downsample to;
doc_optimize mono: { downsample, ppi, above_ppi, compression }).
A 1-bit image or image mask (no filter, general-purpose filters, or CCITT
fax after any of them) and eight-bit grey of only black and white are averaged by
area: each new pixel takes the sample covering more of it, a tie 0, so a
dark line half a new pixel wide stays. They are stored as Group 4, or as
1-bit Flate when that is smaller; with Retain a resampled image keeps its
kind of compression. Images with a soft mask keep their size.
CCITT decoding moves from Export ▸ Images into pdfcraft-filters
(decode_ccitt, CcittParams, through hayro-ccitt), and
pdfcraft_cos::Document::decoded_image reads a fax image's /DecodeParms
for it (undoing filters before CCITT, such as ASCII85Decode, with any
/DecodeParms given by reference made direct first), so export
and the optimizer share one reader, and Group 4 scans can be
downsampled. A 600 ppi text page went from 79 KB to 40 KB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
Redaction doesn't decode CCITT yet, so a mark over part of a picture
Create PDF stored as Group 4 removes the whole picture (nothing leaks);
before this PR such pictures were eight-bit Flate and had only the
covered pixels cleared. The create README and the image-to-PDF parity
note say so; #4384 adds CCITT to redaction. The workspace's comment on
hayro-ccitt says Export uses it, not only tests.
From reviews with the pdfcraft-review skill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6yUtvWGPsJpMLyh6pxSXG
JBIG2 images, and JBIG2 stencil masks, were reported as "can't be exported
yet", so an image the Optimizer stores as JBIG2 would stop exporting. They
are now decoded with hayro-jbig2 (already in the tree through hayro), with
their /JBIG2Globals from a DecodeParms dictionary or array, as JBIG2Decode
gives them: black as 0 (ISO 32000-2 §7.4.7). Unreadable globals, a JBIG2
page of another width, or fewer rows than the image skip the image; rows
past its height are dropped, so the output is bounded by the checked size.
docs/pdf-spec-parity.md §7.4 no longer says there is no JBIG2 encoding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
pdfcraft_filters::encode_jbig2_generic writes 1-bit rows as a JBIG2
stream for JBIG2Decode (embedded organisation: a page information
segment and an immediate lossless generic region, no globals): template
0 with its nominal AT pixels, typical prediction, and the MQ arithmetic
coder (mq.rs), written from the JBIG committee's ISO/IEC 14492 Final
Committee Draft. The coder reproduces its Annex H.2 test sequence byte
for byte; output round-trips through hayro-jbig2, and Poppler renders
it the same as the original.
The optimizer's Monochrome Images gets JBIG2 (lossless) beside CCITT
Group 4 (still the default), doc_optimize mono jbig2; a document below
PDF 1.4 gets /Version /1.4 in its catalog. On 300 dpi text JBIG2 is
about a third smaller than Group 4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
pdfcraft_filters::encode_g4 encodes packed 1-bit rows as Group 4 (ITU-T
T.6, /K -1) through the fax crate (MIT, already in the tree via tiff),
with rows capped at 32,767 samples so its 16-bit positions can't
overflow; round trips are checked against hayro-ccitt.
The optimizer (Settings::mono, CCITT G4 by default, so Reduce File Size
too) re-encodes a 1-bit image of one colour component, or an image
mask, stored with no filter or general-purpose ones, when that is
smaller as stored. The samples decode to the same bits, so colour
space, decode array and masks stay. PDF Optimizer gets a Monochrome
Images row and doc_optimize a mono option. optimize.mono-compression
goes from planned to partial (JBIG2 and monochrome downsampling not
yet).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
vello_cpu 0.0.8's clip_color, shared by the nonseparable blend modes,
divided 0 by 0 when a gray's equal components were rounded just below 0
or above 1 after set_lum. The NaN drew the pixel black at the source's
alpha whatever the backdrop: nine gray levels under /BM /Color in an
empty isolated group (0.96 g among them) rendered (0, 0, 0), and two
more at half opacity came out wrong.
Vendor vello_cpu 0.0.8 (Apache-2.0 OR MIT) with a backport of
linebender/vello#2000, which clips only when the denominator is
positive. No vello_cpu release carries the fix yet; drop the copy when
one does.
Tests in it::nonseparable_gray run every gray level. The two Color
ramps fail with the unpatched blend.rs. The Luminosity ramp (black over
each opaque gray, upstream's other case) passes either way here: pages
use the u8 pipeline, and that case only failed in the f32 one; it stays
as a guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
craft-libs#366 drops the default-features override on craft-keymap that
older Cargo rejects. FreeBSD aarch64 builds with Rust 1.98.1 and failed
with "no matching package named craft-menu found" (craft-libs#364).
* feat(ui): menus from craft-menu, native on macOS
One menu definition (crates/ui-egui/src/menus.rs) built from the command
registry drives the native macOS menu bar (craft-menu's muda backend) and
an in-window egui bar on Windows, Linux and the web, which takes the place
of the "Menu" button at the start of the top bar.
- File, Edit, View, Sign, Window and Help, with separators and submenus.
Edit gains Cut, Paste, Delete and Select All (they act as their keys
do) and a Pages submenu; View gains a Tools submenu built from the All
tools groups; Sign holds Fill & Sign and the certificate commands;
Window lists the open documents.
- Toggles are check items and the page layouts, side panels and themes
radio items, with their state from the app.
- File > Open Recent lists the recent files (file.open_recent with an
index).
- A menu click and its shortcut run the same dispatch, including the
form-field deferral (#166) and macOS Cmd+W closing the window (#869).
Dialogs and the palette grey the menus through craft-menu's gate.
- Quit goes through craft-quit, so the unsaved-changes prompt runs.
- craft-ui, craft-quit, craft-menu and craft-keymap pinned to
storytold/craft-libs 70ade1fb.
Menu state and dispatch follow #2610 by @hajdukste; the menu tree and the
view and panel items follow #3870 by @RivanJarjes.
* test(ui): the menu model, native dispatch and the in-window bar
- Unit tests for the menu definition: every registry menu command is
listed once with its registry shortcut; check and radio items follow
the app; a dialog greys document commands and drops their clicks; the
recent files and open documents are listed once each; a native menu
click (through a fake craft-menu backend) runs its command once.
- The kittests that opened the old "Menu" button use the menu bar, and
the ones that look up a label the bar's titles share (Edit, File,
Sign) skip the bar.
- The in-window bar takes its titles' width, so the mode tabs, search
and buttons keep their place in the top bar.
- Edit menu clicks that stand for keys (Copy, Cut, Delete, Select All)
reach this frame's input from `logic`, where test hosts see them too.
* test(ui): the language switch test opens File in the menu bar
Translated titles and rows carry their mnemonic ("ファイル(F)", "開く(O)…").
* feat(ui): the menu bars run Quit themselves, the bar shrinks to its titles, and AppKit's window list stays off
craft-libs main edc41f6 (craft-libs#354): MenuBarUi runs Quit and Minimize, shrink_to_fit sizes the in-window bar, Key::parse reads the registry's arrow and minus glyphs, and mac::install_with(.., window_list(false)) keeps the Window menu to PdfCraft's own document list.
* fix(ui): the menu command filter is an if, for clippy
* test(ui): drop a needless borrow in the edit-tab helper
* test(ui): two tests follow the menu bar
The tests were wrong, not the app. The Preferences language test clicked the old Menu button; it opens Edit in the menu bar and its Preferences row. The prepare Actions test counted the Edit labels: the narrower menu bar leaves room for the Edit tab, so the rows' Edit buttons come third and fourth.
Picks up the update prompt fix that draws its close mark and version pill
without glyphs a font may lack, so the prompt no longer takes the app down
when a bundled face has no ✕ or →.
Part of tracking issue storytold/craft-libs#319🤖 Generated with Codebuff
Co-authored-by: Codebuff <noreply@codebuff.com>
* feat: startup update prompt modal and direct self-updating
Help ▸ Check for updates and the check at start run through craft-update's Updater, which replaces the old GitHub fetcher and update source. The check at start is opt-in for the desktop app (test harnesses never reach the network), an install holds back while documents are unsaved, and a finished update closes through the app's quit path. craft-update stays out of the wasm build.
Ref: storytold/craft-libs#319
* test(updates): the prompt's buttons are DOWNLOAD and LATER
* build: pin craft-update to craft-libs main (db19c37)
* fix(updates): run the Windows update helper before argument parsing
* fix(updates): point the parity evidence at the current update tests
* Record successful PdfCraft web application check
* E21: bound startup requests and validate font and license inputs
* E21: separate pending control completion and gesture regression phases
* M-egui037: enforce control admission and per-family font notices
* M-egui037: qualify capture fixes and document migration behavior
* egui-037: identify current and historical qualification results
* refactor(ui): side panels on craft-ui task panes and the shared rail
The Tools pane and the open side panel are docked craft-ui task panes
(craft_ui::panes::PaneHost, Columns policy, no title bars, not movable),
and the rail is craft_ui::panes::Rail with the page navigation cluster
as its foot. On the right, outermost first: the rail, Tools when
Preferences > Tools panel side is Right, the side panel, the page.
Read mode uses the same panes without Tools; All tools full view still
covers them. Floating, tab groups and drag-in are gone with docking.rs.
- Widths are resizable within the old limits and saved under a new
side_panels preference (version 1) with the Tools side and open state.
The old docking value is read once as plain JSON and dropped on save.
- ui.dock becomes ui.panel {panel: tools|side, open, width};
ui.dock.reset resets the side panels ("Reset side panels").
- F4 shows or hides the side panel (reopening the last one) and
Shift+F4 the Tools panel, as View menu commands.
- craft-ui pinned to e9630ae7 (craft-libs#309).
* fix(ui): side panels clippy lint and the dragged-width test's grab offset
* chore(deps): pin craft-ui at 7c42807, whose pane resize edges work over the page
* chore(deps): pin craft-ui at craft-libs main ea4c823, with panes::Rail
hayro-cmap reads CMap streams with hayro-postscript's token scanner,
whose object::read skipped each `<<` and `>>` by calling itself again.
A CMap holding a long run of `<` or `>` used one stack frame per pair,
overflowed the stack and aborted the process.
Vendor hayro-postscript 0.1.0 with read() looping over the delimiters
instead (from storytold/craft-libs#263), patched in beside hayro-cmap.
Four object::tests cover long `<<` and `>>` runs and the skipped
delimiters, plain and nested.
* CI speed: line tables for workspace crates, no debug info for dependencies
* CI speed: xtask ci runs nextest and doctests, one wasm check, the CLI without default features
* 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.
* 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>
A /ToUnicode CMap with any unparsable part made the reading path
discard the whole map and fall back to the font encoding, so search
and redaction missed text the edit layer could read. Vendor
hayro-cmap 0.1.0 and patch it to skip malformed bfchar/bfrange
entries, unresolvable usecmap references and unparseable tails,
keeping every readable entry, matching MuPDF/pdftotext and
PdfCraft's own edit-layer parser.
Fixes#3177.
Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
craft-libs#169 is merged, so the fork pin is no longer needed. Pinned to
storytold/craft-libs 82a24b006eefa036b43c09a67fb9a3fbd17ffe63 (main after the merge) rather than the older 12281d07,
which predates resolve_absolute and the Windows colon refusal.
GoToR file specifications go through craft-docpath: network, device and
object-manager paths (\??\, /??/) are refused without touching the
filesystem, relative targets resolve against the source document's
folder, and a result that is not absolute is refused rather than read
against the working directory. Files named on the command line open
under their absolute path, so their relative links keep resolving.
* fix(macos): Cmd+Q asks about unsaved changes (#1136)
winit's default app menu sends Quit (Cmd+Q) straight to
-[NSApplication terminate:], so PdfCraft quit without asking and
unsaved edits were lost; the close button and Dock > Quit already ask.
Install craft-quit's app menu (storytold/craft-libs#21) from the app
creator. Its Quit sends ViewportCommand::Close, which guard_quit
handles like the close button. If the menu can't be built, winit's
menu stays and a warning is logged.
craft-quit is pinned to the head of craft-libs#21 until #5 and #21
are merged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(macos): closing the window quits through terminate: (#1136)
On Touch Bar Macs the close button crashed PdfCraft (exit 133,
-[_NSTouchBarFinderObservation invalidate]): eframe drops the window
inside AppKit's run loop. craft_quit::on_close_request, right after
guard_quit in `logic`, cancels an allowed close and quits through
-[NSApplication terminate:] instead. A close that guard_quit holds for
the unsaved-changes prompt is left alone. Elsewhere it does nothing.
craft-quit moves to the workspace dependencies, pinned to craft-libs
main (f58bc1a, with #5 and #21 merged) instead of #21's head.
The on_close_request change and its Touch Bar test are from jmacek.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M10: Windows print jobs show per-sheet progress; printers list faster
Follow-up to #944, carrying the two parts main doesn't have yet.
- The progress card follows a job being spooled and counts its sheets
as they reach the spooler ("Sending sheet 2 of 3 to Office…"), using
craft-print's new submit_with_progress. It replaces the "Sending to…"
toast, leaves another long job's card alone, and goes when the job
ends; the spooler's answer is still a notice.
- craft-print bumped to storytold/craft-libs#27, which also lists
Windows printers through .NET's PrinterSettings (the API jobs spool
with) instead of a CIM query of Win32_Printer.
- "Sending sheet {s} of {n} to {printer}…" in all 20 catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M2: clip intersection starts its row walk at the first shared row
clip::intersect in vello_common, the step every clipped path passes
through, began its walk over strip rows at the earlier of the two paths'
first rows. Each path therefore visited every row above it, although rows
where one path has no strips produce no output. Every page is clipped (the
crop box is a clip), so this ran for every stroke of every render: on the
A1 engineering sample it was the hottest function in a full-page render.
Start at the later first row, as vello_common 0.1 does. The strips and the
pixels are unchanged; a test counts the rows the walk visits and fails
without the change.
vello_common 0.0.8 is vendored with that one-line PdfCraft patch (marked
in the source), its unit test, the vendor/README.md row and an
ATTRIBUTION.toml entry for its reference image. Cargo.lock records it as a
path crate.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
55 files hand-rolled the same serialiser -- number the objects, record each one's offset, emit a
classic cross-reference table and a trailer -- 111 times over. The object bodies are properly the
business of the crate under test; the envelope around them is not, and every copy is a place for a
fixture to be quietly wrong in a way that makes a test prove less than it appears to.
`pdfcraft-testkit` holds that envelope. It has no workspace dependencies on purpose, so the crates
at the bottom of the layering can use it without a cycle, and it returns bytes rather than a parsed
document for the same reason.
The builder is byte-identical to the serialiser it replaces, asserted against a verbatim copy of
the old code across versions and trailer variations, because a migration that changes a fixture's
bytes silently changes what the test using it proves. Watched failing by breaking the free-entry
generation: three tests caught it. The one intended difference is that an empty stream dictionary
gives `<< /Length N >>` rather than a doubled space.
Migrates cos, measure and redact: 111 copies in 55 files becomes 108 in 53. The rest are
follow-ups rather than one sweep -- ui-egui alone is 27 of those files and is the most contested
area in the repository. Some copies should stay: a test about a malformed cross-reference table
needs to write its own bytes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every test that wanted an external oracle probed for it by hand and chose for itself what to do
when it was missing. The common choice -- print a line and return -- reports as a pass, so a suite
with no oracles installed looked exactly like one in which every oracle agreed.
That was not hypothetical. No CI runner installed qpdf, so `qpdf_agrees` had never executed on a
runner, while README.md said saved output was "independently checked with qpdf". For R2-R4 that
test is the only check of owner-password authentication (Algorithm 7) that does not go through our
own code.
`pdfcraft-oracle` holds the probes and one rule: a missing oracle is a declared skip or a failure,
never a silent pass. `PDFCRAFT_ORACLES_REQUIRED` picks which, on the same convention as
`CRAFT_FONTS_REQUIRED`. CI installs qpdf and sets it, so a skip notice there is now a red build.
The crate has no panicking path of its own; `or_skip!` expands its assert into the caller, which
is test code.
Verified by running the qpdf tests against qpdf 12.4.2 for the first time -- they pass, so the
README claim is now both enforced and true. Both gate directions were watched: absent and
unrequired skips with a notice, absent and required fails with the install command in the message.
Only qpdf is installed in CI, because it is the only tool the test job uses. The Poppler pixel
comparison in crates/organize stays ignored until a change turns it on and verifies it there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adopt the canonical craft-print revision from craft-libs #28 so Windows printing uses the in-process Windows.Data.Pdf/XPS backend instead of PowerShell. Keep exact paper, orientation and scaling validation and the existing PDFCraft composition and permission checks.
Pin the reviewed winprint fork, scope its compile-time licence/advisory exceptions, and distinguish the earlier WDAC Sandbox exercise from the final pinned revision's Windows CI coverage. Physical printer output remains unverified.
Add worker-barrier regressions for Home-selected tool handoff and startup document ordering.
Fixes#1107
Co-authored-by: durtyrobbin <76844303+durtyrobbin@users.noreply.github.com>
Co-authored-by: Dimitar Klaturov <7221384+bulgariamitko@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
The previous release build failed in craft-common-actions, not in this
repo: riscv64 packaging got --formats deb,rpm,tar (fixed in
craft-common-actions#3), Linux jobs lacked the ALSA headers
(craft-common-actions#5), and Windows signing needed the AZURE_* secrets
listed in the caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>