Port behavior and regression coverage into current modules; preserve current upstream behavior.
Validate benchmark names before both measurement and baseline-from-files paths, retaining newer baseline modes.
The i18n coverage scan and prefs_usage stopped reading a file at its first `#[cfg(test)] mod`. A split parent declares `#[cfg(test)] mod tests;` before the rest of its code, so the scans skipped that code, and on main an inline test module in the middle of panels.rs hid the code after it. Both now drop only the body of an inline test module.
The prefs audits in scorecard and prefs_usage count a read when the section and the field appear in the same file. Both now also treat a split module (`foo.rs` with `foo/`) as one file, and prefs_usage leaves out `engine/src/prefs/` as scorecard already does, since that directory now holds the rest of the prefs model.
On main and on this branch: the i18n coverage report is identical (2,142 source keys; the old scan found 2,049 on main), `cargo xtask scorecard --check` is up to date, and prefs_usage passes.
The source scanners in xtask (i18n coverage, scorecard) and engine/tests/it/prefs_usage.rs already walk src/ recursively. The i18n coverage scan also skips directories named tests, as the other two do, so test files moved to foo/tests/*.rs stay out of the key list. The report is unchanged on current main.
* chore: resolve SonarCloud maintainability issues
* fix: format imports and fix effect_entry argument type
* fix: restore test closure and wildcard import that broke compilation
* refactor: split composite_to_surface to reduce cognitive complexity
The 2026-10-05 baseline was recorded at load 224.9 on 14 cores; the scorecard now
says so without changing a number, shows the baseline's run count, and replaces
the claim that the nightly checks regressions with the machine class it actually
runs on.
--update-baseline refuses a contended run (1-minute load at the start above
settings.max_load_per_core per core) unless --allow-contended, which labels it.
--update-baseline --from builds the baseline from several saved results.json
files: the median p50 per scenario and the highest as its noise ceiling, which
regressions are now judged against. It also reports which budgets could be
enforced and which enforced ones are flaky. The single-run path is the N = 1
case of the same function.
The PR's test compares the rendered scorecard with the tree, where the
corpus.rs doc comments and the sha256 manifests agree, so it still passes
with the PR's code reverted. The manifest loop is now pinned_file_counts,
and a synthetic root whose doc comment says 999 files and whose manifest
lists 3 must give 3 (an unpinned corpus keeps its doc comment count). It
fails with the loop skipped.
Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
docs/file-format-parity.md: main's PSD row with the round-trip count
258/258. docs/scorecard.md: regenerated with `cargo xtask scorecard`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report prints, per language, the source keys its catalog lacks (one
line for a language with none). `cargo xtask ci` runs it as its last step
and never fails on it: a missing translation shows in English. A report
that can't run prints a warning.
cargo xtask test and the ci test step run cargo nextest over libs, bins and tests when nextest is
installed (cargo test otherwise, with a notice), then cargo test --doc. The wasm check is one
cargo check with --keep-going over the wasm-safe crates, plus one call per optional web feature.
Clippy runs with --keep-going.
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the civil_from_days copies behind the app's log timestamp, the
Measurement Log's date/time, and the perf report date with chrono as a
workspace dependency (reusing the version already in the tree). Output formats
are unchanged and pinned by the existing tests. AGENTS.md now says time goes
through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed
chrono's From<SystemTime> unwraps internally, so a clock past year 262143
panicked. logging::timestamp and analysis_cmds::now_iso now read the duration
since the epoch and go through the fallible DateTime::from_timestamp, reading
an out-of-range or pre-epoch clock as the epoch as the old saturating code
did. All three ISO helpers (timestamp, now_iso, perf::iso_utc) write the year
with {:04} rather than %Y, which prefixes a '+' past year 9999.
Tests cover the far-future and year-10000 cases for timestamp and iso_utc.
* 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.
* time: preserve old out-of-range date conversion (no epoch fallback)
`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any
instant past chrono's year range to 1970-01-01, changing output the old
hand-rolled Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s
was 318857-05-20 17:46:40, not the epoch). Restore the old conversion domain
with a cycle-based helper that routes the calendar through chrono (the
calendar repeats every 400 years = 146_097 days, so month/day/time come from
chrono and only the year is carried in i128, keeping every i64 exact).
- apps/photocraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/engine/src/analysis_cmds.rs: extract pure `iso_from_unix` (cycle
based) from `now_iso` so the conversion is tested directly.
- xtask/src/perf.rs: `iso_utc` cycle-based, accepts the full u64 range the
old code did.
Pinned old outputs in tests:
logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
iso_from_unix 10_000_000_000_000 -> 318857-05-20T17:46:40Z,
-10_000_000_000_000 -> -314918-08-13T06:13:20Z, i64 extremes
iso_utc u64::MAX -> 584554051223-11-09T07:00:15Z (was epoch),
10_000_000_000_000 -> 318857-05-20T17:46:40Z
* 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.
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
module path (compound_limits::<name>); with --exact, the bare name
matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
crates/engine/src/file_cmds.rs: batch keeps the save-point closure (takes the output path) and also copies the session's authorize hook into each scratch session, as main now does.
crates/raw/src/sensor.rs: both sides cap the memory of retained tiles; kept the existing retained_segment_bytes check (checked multiplication, same limit) and dropped main's duplicate inline loop.
Row 16 (9f6a3c79): preserve platform filename semantics in corpus scans
(no backslash-to-separator rewrite on Unix) and enforce destination
containment before copying files into the corpus directory.
Row 20 (6e2a985b): skip directory symlinks, dedupe visited directories by
canonical identity, and cap total directories/files so symlink cycles can
no longer drive exponential traversal.
Fixes findings 9f6a3c79-8bdf-4751-8eb4-87d3154074c9 and
6e2a985b-6b14-4b97-af07-7ca8333b9b75.
PhotoCraft reads GIMP's .xcf (and .xcf.gz) files, read only, through a new
reader in photocraft-io written from the public XCF specification: versions
0 to 26, uncompressed, RLE and zlib tiles, 8/16/32-bit integer and
16/32/64-bit float precisions (linear precisions get a linear profile),
pixel layers at any position, groups by item path with pass-through,
layer masks applied or disabled, opacity, visibility, locks, colour tags,
the 2.10 and legacy blend modes mapped to Photoshop's (modes without an
equivalent open as Normal with a note), the ICC profile, resolution,
guides, the comment, extra channels and the saved selection; indexed
images open as RGB. Text, vector and link layers open as their pixels;
layer effects, paths and floating selections are reported.
Fixtures are written by GIMP 3.2 itself (scripts/xcf_fixtures.py), each
with GIMP's merged rendering as a PNG. In gamma-encoded blend and
composite spaces our composite matches GIMP within 0.5/255; GIMP's
linear-light default, its Clip to backdrop and its clamp-after-alpha
for unbounded modes are documented and measured in docs/xcf.md.
The PR stopped writing PhotoCraft's defaults for params a step leaves
out. Checked against Photoshop, that is fine for Fill opacity and mode,
Crop delete, Selective Color method, Channel Mixer monochrome, Paint
Bucket contiguous and Gradient Map reverse/dither, but Photoshop plays
other missing keys differently: Canvas Size anchors top left (PhotoCraft:
centre), Apply Image blends Normal (Multiply), Color Balance turns
preserve luminosity off (on), and a Paint Bucket step without a
tolerance doesn't play at all. Those, and the unverified Hue/Saturation,
Color Balance tone and Save for Web keys, are written again as before.
- `tools.setColors` exported only the foreground of a step that sets
both colours; such a step now doesn't export (the strict save refuses
it, as before the PR).
- `kind` on an imported `layer.setAdjustment` made Photoshop's "reset to
default" (`set` with an empty `T`) a no-op merge; it now joins only
params that edit something.
- `cargo xtask corpus --atn` (and `--all`) failed on Windows: the Bjango
archive has a macOS `Icon\r` file tar can't create there; it's skipped.
- `test-corpus --changed` now runs for `atn_cmds` changes, and the
`ALL` doc comment is back on `ALL`.
Tests: defaults_photoshop_plays_differently_are_written,
set_colors_exports_only_a_single_colour and
adjustment_layer_reset_still_resets fail without this commit; the psd
and engine .atn corpus tests still pass (16 files byte-identical; 513
kept Photoshop steps).
Co-authored-by: Fabio Lissi <3223836+FabioLissi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fill_to (Paint Bucket export): keep main's `source` alias and explicit
colour (`Usng` = `Clr ` plus `Clr `, #7199) together with the PR's
optional `Tlrn`/`AntA`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main prints through craft-print, which runs `lp`; a Flatpak has no `lp`,
so File › Print fails there. This keeps craft-print as the route wherever
`lp` is on the path (and on Windows) and adds two more behind it:
- `photocraft-ipp`, a pure-Rust IPP client (CUPS-Get-Printers/Classes/
Default, Print-Job over HTTP/1.1) that talks to CUPS over its socket or
TCP; the Flatpak manifests grant --socket=cups for it. A `printServer`
named in the request always takes this route. `file.print.printers`
lists the queues.
- The XDG Print portal as the app's spooler (`Session::print_spooler`),
for a sandbox without the CUPS socket or a system without a printer:
the system print dialog, seeded with copies, paper and orientation.
PHOTOCRAFT_PRINT_PORTAL=1 prefers it, =0 removes it.
A route that exists but fails (unknown printer, refused job) is an error;
only a missing route moves on. Results gain `backend` (lp, windows, ipp,
portal). docs/printing.md describes the routes.
cargo xtask --help and the layers command output both referred to plan/architecture.md, which does not exist in this repository; there is no plan/ directory at the root. The dependency layering rules are documented as section 3 of docs/architecture.md, so point both references there.
Strict workspace Clippy stopped on collapsible_match in affinity, raw, plugins, engine, ui-egui and xtask. Pure conditions become guarded match arms (each falling through to an existing empty arm), and the ruler drag state and the measurement command result are bound to locals, preserving evaluation order, error messages and fallbacks.
The same lint fixes also appear as a side commit in #5157 (David2766), which can drop it on rebase.
* Fix two ui-egui tests broken on main by concurrent landings
- profile_open_tests: #5614 renamed the built-in profile to "(PhotoCraft)" after #5208
hard-coded "(Photocraft)" in the Use-embedded button label.
- smart_object_curves_preview_keeps_its_size_on_a_reduced_proxy (#5779): the reduced
square is in the right place, but its corner pixels are 220/255 after the smart
object's resampling; assert full coverage inside and over half at the corners.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web: ship the wasm as compressed parts behind a boot module (#5302)
The release site held the app's wasm as one file, which sits at the 25 MiB
per-file cap of Cloudflare Pages, so the 24 MiB gate failed unrelated PRs.
packaging/web/package.sh now gzips the wasm and cuts it into parts of at
most 20 MiB under app/, and ships the page of a new crate,
apps/photocraft-web-boot. That module fetches the parts in order, has the
browser inflate them (DecompressionStream) and passes the stream to the
app's own wasm-bindgen loader, so the browser still compiles while it
downloads. A failed download is retried twice, then the page says so.
The check is now that no file in the site is over 24 MiB. trunk build in
apps/photocraft-web and the Docker image still give one .wasm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Web boot: inflate the parts in the module where the browser can't (#5302)
* Tests: the adjustment-layer drag preview test follows #5778 (#5926)
#5778 caps a live adjustment-layer preview by the view's zoom, so at 100 %
the drag shows full resolution even with Low Resolution Previews on. The
test still expected the reduced copy there. It now checks that at 100 % every
column shows by default, and moves the low-res on/off comparison to 50 %,
where the reduced copy is still used: on, the drag shows only the (inverted)
black columns; off, it matches the committed view.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- corpus/atn: 16 sets from seven public repositories (MIT, BSD, Unlicense), fetched at pinned
commits by cargo xtask corpus --atn, verified against xtask/atn-corpus.sha256, licences next to
the files, rows in ATTRIBUTION.md and docs/development.md. photocraft-corpus takes only our own
generated files, so third-party sets are pins here, like psd-tools and ag-psd.
- photocraft-psd corpus_atn: every set parses and rewrites byte for byte (floor 16 files).
photocraft-engine corpus_atn: every set loads, saves and reloads to the same actions; the
steps still kept as Photoshop steps are capped (517 of 5 296 measured; generative AI and
scripts), and the first differing step is named on a mismatch.
- What the corpus found: localised panel names are platform legacy bytes (Mac Roman), now kept
verbatim with AtnItem::label from the descriptor's Unicode class name; exporters emitted
Photoshop defaults the importer had not read (now symmetric); setAdjustment carries its kind;
and 300 more steps map: transform without a reference, selection nudges, moves to the bottom,
View menu items, toolbox colours, clipping masks, ungroup, copy/paste styles, collapse groups,
layer comps. New example atn_roundtrip shows the first differing byte.
* i18n(sr): add a Serbian (Latin, Ekavian) UI catalog
Adds sr.tsv with 4,022 rows: every key the i18n-coverage tool measures
plus the union of the dynamic labels the other catalogs translate. The
catalog is written in Latin script, Ekavian; every Serbian locale
(sr-RS, sr-Latn-*, sr_RS@latin and Cyrillic sr-Cyrl-*) resolves to it.
Plurals use the one/few/many rule (plural_russian) with three forms per
@plural entry.
Original translations from the plain meaning of the English labels; no
proprietary localisation resources were used (ATTRIBUTION.md and
LICENSE-translations.txt updated). Tests cover locale resolution,
contextual and id overrides, placeholders, and plurals across teens and
compound counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Info.plist: declare the Serbian localization
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dejan Port <snork1982@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Combined with #4147's opt-level "z" list. Conflict in Cargo.toml's
wasm-release profile: kept #4147's photocraft-psd = "z" and dropped
#4253's "s" entry for it (one table per package). Measured with trunk
--release: main 184c226a is 25,235,857 bytes (over the 25,165,824 gate);
with this merge, 24,161,781.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n(cs): drop duplicate catalog entries
The Czech catalog listed Tool, Clone Stamp, History Brush, Dodge, Burn,
Sponge and Stroke Path… twice, which fails `cargo xtask i18n-coverage`
and the bundled_catalogs_are_consistent test. Keep one row each; for
Stroke Path… keep the later row, the one the loader already used.
* test(crop): pin the UI language to English in crop_default_mode
The test queries widgets by their English labels ("Ratio", "Swaps height
and width") but its harness followed the system locale, so it failed on a
machine whose locale has a registered catalog. Set interface.language to
"en" in the harness, as paint_bucket_all_layers and export_as_scaling do.
* i18n: add Hungarian (hu) UI translation
A complete Hungarian catalog (hu.tsv, 3908 entries: 3829 plain, 50
contextual, 21 plural, 8 command-id overrides) registered with the
complete-menu languages; hu-HU locales resolve to it.
Plural rule: plural_none. A Hungarian count takes the singular noun
("3 réteg"), so every @plural entry has one form.
Clean room: written from the plain meaning of the English labels with
ordinary Hungarian image-editing vocabulary; no proprietary localisation
resource, string table or product UI was used.
Also listed in the coverage test, LICENSE-translations.txt,
ATTRIBUTION.md and docs/localization.md, with tag, system-locale and
plural tests.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Add Turkish (tr) translation catalog covering 100% of measured UI strings (1,727/1,727 keys, 3,731 catalog entries).
- Register Turkish language in crates/ui-egui/src/i18n/mod.rs with complete_menus: true and plural_none
- Add tr.tsv translation catalog
- Add negotiation and coverage tests in system.rs, mod.rs and i18n_coverage.rs
- Add MIT OR Apache-2.0 notice in LICENSE-translations.txt
- Update docs/localization-parity.md
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>