Consent expiry is now per kind of data: renewing text no longer renews an expired image permission. A connection edit keeps models found by a test at the new address, and drops only the ones saved for the old one.
A configured connection does not authorise an app by itself. An agreement now
covers one app, connection, destination and capability, for the kinds of data
it named, and may carry an expiry (unix seconds).
- Disclosure carries the capability; ConsentRecord gains optional capability
and expires_at.
- ConsentLedger::needs_consent takes the time (the crate has no clock);
grant_until sets an expiry, grant stays open-ended. The latest grant sets
the expiry for that capability only.
- Records saved before this have no capability and cover nothing, so the
person is asked once more.
Nine new tests; six deliberate bugs (expiry ignored, off by one, capability
ignored, legacy record covers everything, expiry not updated, timed grant made
permanent) were each caught.
Builtin::from_profile_name matches only a built-in's own description.
Builtin::stand_in_for adds the FOGRA39 approximations (Coated FOGRA39, ISO
Coated v2) for documents that name a profile without carrying its bytes.
ISO Coated v2 300% (ECI) has no stand-in: the built-in separates to up to
330 % total ink. The README example parses the document's ICC bytes first
and handles a missing stand-in explicitly.
The reference value for CMYK 0/41/90/4 in Coated FOGRA39 is 239,161,35.
The fitted profile gives 239,161,33 (ΔE00 0.16); the uncalibrated formula
DesignCraft used gives 245,144,25 (ΔE00 6.36). Six more reference values
(K50 grey, rich black, light tint, skin tone, C+Y green, near-neutral)
are within 0.7 ΔE00.
The Generic CMYK oracle only runs where macOS ships that profile. The
same comparison (CMYK -> sRGB, 8-bit, relative colorimetric without
BPC, same bytes for both engines) with the built-in Coated FOGRA39
runs everywhere. Measured: max 10, p99 5, mean 0.89 codes; the worst
pixel matches an independent float computation, not moxcms.
With a LUT destination, sRGB white came out as 0.3-0.5 % C/M/Y through
Transform::eval and precise float conversions: the legacy Lab encoding
puts L* 100 at 0.996 and a* = 0 at 0.50001, between the BToA grid
nodes, and fix_white only patched the device-link node, so only the
table paths were right.
Transform: for every intent but absolute colorimetric (and proofs
without paper simulation), a pixel equal to the source's device white
gives exactly the destination's device white. fix_white still sets the
device-link node; the paths that do not read that node exactly (the
exact pipeline, float interpolation, and 8-bit lookups when white is no
node) compare each pixel with white once. No other colour changes.
device_white(Lab) had a* = b* = 0.5; Lab white is 128/255 in the v4
encoding, which is no node of the 33-point grid, so Lab sources take
the per-pixel check there too.
Profile: synth::CmykParams::white_on_node maps no ink to exactly
L* 100, a* = b* = 0 in the AToB and gives the BToA tables input curves
that put that white on a node holding no ink. The Coated FOGRA39
profile is regenerated with it and separates PCS white to 2e-5 ink
(was 0.5 %), so the step into white is as smooth as its neighbours.
The default parameters and the Photocraft profile are unchanged; its
white still falls between nodes, so its last grey step into white is
up to about 0.4 % C larger than the one before, as with lcms's
white-node fix, and the test allows that for this profile.
FOGRA39 accuracy against 16a66e1: AToB against the patches unchanged
(mean 0.07, p95 0.18, max 0.53 ΔE00), reference sRGB values unchanged
(max 0.76), Lab -> CMYK -> Lab p95 1.04 -> 1.07, max 2.54 -> 2.55
(mean 0.24).
The crate denies panics, but asserts and slice indexing were reachable
from arguments: pixel strides, buffers that are not whole pixels or a
destination shorter than the source, single-colour slices shorter than
the channel count, CLUT and pipeline dimensions, and LUT sizes.
Every such check now returns CmsError::InvalidArgument. Buffer
conversions validate once per call, before any pixel is written, so the
per-pixel loops carry no extra checks; internal callers that already
guarantee lengths use crate-private unchecked evaluation.
Pipeline, Clut and Lut3d keep their fields private (read through
accessors) so a validated value cannot be changed into one that panics.
Pipeline::new checks matrix shapes and channel counts; Clut::new and
Clut::sample check grid, outputs and the table size limit.
API changes (unreleased crate):
- Transform::eval, eval_fast, convert_u8/u16/f32, apply,
convert_bytes_many and from_pipeline return Result.
- Pipeline::new, extend and eval, Clut::new, sample, eval,
eval_trilinear and eval_multilinear return Result.
- GamutCheck::delta_e and out_of_gamut, Lut3d::from_transform,
LutFile::identity and from_fn, synth::cmyk_profile and coated_cmyk
return Result.
- Pipeline, Clut and Lut3d fields are read through methods.
Add Builtin::CoatedFogra39 ("Coated FOGRA39 (fitted, craft-cms)",
profiles/craft-coated-fogra39.icc: AToB 11^4, BToA 33^3, 330% TAC,
medium GCR). It is fitted to Fogra's FOGRA39 characterisation data
(tests/data/FOGRA39.txt, unmodified, 1617 patches): a Yule-Nielsen
Neugebauer model with free overprints, per-ink tone curves and n, plus
a Gaussian-kernel ridge correction of the residuals. Against the
patches, relative colorimetric through the AToB: mean dE00 0.07,
p95 0.18, max 0.53. The BToA tables invert the AToB table
(synth::cmyk_profile_from_model); the Photocraft coated profile and its
synth defaults are unchanged.
Builtin::from_profile_name maps the names documents carry for
FOGRA39-based profiles (Coated FOGRA39, ISO Coated v2 (ECI)) to the new
profile and every built-in by its description. Adds math::delta_e2000.
Refs #36
Add shared config.toml storage while keeping settings types, defaults and UI policy in each app.
Includes isolated in-memory operation, configurable portable paths, atomic saves with private backups, damaged-file preservation, schema migrations, and documented best-effort handling of external edits.
Validation: native macOS formatting, strict workspace clippy, 172 workspace tests and the craft-settings WASM check. Independent backup-permission and failed external-migration repros fail before the fixes and pass afterward.
Hosted CI: all seven UI and printing jobs pass across macOS, Linux, Windows and UI WASM.
Refs #12
retry_rename also retries ERROR_SHARING_VIOLATION (32) and
ERROR_LOCK_VIOLATION (33), which a rename can report while a scanner
holds the file, and caps each pause at 200 ms.
A rename needs only a writable folder, so a read-only file would be
replaced where writing it in place would fail. Before staging, the
existing file is opened for writing without truncating; when that fails
the save fails with a read-only error.
The target is found by reading links one hop at a time, resolving
relative links against the link's folder. A dangling link writes the
file it names and stays a link, and a loop of links is an error.
A save into a folder that does not exist fails with an error naming the
folder, so a save to an unmounted network share does not create a local
folder in its place.
write_new_with gives the staged file its name with a hard link on every
platform. Where hard links fail for any reason other than an existing
target (FAT/exFAT, some SMB and FUSE volumes), it copies the staged
content into a file opened with create_new. It shares the temporary-file
retry, error context and folder flush with the replace path, and its
errors only mention an original file when there is one.
atomic_write, atomic_write_with and atomic_write_custom all run through
one internal function that takes the AtomicFs, the RenameRetry and a
writer callback, so fault injection also covers streaming writes.
AtomicFs creates and flushes the temporary file (create_new, sync)
instead of writing a byte slice.
install_app_menu printed the error and carried on, so an app could not
tell that ⌘Q had fallen back to winit's Quit, which skips the prompt.
It now returns the error; the app decides whether it is fatal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apps that build their own muda app menu can't use install_app_menu: it
replaces the whole menu and sets muda's process-wide event handler.
QUIT_MENU_ID and on_menu_event(ctx, id) let them give their own Quit
item the id and route its event through the app's close handling.
Neither needs muda, so any muda version works. install_app_menu now
uses both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
winit's default app menu sends Quit (Cmd+Q) straight to
-[NSApplication terminate:] and winit doesn't implement
applicationShouldTerminate:, so a Craft app quits without its
unsaved-changes prompt. The new opt-in `menu` feature adds
`install_app_menu(ctx, app_name)`, which replaces that menu with a
muda-built copy whose Quit sends ViewportCommand::Close. The app's own
close handling then runs, as for the close button, and
on_close_request quits.
muda is a macOS-only, optional dependency; no unsafe code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>