Keep tests correct and docs current with one test binary per crate

- 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.
This commit is contained in:
aiongg
2026-10-22 16:55:34 +00:00
parent a6428d3dfb
commit ffa400c754
50 changed files with 112 additions and 103 deletions
+1 -1
View File
@@ -221,7 +221,7 @@ Every screenshot here is the real app at work on public-domain art, rendered off
PhotoCraft's PSD support is a standalone crate written from Adobe's public specification and tested against a corpus of real-world files.
- **Faithful round trips:** opening and re-saving a document renders the same for 307 of the 309 files in the psd-tools test set and 169 of 170 in our mixed ag-psd/psd-tools set (`crates/io/tests/corpus.rs`; fetch the psd-tools set with `cargo xtask corpus --psd-tools`), and anything we don't model yet (raw blocks, descriptors, extras) is carried over instead of being dropped. A re-saved file is not byte-identical to its source: PhotoCraft rewrites image resources, layer records and the composite. Only the standalone `photocraft-psd` crate, parsing and writing a file without the document model, reproduces every parseable corpus file byte for byte (`crates/psd/tests/corpus.rs`).
- **Faithful round trips:** opening and re-saving a document renders the same for 307 of the 309 files in the psd-tools test set and 169 of 170 in our mixed ag-psd/psd-tools set (`crates/io/tests/it/corpus.rs`; fetch the psd-tools set with `cargo xtask corpus --psd-tools`), and anything we don't model yet (raw blocks, descriptors, extras) is carried over instead of being dropped. A re-saved file is not byte-identical to its source: PhotoCraft rewrites image resources, layer records and the composite. Only the standalone `photocraft-psd` crate, parsing and writing a file without the document model, reproduces every parseable corpus file byte for byte (`crates/psd/tests/it/corpus.rs`).
- **Pixels that match:** a composite oracle compares our render with Photoshop's own merged image, covering gradient interpolation (Classic, Perceptual and Linear), layer effects, shape strokes, clipping and fill opacity.
- **Large documents:** PSB, 16 and 32-bit files, and CMYK and Lab documents open natively.
+1 -1
View File
@@ -137,7 +137,7 @@ Detail and the next releases: [docs/roadmap.md](docs/roadmap.md).
| Date | Change | Summary |
|---|---|---|
| 2026-10-17 | minor | Object Finder retains large enclosed foregrounds (#6548) |
| 2026-10-13 | minor | Focus item 1 quoted the oracle floor as 133 of 256; it is 145 of 258 (`crates/io/tests/corpus.rs`, gaps.md G1). Open issues 579 → 953 (2026-10-13) |
| 2026-10-13 | minor | Focus item 1 quoted the oracle floor as 133 of 256; it is 145 of 258 (`crates/io/tests/it/corpus.rs`, gaps.md G1). Open issues 579 → 953 (2026-10-13) |
| 2026-10-15 | minor | Russian catalog covers every dynamic UI label the other catalogs translate |
| 2026-10-11 | minor | Toolbox count 56 of 68 (12 missing) with the Convert Point tool (#3204) |
| 2026-10-10 | minor | Readiness by audience with hours per tier |
+3 -3
View File
@@ -108,11 +108,11 @@ The same reader is shared, as an independent copy, with VectorCraft's `vectorcra
* Unit tests on synthetic containers built by the `synth` feature (stored, zlib and zstd entries,
checksums, budgets, cycles, hostile streams, every truncation) and property tests of random
mutations, here and in `photocraft-io`'s `tests/affinity.rs`.
mutations, here and in `photocraft-io`'s `tests/it/affinity.rs`.
* `cargo xtask corpus --affinity` fetches 21 public CC0/MIT documents, including the four
Affinity 3 `.af` files of [samuel-etver/vector-art](https://github.com/samuel-etver/vector-art)
(CC0), at pinned commits, each checked against `xtask/affinity-corpus.sha256`. `tests/real_files.rs`
parses them; `photocraft-io`'s `tests/affinity_corpus.rs` imports and flattens each one and
(CC0), at pinned commits, each checked against `xtask/affinity-corpus.sha256`. `tests/it/real_files.rs`
parses them; `photocraft-io`'s `tests/it/affinity_corpus.rs` imports and flattens each one and
compares it with the thumbnail Affinity saved in it (mean difference 0–4.6 of 255, with a
ceiling per file).
* `fuzz/` has `cargo-fuzz` targets for the whole reader (`preview`), the object stream (`stream`)
+1 -1
View File
@@ -1,4 +1,4 @@
//! Unit tests on synthetic inputs; `tests/real_files.rs` covers public Affinity documents.
//! Unit tests on synthetic inputs; `tests/it/real_files.rs` covers public Affinity documents.
mod preview {
use crate::container::header;
+3 -1
View File
@@ -48,8 +48,10 @@ fn isolated(name: &str, check: impl FnOnce()) {
check();
return;
}
// The harness names a test by its module path below the crate root.
let test = module_path!().split_once("::").map_or(name.to_string(), |(_, m)| format!("{m}::{name}"));
let mut child = std::process::Command::new(std::env::current_exe().unwrap())
.args(["--exact", name, "--nocapture"])
.args(["--exact", &test, "--nocapture"])
.env("PHOTOCRAFT_COMPOUND_CASE", name)
.spawn()
.unwrap();
+1 -1
View File
@@ -75,7 +75,7 @@ sampled gamut boundary. `profiles/photocraft-coated-cmyk.icc` (237 KB, AToB 11
21³ lut16) is that output, dedicated to the public domain (CC0-1.0). Regenerate with
```sh
PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test regen
PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test it -- regen::
```
It is a plausible coated-offset profile, not a measured characterisation: use your printer's
+1 -1
View File
@@ -1,4 +1,4 @@
//! Throughput check: `cargo test -p photocraft-cms --release --test bench -- --ignored --nocapture`.
//! Throughput check: `cargo test -p photocraft-cms --release --test it -- bench:: --ignored --nocapture`.
use photocraft_cms::{Builtin, Intent, Transform};
+1 -1
View File
@@ -1,7 +1,7 @@
//! Regenerates the shipped synthetic CMYK profile when `PHOTOCRAFT_REGEN_PROFILES=1`:
//!
//! ```sh
//! PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test regen
//! PHOTOCRAFT_REGEN_PROFILES=1 cargo test -p photocraft-cms --release --test it -- regen::
//! ```
//!
//! Without the variable the test only checks that the generator's model still matches the
+1 -1
View File
@@ -348,7 +348,7 @@ fn compression_names_round_trip() {
/// Encode timings on a synthetic ~30 MP texture (6336×4752). Ignored by default; run in release:
///
/// ```sh
/// cargo test --release -p photocraft-codecs --test dds_write -- --ignored --nocapture
/// cargo test --release -p photocraft-codecs --test it -- dds_write:: --ignored --nocapture
/// ```
#[test]
#[ignore]
+1 -1
View File
@@ -36,7 +36,7 @@ fn check_format(format: Format) {
}
if c.exif && format == Format::Tiff {
// TIFF writes the block's directories as its own and rebuilds a block from them on
// reading (tests/tiff_exif.rs); IFD0's orientation here is the image's.
// reading (tests/it/tiff_exif.rs); IFD0's orientation here is the image's.
assert!(!warnings.contains(&FidelityWarning::ExifDropped), "{format:?}");
} else if c.exif {
assert_eq!(back.meta.exif, img.meta.exif, "{format:?} exif");
+1 -1
View File
@@ -372,7 +372,7 @@ fn every_layout_and_depth_orients_and_inverts() {
}
/// #285 performance budget: turning a 24 MP photo upright on open adds ≤ 50 ms
/// (release build: `cargo test --release -p photocraft-codecs --test orientation -- --ignored`).
/// (release build: `cargo test --release -p photocraft-codecs --test it -- orientation:: --ignored`).
#[test]
#[ignore = "timing; run in release"]
fn rotating_24_mp_is_fast() {
+1 -1
View File
@@ -2,7 +2,7 @@
//! release:
//!
//! ```sh
//! cargo test --release -p photocraft-codecs --test tiff_bench -- --ignored --nocapture
//! cargo test --release -p photocraft-codecs --test it -- tiff_bench:: --ignored --nocapture
//! ```
use crate::common;
+2 -2
View File
@@ -1,7 +1,7 @@
//! Krita documents opened through the session: read-only source, 16-bit layers, editable
//! layers with history, and saving as OpenRaster or native. The files come from
//! `tools/krita-oracles/generate.py` in https://github.com/storytold/photocraft-corpus. Run with
//! `KRITA_TEST_DIR=<dir> cargo test -p photocraft-engine --test kra -- --ignored --nocapture`.
//! `KRITA_TEST_DIR=<dir> cargo test -p photocraft-engine --test it -- kra:: --ignored --nocapture`.
use photocraft_engine::Session;
use serde_json::json;
@@ -24,7 +24,7 @@ fn krita_documents_open_read_only_and_stay_editable() {
assert!(session.execute("file.save", json!({})).is_err());
// Krita's merged image is the composite (the corpus files carry no ICC profile, so both
// sides are sRGB); crates/io/tests/krita_corpus.rs compares every file.
// sides are sRGB); crates/io/tests/it/krita_corpus.rs compares every file.
let ours = photocraft_compose::flatten(&session.active().unwrap().doc);
let zip_bytes = std::fs::read(&path).unwrap();
let zip = photocraft_format::zip::ZipReader::new(&zip_bytes).unwrap();
+1 -1
View File
@@ -3,7 +3,7 @@
//! `cargo xtask corpus --all`. Feature `corpus` (run with `cargo xtask test-corpus`); a missing
//! corpus fails.
//!
//! The io corpus test (`crates/io/tests/corpus.rs`) flattens imported documents, which composite
//! The io corpus test (`crates/io/tests/it/corpus.rs`) flattens imported documents, which composite
//! Photoshop's own cached pixels for smart objects and type layers. This test throws those caches
//! away: every smart object is re-rendered from its embedded source through our smart-filter
//! stack, and every type layer is re-laid-out and re-rasterised by our text engine; the flatten is
+1 -1
View File
@@ -331,7 +331,7 @@ fn photoshop_samples() {
}
/// PSD export of a 24 MP smart object versus the same layer as pixels (release:
/// `cargo test --release -p photocraft-engine --test smart_psd -- --ignored --nocapture perf`).
/// `cargo test --release -p photocraft-engine --test it -- --ignored --nocapture smart_psd::perf`).
#[test]
#[ignore = "benchmark"]
fn perf_24mp_smart_object_export() {
+1 -2
View File
@@ -8,8 +8,7 @@ use photocraft_geom::{Rect, Size};
use photocraft_gpu::{Compositor, DeviceHealth, Fault, render_to_vec};
use photocraft_raster::Surface;
/// Concurrent wgpu instances in one process segfault on some drivers (see `parity.rs`).
static GPU_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
use crate::GPU_LOCK;
fn device() -> Option<(wgpu::Adapter, wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::default();
+4
View File
@@ -1,5 +1,9 @@
//! The crate's integration tests, linked into one test binary.
/// Concurrent wgpu instances in one process segfault on some drivers (RADV), so every test that
/// creates a device takes this lock and holds it until the device is dropped.
static GPU_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
mod device_loss;
mod parity;
mod timing;
+1 -3
View File
@@ -12,9 +12,7 @@ use photocraft_gpu::{Compositor, render_to_vec};
/// lands on that side at the Hue/Saturation pixel in `adjustment_layers`).
const TOL: f32 = 1.0 / 255.0 + 4.0 * f32::EPSILON;
/// Concurrent wgpu instances in one process segfault on some drivers (RADV), so the GPU tests
/// take this lock and hold it until their device is dropped.
static GPU_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
use crate::GPU_LOCK;
struct Gpu {
device: wgpu::Device,
+5
View File
@@ -111,6 +111,7 @@ fn measure(device: &wgpu::Device, queue: &wgpu::Queue, comp: &mut Compositor, do
#[test]
fn gpu_time_tracks_execution_not_submission() {
let _lock = crate::GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let Some((device, queue, mut comp)) = gpu() else { return };
let size = Size::new(3000, 2000);
let doc = document(size);
@@ -164,6 +165,7 @@ fn gpu_time_tracks_execution_not_submission() {
#[test]
fn an_unmeasured_compositor_reports_nothing() {
let _lock = crate::GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let Some((device, queue, _)) = gpu() else { return };
let mut comp = Compositor::new(&device);
assert!(!comp.measures_gpu_time(), "timing is off until it is asked for");
@@ -175,6 +177,7 @@ fn an_unmeasured_compositor_reports_nothing() {
#[test]
fn a_refresh_that_submits_several_times_is_one_measurement() {
let _lock = crate::GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let Some((device, queue, mut comp)) = gpu() else { return };
// A budget far below the document's layer pages makes the refresh evict as it goes, which is
// what makes it submit in batches: the measurement must span all of them, not just the last.
@@ -208,6 +211,7 @@ fn a_refresh_that_submits_several_times_is_one_measurement() {
#[test]
fn refreshes_faster_than_the_readback_stay_valid() {
let _lock = crate::GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let Some((device, queue, mut comp)) = gpu() else { return };
// Refreshes arriving before the last measurement has been read back must simply go
// unmeasured. Mapping the readback buffer again while its first map is still pending is a
@@ -232,6 +236,7 @@ fn refreshes_faster_than_the_readback_stay_valid() {
/// wrote no timestamps itself — and the compositor would silently never measure again.
#[test]
fn a_failed_refresh_that_recorded_nothing_keeps_the_timer_alive() {
let _lock = crate::GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let Some((device, queue, _)) = gpu() else { return };
let mut timer = timing::GpuTimer::new(&device, &queue).expect("the device has timestamp queries");
+1 -1
View File
@@ -5,7 +5,7 @@ use photocraft_doc::{Document, Fill, Layer, LayerContent};
use photocraft_geom::Size;
use photocraft_gpu::{Compositor, DeviceHealth, plan, render_to_vec};
static GPU_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
use crate::GPU_LOCK;
fn fills(count: usize) -> Document {
fills_size(count, Size::new(1, 1))
+1
View File
@@ -47,6 +47,7 @@ fn painted(name: &str, fmt: PixelFormat, rgba: [f32; 4]) -> Layer {
#[test]
fn vector_masks_match_the_cpu() {
let _lock = crate::GPU_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let Some(mut g) = gpu() else { return };
for depth in [SampleType::U8, SampleType::U16] {
for (inverted, density) in [(false, 1.0), (true, 1.0), (false, 0.6)] {
+1 -1
View File
@@ -30,5 +30,5 @@ inside it (fuzzing found two in 0.1.1) becomes `Error::Malformed`.
The crate depends on no other PhotoCraft crate and knows nothing of `photocraft-codecs`' types;
`codecs` → `heif` is the one allowed dependency between standalone crates (`cargo xtask layers`).
Tests on real files live in `photocraft-codecs` (`tests/heif.rs`, features `corpus,heif`, files
Tests on real files live in `photocraft-codecs` (`tests/it/heif.rs`, features `corpus,heif`, files
fetched by `cargo xtask corpus --heif`).
+1 -1
View File
@@ -2,7 +2,7 @@
//!
//! `cargo run --release -p photocraft-io --example big_psb_check -- <file.psb>...`
//!
//! The opt-in tests in `tests/large_files.rs` generate and check the same files.
//! The opt-in tests in `tests/it/large_files.rs` generate and check the same files.
use photocraft_psd::fixtures::sample;
+1 -1
View File
@@ -437,7 +437,7 @@ fn main() {
}
}
/// The corpus oracle table (as `tests/corpus.rs`), files in parallel, without a test build.
/// The corpus oracle table (as `tests/it/corpus.rs`), files in parallel, without a test build.
fn corpus_summary(root: &std::path::Path) {
fn collect(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
+1 -1
View File
@@ -1,5 +1,5 @@
//! Synthetic reproductions of failures found by the psd-tools corpus
//! (`cargo xtask corpus --psd-tools`; see `tests/corpus.rs`). The corpus
//! (`cargo xtask corpus --psd-tools`; see `tests/it/corpus.rs`). The corpus
//! files themselves are never committed: each test rebuilds the relevant
//! structure from our own documents.
+1 -1
View File
@@ -2,7 +2,7 @@
//! Krita's own rendering of the layer stack (`mergedimage.png`). The files are generated by
//! `tools/krita-oracles/generate.py` in https://github.com/storytold/photocraft-corpus and are
//! not bundled. Run with
//! `KRITA_TEST_DIR=<dir> cargo test -p photocraft-io --test krita_corpus -- --ignored --nocapture`.
//! `KRITA_TEST_DIR=<dir> cargo test -p photocraft-io --test it -- krita_corpus:: --ignored --nocapture`.
use std::path::{Path, PathBuf};
+1 -1
View File
@@ -5,7 +5,7 @@
//!
//! The two layered documents run in memory; the streamed files are written to
//! `$PHOTOCRAFT_FIXTURE_DIR` (default: the system temp dir), removed afterwards, and are opt-in:
//! `cargo test --release -p photocraft-io --test large_files -- --ignored`.
//! `cargo test --release -p photocraft-io --test it -- large_files:: --ignored`.
use std::path::PathBuf;
+1 -1
View File
@@ -25,7 +25,7 @@ fn undecodable_channel_leaves_the_layer_empty() {
/// A flattened 29000² RGB PSB: its merged image decodes to 2.52 GB, past the old 2 GiB cap, which
/// opened it as a document with no layers. Needs about 12 GB of memory, so it is opt-in:
/// `cargo test --release -p photocraft-io --test large_psb -- --ignored`.
/// `cargo test --release -p photocraft-io --test it -- large_psb:: --ignored`.
#[test]
#[ignore = "allocates ~12 GB; run in release"]
fn flattened_psb_with_a_merged_image_over_2_gib_opens() {
+1 -1
View File
@@ -1,5 +1,5 @@
//! Local PDN oracles are not bundled. Set PDN_TEST_DIR to a directory containing PDNs and their
//! exported PNGs, then run `cargo test -p photocraft-io --test pdn_corpus -- --ignored --nocapture`.
//! exported PNGs, then run `cargo test -p photocraft-io --test it -- pdn_corpus:: --ignored --nocapture`.
use std::path::{Path, PathBuf};
+1 -1
View File
@@ -145,7 +145,7 @@ allocations for padded channels return an error on failure.
* Builder → `rgba8` correctness.
* Unknown blend modes and unknown blocks pass through unchanged.
`tests/corpus.rs` walks `corpus/psd/**/*.{psd,psb}` at the workspace root when that directory exists. For each file it asserts that parsing succeeds and the file round-trips byte for byte, and it prints a result per file. Without the directory it skips silently.
`tests/it/corpus.rs` walks `corpus/psd/**/*.{psd,psb}` at the workspace root when that directory exists. For each file it asserts that parsing succeeds and the file round-trips byte for byte, and it prints a result per file. Without the directory it skips silently.
A `cargo-fuzz` skeleton lives in `fuzz/`. It is its own workspace and is excluded from photocraft's. Run it with `cargo +nightly fuzz run parse` (or `descriptor`) from `crates/psd`.
+1 -1
View File
@@ -3,7 +3,7 @@
//!
//! The cap assertions below are invariants, not fast checks: at libFuzzer default `-max_len`
//! of 4096 no input can carry 10 001 gradients or 1001 stops, so they only fire on long runs with
//! a raised limit. The inputs that do reach those caps are in `crates/psd/tests/swatch_malformed.rs`.
//! a raised limit. The inputs that do reach those caps are in `crates/psd/tests/it/swatch_malformed.rs`.
#![no_main]
use libfuzzer_sys::fuzz_target;
use photocraft_psd::grd::{self, GrdColor};
+1 -1
View File
@@ -1,5 +1,5 @@
//! Prints the raw metadata of files: `cargo run -p photocraft-raw --example inspect -- a.cr2 b.nef`
//! (handy while adding corpus files; the oracle expectations in `tests/raw_corpus.rs` come from it).
//! (handy while adding corpus files; the oracle expectations in `tests/it/raw_corpus.rs` come from it).
use std::env;
fn main() {
@@ -1,7 +1,7 @@
//! GPU parity of the Image › Adjustments dialog preview (`adjust_preview`): the document with
//! the temporary clipped adjustment layer must composite on the wgpu compositor like on the CPU
//! reference, and like the destructive command's result, for every kind with a dialog. Skips
//! without an adapter that renders 32-bit float targets (like `crates/gpu/tests/parity.rs`).
//! without an adapter that renders 32-bit float targets (like `crates/gpu/tests/it/parity.rs`).
use eframe::wgpu;
use photocraft_color::{BlendMode, Color, ColorMode, PixelFormat, SampleType};
@@ -1,7 +1,7 @@
//! The canvas is colour-managed (#46): a Display P3 document shows its colours converted to
//! the (sRGB) monitor on the GPU canvas (display LUT) and on the CPU canvas (flipped views draw
//! through the CPU path); sRGB documents are shown unchanged. Skips when no GPU adapter with
//! 32-bit float render targets exists (like `crates/gpu/tests/parity.rs`).
//! 32-bit float render targets exists (like `crates/gpu/tests/it/parity.rs`).
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
+5 -5
View File
@@ -292,7 +292,7 @@ PSD placed layer, the `Idnt` uuid its duplicates share), shared contents (`smart
every copy of the same contents has, and the number of `instances`; Replace Contents changes them
all and lists them in its result), type
text, adjustment settings, channels and history, so agents can verify what they did without a
screenshot. `crates/automation/tests/agent_tasks.rs` is the reference: ten realistic edit tasks
screenshot. `crates/automation/tests/it/agent_tasks.rs` is the reference: ten realistic edit tasks
(title card, colour grade, undo/redo, editable smart blur, masks, saved selections, align,
capability-scoped export, resize/crop, CMYK + native save) driven purely over MCP.
@@ -384,7 +384,7 @@ cargo xtask perf --update-baseline # also write perf/baseline.json from this r
- `perf/baseline.json`: the numbers the scorecard shows, written only by
`cargo xtask perf --update-baseline`, with the machine (CPU, RAM, GPU adapter, OS), its machine
class, commit, date and load average.
- `crates/io/tests/corpus.rs`: the corpus floors (every `Source { .. }` constant).
- `crates/io/tests/it/corpus.rs`: the corpus floors (every `Source { .. }` constant).
- The prefs audit: `Preferences` fields that no code reads (target 0, #204), plus counts taken
from the tree (never-crash attribute coverage, `docs/parity-checklist.md`).
@@ -634,12 +634,12 @@ per source with its own floors: `corpus/psd`, the psd-tools set (also a truncati
over every file that must never panic) and our Photoshop set `corpus/photoshop` (per-feature-group
totals: `smart-filters`, `effects`, `text`, `adjustments/<mode><bits>`). Smart objects and type
layers composite Photoshop's cached pixels there; `cargo xtask test-corpus -p engine -- --nocapture`
(`crates/engine/tests/photoshop_oracles.rs`) re-renders them with our smart-filter stack and text
(`crates/engine/tests/it/photoshop_oracles.rs`) re-renders them with our smart-filter stack and text
engine and is the failure map for both. A panic is reported as `CRASH` and fails the run. Files without a real merged image (Maximize Compatibility off)
are judged against their embedded thumbnail instead (`PASS (thumbnail)`, a strict low-resolution
check); a file only SKIPs when it has no oracle at all. Raise the floors in `crates/io/tests/corpus.rs` when they
check); a file only SKIPs when it has no oracle at all. Raise the floors in `crates/io/tests/it/corpus.rs` when they
improve; never lower them. Synthetic reproductions of corpus findings live in
`crates/io/tests/corpus_regressions.rs` (corpus files are never committed). To dig into one file:
`crates/io/tests/it/corpus_regressions.rs` (corpus files are never committed). To dig into one file:
```sh
cargo run --release -p photocraft-io --example oracle_diff -- corpus/psd/<file>.psd 0 png /tmp/diff.png
+3 -3
View File
@@ -22,7 +22,7 @@ XL are missing (gap G10). Remaining: 300–500 h including PSD fidelity.
| Format | Photoshop | PhotoCraft read | PhotoCraft write | Fidelity and tests |
|---|---|---|---|---|
| PSD | R/W | yes | yes | 1/8/16/32-bit; Bitmap, Gray, Indexed, RGB, CMYK, Multichannel, Lab, Duotone; layers, groups, masks, vector masks, adjustment and fill layers, effects, text, shapes, smart objects (SoLd/lnk2; CS2 PlLd/lnkD read with their warp, kept as is or re-saved as SoLd once edited), smart filters (45 mapped, among them Clouds, Difference Clouds, Fibers and Lens Flare, plus Hue/Saturation, Perspective Warp, Filter Gallery with Chrome and a Camera Raw subset, rest verbatim), slices, comps, Advanced Blending, each layer's XMP packet (`layer.xmpMetadata`). Oracle floors: Photoshop-authored 190/258, io 148/170, psd-tools 249/309; round trip ≥ 169/170, 308/309, 258/256. Known failures: #1281, #2469 (#2374, text layers, closed 2026-10-10; #2465, CS2 `PlLd`-only smart objects, fixed 2026-10-12) |
| PSB | R/W | yes | yes | Auto-switches above 2 GB (REL-212-2); generated 20 MP, 150-layer, 2 GB PSD and 4.4 GB PSB fixtures opened, saved and re-opened by io tests/large_files.rs (FILE-216-2, 2026-10-13; the 4.4 GB PSB opens on Windows); > 4 GB from the app's File › Open on Windows: #375 |
| PSB | R/W | yes | yes | Auto-switches above 2 GB (REL-212-2); generated 20 MP, 150-layer, 2 GB PSD and 4.4 GB PSB fixtures opened, saved and re-opened by io tests/it/large_files.rs (FILE-216-2, 2026-10-13; the 4.4 GB PSB opens on Windows); > 4 GB from the app's File › Open on Windows: #375 |
| Cloud PSD (.psdc) | R/W | no | no | Out of scope (Adobe cloud) |
| TIFF | R/W | yes | yes | 8/16/32f, Gray/RGB/CMYK, BigTIFF, every IFD, banded parallel decode; layered TIFF both byte orders, including Photoshop mixed-order type blocks (#4758); LZW/ZIP for scripted saves by `tiffCompression`; undecodable type keeps saved pixels (FILE-215-7: extra alpha channels not written) |
| JPEG | R/W | yes | yes | 8-bit Gray/RGB/CMYK, ICC/EXIF/XMP; Save keeps the opened file's quality (estimated from its quantisation tables, #2399); File › Save, Save As and Save a Copy ask JPEG Options: Quality 0–12, starting at the opened file's quality or else the Export preference, and Baseline ("Standard") / Baseline Optimized / Progressive; scripts pass `quality`/`jpegFormat`, the CLI `--jpeg-format` (FILE-215-19). Baseline Optimized fits the Huffman tables only at 4:4:4 (Gray, CMYK) or progressive: RGB is written 4:2:0, which zune-jpeg 0.5.15 decodes wrongly with fitted tables, as in Save for Web. No Matte, progressive Scans (3–5) or size preview |
@@ -63,7 +63,7 @@ supports many more, including CR3).
| Camera format | PhotoCraft | Evidence |
|---|---|---|
| DNG (incl. LJPEG, lossy JPEG and JPEG XL tiles) | yes | 2026-10-13: lossy DNG (DNG 1.4 compression 34892: 8-bit baseline JPEG tiles or strips, CFA and LinearRaw, partial edge tiles) decodes to the stored samples, which go through LinearizationTable / BlackLevel / WhiteLevel (default 255) like every other depth; `crates/raw/tests/dng.rs` `lossy_jpeg_*` (FILE-215-11, lossy DNG part; no lossy file in the raw corpus yet) |
| DNG (incl. LJPEG, lossy JPEG and JPEG XL tiles) | yes | 2026-10-13: lossy DNG (DNG 1.4 compression 34892: 8-bit baseline JPEG tiles or strips, CFA and LinearRaw, partial edge tiles) decodes to the stored samples, which go through LinearizationTable / BlackLevel / WhiteLevel (default 255) like every other depth; `crates/raw/tests/it/dng.rs` `lossy_jpeg_*` (FILE-215-11, lossy DNG part; no lossy file in the raw corpus yet) |
| Canon CR2 | yes | |
| Canon CR3 | detected, unsupported | `crates/raw/src/lib.rs:219` |
| Nikon NEF/NRW (lossless, lossy) | yes; "lossy after split" no | #50; D5100 and D90 image areas measured (14-bit NEFs); D4 profile levels follow the sample depth (12 and 14 bit); [validation](nikon-nef-development.md) |
@@ -170,7 +170,7 @@ remaining gaps in #1606 stay tracked with per-sample corpus ceilings.
| 2026-10-14 | minor | PSD smart filter masks: Photoshop stores a smart object's filtered pixels without its filter mask and mixes the unfiltered ones (`FEid`) back in; import now mixes them and export writes them that way, so sf-filter-mask-hard, -gradient and sf-stack-blend-and-mask pass; Photoshop-oracle floor 185 → 188 of 258 |
| 2026-10-13 | minor | Lossy DNG (compression 34892) decodes: 8-bit baseline JPEG tiles and strips, CFA and LinearRaw, partial edge tiles; the stored values take the normal levels and LinearizationTable (FILE-215-11, lossy DNG part) |
| 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 |
| 2026-10-13 | minor | Large-file fixtures generated at test time (20 MP layered, 150-layer layout, 2.02 GB PSD, 4.38 GB PSB), opened, saved and re-opened by io tests/large_files.rs; FILE-216-2 done; P20/P63 open times measurable (#5107) |
| 2026-10-13 | minor | Large-file fixtures generated at test time (20 MP layered, 150-layer layout, 2.02 GB PSD, 4.38 GB PSB), opened, saved and re-opened by io tests/it/large_files.rs; FILE-216-2 done; P20/P63 open times measurable (#5107) |
| 2026-10-13 | minor | CMYK Hue/Saturation (not Colorize) and Color Balance work on C, M and Y as inverted RGB with black kept, on the inks for display, export and destructive edits; cmyk8/16 hue-saturation 100% → 0.4–0.7% bad pixels, color-balance 74% → 25% |
| 2026-10-13 | minor | CMYK Posterize works on the stored values (1 − ink), as Photoshop does: psd-tools posterize_cmyk passes (13.8 % → 0 of pixels off on the ink path); psd-tools floor → 242 of 309 |
| 2026-10-13 | minor | CMYK documents whose layers are plain raster layers plus Invert, Posterize, Threshold, Brightness/Contrast, Levels or Curves layers composite on their own inks for display, export and flattening: the cmyk8/16 oracles for those seven adjustments pass; Photoshop-oracle floor 151 → 165 of 258 |
+4 -4
View File
@@ -46,13 +46,13 @@ what an agent can't finish alone.
constant and transparency is disabled. Variable-range synthesis, HSB/Lab modes and
transparency remain unsupported and are preserved verbatim; the five visual examples do not
establish Photoshop's exact noise algorithm or include this degenerate constant case.
- **Evidence:** corpus floors (`crates/io/tests/corpus.rs`, scorecard): Photoshop-authored
- **Evidence:** corpus floors (`crates/io/tests/it/corpus.rs`, scorecard): Photoshop-authored
oracles 190/258 (74%; 32-bit Hue/Saturation 2026-10-22; smart filter masks 2026-10-14; CMYK and Lab adjustment layers, 32-bit unclipped adjustments, Levels, Threshold, Posterize and mask feathers 2026-10-13; Gradient Map luminosity 2026-10-13; CMYK Invert, Posterize and Threshold on the inks 2026-10-12), io corpus 148/170 (87%), psd-tools 249/309 (81%); round trips ~100%
within the corpus. Smart-filter re-render matches 5/30. User reports: a PSD with text layers
re-saved unmodified would not reopen (#2374, closed 2026-10-10), Photoshop 26.6 cannot reopen our files (#1281), "PSD not
opening in Photoshop" (#2469), a CS2-era smart object unavailable (#2465, fixed 2026-10-12), PSB smart object
blank (#1764), perspective placed layers flattened (#579, fixed by #426), PSB > 4 GB fails on Windows (#375; a generated 4.38 GB PSB opens, saves and re-opens
through io on Windows, io tests/large_files.rs, 2026-10-13).
through io on Windows, io tests/it/large_files.rs, 2026-10-13).
No test opens our exports in a third-party editor (FILE-216-4).
- **Impact:** PSD is the format professionals exchange. Beta requires it to be reliable.
- **Estimate:** 150–250 h. **Human:** opening exports in Photoshop on the owner's Mac (or a
@@ -171,7 +171,7 @@ what an agent can't finish alone.
sample depth (15520 at 14 bits, 3880 at 12), so a 12-bit D4 NEF opens as bright as the same
scene at 14 bits instead of 2.6 stops darker (#5978). Real CC0 pair, mean linear luminance
against the 14-bit file: 12-bit lossy +0.02, uncompressed -0.02, lossless +0.01 stop;
14-bit output unchanged (`cameras.rs`, `tests/nikon_d4_depth.rs`).
14-bit output unchanged (`cameras.rs`, `tests/it/nikon_d4_depth.rs`).
- **Estimate:** 120–200 h.
### G15. AI and generative features
@@ -372,7 +372,7 @@ what an agent can't finish alone.
| 2026-10-14 | minor | G14: a Nikon NEF's Adobe RGB preview is read as Adobe RGB (maker-note ColorSpace 0x001E); gap remains open |
| 2026-10-13 | minor | G14: D90 image area measured (4288 × 2848 at (10, 10)); gap remains open |
| 2026-10-14 | minor | Corpus floors raised to the measured results of the CMYK, Lab, 32-bit, Levels, Threshold, Posterize, feather and pass-through fixes landed together: Photoshop-authored 165 → 185 of 258, io 146 → 148 of 170, psd-tools 242 → 249 of 309 |
| 2026-10-13 | minor | G1: generated large-file fixtures (20 MP, 150 layers, 2 GB PSD, 4.38 GB PSB) round-trip in io tests/large_files.rs; the 4.38 GB PSB opens on Windows through io (#375 evidence); FILE-216-2 done (#5107) |
| 2026-10-13 | minor | G1: generated large-file fixtures (20 MP, 150 layers, 2 GB PSD, 4.38 GB PSB) round-trip in io tests/it/large_files.rs; the 4.38 GB PSB opens on Windows through io (#375 evidence); FILE-216-2 done (#5107) |
| 2026-10-13 | minor | G1: CMYK Hue/Saturation and Color Balance work on C, M and Y as inverted RGB with black kept, as Photoshop does; cmyk hue-saturation 100% → 0.4–0.7% bad pixels, color-balance 74% → 25%; floor unchanged |
| 2026-10-13 | minor | G1: CMYK Posterize works on the stored values (1 − ink); psd-tools posterize_cmyk passes; psd-tools floor → 242 of 309 |
| 2026-10-13 | minor | G1: CMYK stacks with Invert, Posterize, Threshold, Brightness/Contrast, Levels or Curves layers composite on the document's own inks (14 CMYK oracles pass); Photoshop-oracle floor 151 → 165 of 258 |
+4 -4
View File
@@ -54,8 +54,8 @@ corrupted files are covered by tests.
Unit tests (`crates/io/src/kra/tests.rs`) build Krita archives in code (raw and LZF tiles) and
check layers, groups, pass-through, offsets, opacity, visibility, locks, masks, default pixels,
16-bit and gray conversion, the merged-image fallback, LZF edge cases and malformed input.
`crates/automation/tests/krita.rs` opens a `.kra` through the agent surface, edits it and saves
it as OpenRaster. `crates/engine/tests/kra.rs` (opt-in) opens a Krita-authored 16-bit file in a
`crates/automation/tests/it/krita.rs` opens a `.kra` through the agent surface, edits it and saves
it as OpenRaster. `crates/engine/tests/it/kra.rs` (opt-in) opens a Krita-authored 16-bit file in a
session, checks it is read-only, edits with undo and saves as `.ora` and `.pcraft`.
Real-file checks use the Krita-authored oracles in
@@ -64,8 +64,8 @@ Real-file checks use the Krita-authored oracles in
the files are sRGB):
```sh
KRITA_TEST_DIR=/path/to/photocraft-corpus/krita cargo test -p photocraft-io --test krita_corpus -- --ignored --nocapture
KRITA_TEST_DIR=/path/to/photocraft-corpus/krita cargo test -p photocraft-engine --test kra -- --ignored
KRITA_TEST_DIR=/path/to/photocraft-corpus/krita cargo test -p photocraft-io --test it -- krita_corpus:: --ignored --nocapture
KRITA_TEST_DIR=/path/to/photocraft-corpus/krita cargo test -p photocraft-engine --test it -- kra:: --ignored
```
2026-10-09, Krita 5.2.9: 31 of 35 `.kra` files render within 3/255 of Krita's merged image; the
+3 -3
View File
@@ -61,7 +61,7 @@ Unit tests (`crates/io/src/ora/tests.rs`) round-trip layers, groups, pass-throug
opacity, visibility, locks and every blend mode at 8 and 16 bit, check masks, rendered layer
kinds, the archive layout and malformed input. Synthetic 8/16-bit pixel fixtures check modern
and legacy group-isolation defaults, explicit isolation and pass-through.
`crates/automation/tests/openraster.rs` saves,
`crates/automation/tests/it/openraster.rs` saves,
reopens, edits and saves in place through the agent surface, and checks that a document with an
adjustment layer never overwrites its `.ora`; the CLI test converts both ways.
@@ -71,11 +71,11 @@ Real-file checks use Krita-authored oracles from
`mergedimage.png`:
```sh
KRITA_TEST_DIR=/path/to/photocraft-corpus/krita cargo test -p photocraft-io --test krita_corpus -- --ignored --nocapture
KRITA_TEST_DIR=/path/to/photocraft-corpus/krita cargo test -p photocraft-io --test it -- krita_corpus:: --ignored --nocapture
```
2026-10-09, Krita 5.2.9: 32 of 35 `.ora` files render within 3/255 of Krita's merged image; the
other three differ for known reasons listed in `crates/io/tests/krita_corpus.rs` (Krita's
other three differ for known reasons listed in `crates/io/tests/it/krita_corpus.rs` (Krita's
tie-break in Darker and Lighter Color, and Hard Mix at a channel sum of exactly 1). In the other
direction, Krita opened every PhotoCraft re-export of the first 34 of those files and rendered 33
of them identically to its original; the exception is Photoshop's Soft Light, which is written
+2 -2
View File
@@ -32,8 +32,8 @@ padding, reversed chunks, old/new mode records, native saves, cancellation and m
The GPU parity tests include the additional modes. Local real-file checks are opt-in:
```sh
PDN_TEST_DIR=/path/to/pdn-and-png-exports cargo test -p photocraft-io --test pdn_corpus -- --ignored --nocapture
PDN_TEST_FILE=/path/to/layered.pdn cargo test -p photocraft-engine --test pdn -- --ignored
PDN_TEST_DIR=/path/to/pdn-and-png-exports cargo test -p photocraft-io --test it -- pdn_corpus:: --ignored --nocapture
PDN_TEST_FILE=/path/to/layered.pdn cargo test -p photocraft-engine --test it -- pdn:: --ignored
```
Each PDN in `PDN_TEST_DIR` needs a corresponding `.png` export with the same basename. The
+2 -2
View File
@@ -31,7 +31,7 @@ that blocks the workflow, would make PhotoCraft pre-alpha.
| Core workflow | Works end to end? | Evidence | Hours to pass |
|---|---|---|---:|
| Photo retouch: open JPEG/raw, heal/clone/remove, adjustment layers, export JPEG, save and reopen | yes | Spot Healing, Healing, Clone, Patch, Remove tools; 16 adjustment layers; raw for DNG/CR2/NEF/ARW/RAF; JPEG export; MCP workflow tests (`automation/tests/agent_tasks.rs`). Quality complaints (#1092, #2104) and no CR3 don't stop the workflow | 0 |
| Photo retouch: open JPEG/raw, heal/clone/remove, adjustment layers, export JPEG, save and reopen | yes | Spot Healing, Healing, Clone, Patch, Remove tools; 16 adjustment layers; raw for DNG/CR2/NEF/ARW/RAF; JPEG export; MCP workflow tests (`automation/tests/it/agent_tasks.rs`). Quality complaints (#1092, #2104) and no CR3 don't stop the workflow | 0 |
| Composite: layers, masks, selections, blend modes, smart objects; save as PSD and `.pcraft` and reopen | yes | PSD round trip ≥ 169/170, 308/309, 258/256 corpus files; `.pcraft` atomic save and crash recovery. Multi-layer Smart Object conversion (#2604) and mask targeting (#2458) are partial but have workarounds | 0 |
| Cut-out: select a subject, refine the edge, mask, export PNG with transparency | partial (not blocking) | Object/Quick Selection, Select Subject (classical), Refine Edge dialog, layer masks, PNG export. Select and Mask is a dialog, not the workspace; no Refine Hair (#2671) | 0 (depth: 80–140 h, G7) |
| Design: type, shapes, layer styles, artboards; export PNG/JPEG; save and reopen | partial (not blocking) | Point/paragraph type, shapes with stroke controls, all 10 layer effects, artboards, Export As. One macOS report of a 60 s freeze on choosing the Type tool (#2518, not reproduced); no type on a path | 0 (fix #2518: 2–6 h) |
@@ -57,7 +57,7 @@ real work is ~45%, inside the alpha band (~40–75%), but close to its floor.
| M8 PSD v2 | 🟡 | adjustments (incl. Selective Color, Color Lookup), fills, effects, patterns, text, shapes, smart objects, alpha channels; oracle floors 146/170 (io), 236/309 (psd-tools), 133/256 (Photoshop-authored) |
| M9 Text, vector, styles | 🟡 | type engine + Warp Text, shapes / pen / paths, all 10 effects on CPU and GPU (parity ≤1/255, 30/31 corpus effect files on the GPU) |
| M10 Smart features | 🟡 | classical Select Subject / Object, content-aware fill and scale, healing, auto-align / auto-blend; ML backend not started |
| M11 Automation + formats | 🟡 | MCP (headless + live bridge), batch, Image Processor, prefs over MCP; DoD test passes (10 agent tasks over MCP, `automation/tests/agent_tasks.rs`); since 2026-10-07 the CLI rejects unknown flags and answers `<subcommand> --help`, batch runs report same-name outputs instead of overwriting them, MCP `command_batch` steps can start background jobs, and headless saves never flatten over the opened file; JP2 / DICOM / DPX / C2PA pending |
| M11 Automation + formats | 🟡 | MCP (headless + live bridge), batch, Image Processor, prefs over MCP; DoD test passes (10 agent tasks over MCP, `automation/tests/it/agent_tasks.rs`); since 2026-10-07 the CLI rejects unknown flags and answers `<subcommand> --help`, batch runs report same-name outputs instead of overwriting them, MCP `command_batch` steps can start background jobs, and headless saves never flatten over the opened file; JP2 / DICOM / DPX / C2PA pending |
| M12 Pro parity | 🟡 | CMYK / Lab / Indexed / Bitmap / Duotone, ICC + soft proofing, channels + Quick Mask, smart-object stack modes, artboards, layer comps; print, HDR, timeline pending; Photomerge is implemented, with real-world quality validation still open |
Releases: 0.1.0 (2026-10-02), 0.2.0 (2026-10-05, first signed and notarized), 0.3.0 (10-07),
+16 -16
View File
@@ -1,6 +1,6 @@
# PhotoCraft scorecard
Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.json`, `crates/io/tests/corpus.rs`, `scorecard/*.toml` and the source tree. Do not edit by hand: change the sources and regenerate (CI runs `cargo xtask scorecard --check`). Checklist statuses are verified against the code; numbers here supersede estimates elsewhere.
Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.json`, `crates/io/tests/it/corpus.rs`, `scorecard/*.toml` and the source tree. Do not edit by hand: change the sources and regenerate (CI runs `cargo xtask scorecard --check`). Checklist statuses are verified against the code; numbers here supersede estimates elsewhere.
## Summary
@@ -92,7 +92,7 @@ Baseline: 2026-10-05, commit `f5d2bcb54a`, Apple M4 Pro (48 GB), GPU Apple M4 Pr
## File compatibility
Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus tests (cargo feature `corpus`, `cargo xtask test-corpus`, always run in CI) fail below them.
Corpus floors (`crates/io/tests/it/corpus.rs`): the enforced minimums; the corpus tests (cargo feature `corpus`, `cargo xtask test-corpus`, always run in CI) fail below them.
| Corpus | Directory | Files | Oracle pass floor | Round-trip floor |
|---|---|---:|---:|---:|
@@ -102,8 +102,8 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
| Id | Target | Status | Issue | Note |
|---|---|---|---|---|
| FILE-215-1 | Smart Objects export as embedded Smart Objects (SoLd + lnk2), not flat pixels | done | [#215](https://github.com/storytold/photocraft/issues/215) | psd_export.rs smart_blocks: PlLd + SoLd, source in lnk2 (.pcraft -> PSB); engine tests/smart_psd.rs; Photoshop 2026 opens 8/16-bit exports as live smart objects. Linked files are embedded |
| FILE-215-2 | Smart Filters survive PSD save and re-open (filterFX descriptor read and written) | partial | [#215](https://github.com/storytold/photocraft/issues/215) | smart_map.rs: 41 filters plus Hue/Saturation, Perspective Warp, Filter Gallery (Chrome) and a verified Camera Raw subset mapped both ways, others kept verbatim, mask in FEid; io tests/smart_corpus.rs. PhotoCraft-only filters (Twirl, Liquify…) and unmapped Camera Raw settings are left out of filterFX with a warning |
| FILE-215-1 | Smart Objects export as embedded Smart Objects (SoLd + lnk2), not flat pixels | done | [#215](https://github.com/storytold/photocraft/issues/215) | psd_export.rs smart_blocks: PlLd + SoLd, source in lnk2 (.pcraft -> PSB); engine tests/it/smart_psd.rs; Photoshop 2026 opens 8/16-bit exports as live smart objects. Linked files are embedded |
| FILE-215-2 | Smart Filters survive PSD save and re-open (filterFX descriptor read and written) | partial | [#215](https://github.com/storytold/photocraft/issues/215) | smart_map.rs: 41 filters plus Hue/Saturation, Perspective Warp, Filter Gallery (Chrome) and a verified Camera Raw subset mapped both ways, others kept verbatim, mask in FEid; io tests/it/smart_corpus.rs. PhotoCraft-only filters (Twirl, Liquify…) and unmapped Camera Raw settings are left out of filterFX with a warning |
| FILE-215-3 | Filter mask can be painted directly, with undo | missing | [#215](https://github.com/storytold/photocraft/issues/215) | smart_cmds.rs:382 mask only from the selection |
| FILE-215-4 | Adjustments (Hue/Sat + Colorize, Curves, Levels, Shadows/Highlights) apply as Smart Filters | done | [#215](https://github.com/storytold/photocraft/issues/215) | Image › Adjustments on a smart object records image.adjustments.<kind> as a smart filter (adjust_cmds.rs adjust_as_smart_filter; the selection becomes the filter mask), re-rendered from the source by adjust_as_filter and re-editable with layer.smartFilter.setParams; PSD maps Levels/Curves/Shadows-Highlights both ways (smart_map.rs). Opening the editor from the smart-filter row is FILE-215-5's row work |
| FILE-215-5 | Smart Filter rows: collapse, mask thumb, blend icon, drag reorder, context menu, live preview | partial | [#215](https://github.com/storytold/photocraft/issues/215) | smart_ui.rs:31 rows + visibility + editor; no collapse, drag or context menu |
@@ -114,21 +114,21 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
| FILE-215-10 | Multi-page PDF save, PDF presentation, layer comps to PDF, vector/text output, PDF import | partial | [#215](https://github.com/storytold/photocraft/issues/215) | io pdf.rs: Save As / Save a Copy / CLI write one flattened RGB/Gray/CMYK page with ICC (#3401); print PDFs in print_cmds. Multi-page save, presentation, comps, vector/text output and PDF import (#2541) remain |
| FILE-215-11 | RAW: lossy DNG, highlight modes, .xmp sidecars, RAW prefs wired, per-camera corpus | missing | [#215](https://github.com/storytold/photocraft/issues/215) | raw sensor.rs:210 lossy DNG unsupported; raw_defaults unread; no xmp |
| FILE-215-12 | Camera Raw dialog shows a histogram; settings persist as a PSD Smart Filter | partial | [#215](https://github.com/storytold/photocraft/issues/215) | Interactive RGB/Lab scopes. PSD WB/Light/Presence, curves, HSL, grading, sharpening/noise detail, grain and numeric vignette controls mapped with descriptor preservation. Active manual Optics and unverified vignette style keep the updated ACR 18.4 fixture opaque; PSD/.pcraft saves preserve it. HDR, remaining settings and Photoshop acceptance still open |
| FILE-215-13 | Video layer frames persist in .pcraft; PSD video layers mapped | partial | [#215](https://github.com/storytold/photocraft/issues/215) | format convert.rs:89 + tests/roundtrip.rs:282 persist frames; no PSD video mapping |
| FILE-215-13 | Video layer frames persist in .pcraft; PSD video layers mapped | partial | [#215](https://github.com/storytold/photocraft/issues/215) | format convert.rs:89 + tests/it/roundtrip.rs:282 persist frames; no PSD video mapping |
| FILE-215-14 | User swatch model with ACO/ASE import and export | done | [#215](https://github.com/storytold/photocraft/issues/215) | engine presets/swatches.rs (RGB/HSB/CMYK/Lab/Gray + verbatim colour books, persisted in prefs) + swatch_cmds.rs (swatches.*); psd aco.rs (v1/v2) and ase.rs (groups, spot/global) read+write; ui swatches_ui.rs, Color Picker Add to Swatches |
| FILE-215-15 | Layer Style contour picker, curve editor, presets, .shc import/export | partial | [#2912](https://github.com/storytold/photocraft/issues/2912) | Layer Style dialog: contour thumbnail + preset picker (12 standard shapes, own control points) and Anti-aliased on Drop/Inner Shadow, Outer/Inner Glow, Satin, Bevel Gloss Contour and Contour element; Contour Editor (add/drag/delete, Corner, Input/Output, Preset, Load…/Save… .shc per Adobe's public spec, psd shc.rs); corner points render (compose contour_curve_lut) and map to PSD Cnty. User contour library: Contour Editor New… saves the curve under a name (layer.layerStyle.contours.new/list/delete/load/save, kept with the preferences), the picker lists user contours after the standard ones and has Load/Replace/Save Contours… (.shc). Missing: Anti-aliased doesn't change the render yet, .shc not yet checked against a Photoshop-written file |
| FILE-215-16 | Advanced Blending (knockout, blend as group, mask hides effects) end to end | partial | [#215](https://github.com/storytold/photocraft/issues/215) | doc advanced.rs + .pcraft + PSD/layered TIFF (knko infx clbl tsly lmgm vmgm), CPU compose advanced.rs (GPU falls back), blendingOptions command + dialog; 7 psd-tools knockout oracles now pass; knockout-isolated-groups (0.45 % AA edges), advanced-blending, blend-and-clipping still differ |
| FILE-215-17 | CMYK/RGB documents preview spot inks; spot plates export (TIFF extra samples, PDF) | missing | [#215](https://github.com/storytold/photocraft/issues/215) | compose multichannel.rs:1 spot preview for Multichannel only |
| FILE-215-18 | Save for Web per-slice settings remembered; Export As remembers and offers nearest scaling | done | [#215](https://github.com/storytold/photocraft/issues/215) | Save for Web settings survive a restart through the remembered dialog settings in prefs.dialogs (web_cmds::remember_web_settings stores them on a complete export, never the destination; file_ui.rs open_web seeds from the session's last export, else the preferences). export_dialog seeds Export As the same way (seed_remembered: format, quality, transparency, scale, resample, metadata, TGA bits) and confirm remembers them as used. Export As › Image Size › Resample offers Photoshop's six methods (Bicubic Automatic default, Nearest Neighbor included; export_preset_cmds::RESAMPLE, unknown values are errors) for the exported copy only; also layer.exportAs `resample` and export presets (#5838). Tests: export_dialog nearest_neighbor_doubles_each_pixel_into_an_exact_block (8/16-bit), resample_choices_are_checked, resample_is_remembered_and_a_bad_saved_value_falls_back; layer_menu_cmds export_layer_resamples_with_the_chosen_method; tests/export_as_scaling.rs export_as_resample_menu_writes_nearest_neighbor_blocks |
| FILE-215-18 | Save for Web per-slice settings remembered; Export As remembers and offers nearest scaling | done | [#215](https://github.com/storytold/photocraft/issues/215) | Save for Web settings survive a restart through the remembered dialog settings in prefs.dialogs (web_cmds::remember_web_settings stores them on a complete export, never the destination; file_ui.rs open_web seeds from the session's last export, else the preferences). export_dialog seeds Export As the same way (seed_remembered: format, quality, transparency, scale, resample, metadata, TGA bits) and confirm remembers them as used. Export As › Image Size › Resample offers Photoshop's six methods (Bicubic Automatic default, Nearest Neighbor included; export_preset_cmds::RESAMPLE, unknown values are errors) for the exported copy only; also layer.exportAs `resample` and export presets (#5838). Tests: export_dialog nearest_neighbor_doubles_each_pixel_into_an_exact_block (8/16-bit), resample_choices_are_checked, resample_is_remembered_and_a_bad_saved_value_falls_back; layer_menu_cmds export_layer_resamples_with_the_chosen_method; tests/it/export_as_scaling.rs export_as_resample_menu_writes_nearest_neighbor_blocks |
| FILE-215-19 | Save As JPEG opens a JPEG Options dialog (quality 0-12, format options) | partial | [#215](https://github.com/storytold/photocraft/issues/215) | jpeg_options_ui.rs: File > Save, Save As and Save a Copy to a JPEG ask for Quality 0-12 (Low/Medium/High/Maximum) and Baseline / Baseline Optimized / Progressive before writing, starting at an opened JPEG's own quality (file_encoding) or else the Export preference; scripts pass quality/jpegFormat (file.saveAs, file.saveACopy, app.save, CLI --jpeg-format) and recorded actions replay the answer (discard_ui/tests/jpeg_tests.rs, codecs roundtrip jpeg_format_options_round_trip). Baseline Optimized fits the Huffman tables only at 4:4:4 (Gray, CMYK) or progressive: RGB is written 4:2:0, which zune-jpeg 0.5.15 decodes wrongly with fitted tables, as in Save for Web, so RGB Baseline Optimized writes the same file as Baseline. Missing: Matte, progressive Scans (3-5) and the size preview |
| FILE-215-20 | Artboard tool with handles and + buttons, Duplicate Artboard beside, vector PDF pages | partial | [#215](https://github.com/storytold/photocraft/issues/215) | artboard_tool.rs: Artboard tool draws, selects, moves and resizes boards by their handles (⇧ keeps proportions), its + buttons add a board or (⌥) a copy beside the selected one (#3033); Duplicate Artboard places the copy beside (#1584); raster PDF pages only |
| FILE-215-21 | Local libraries of colours, patterns, images and layers, as linked items or copies | missing | [#215](https://github.com/storytold/photocraft/issues/215) | Only preset libraries (preset_store.rs) |
| FILE-216-1 | Every corpus fetched and enforced in CI, cached by pin | done | [#216](https://github.com/storytold/photocraft/issues/216) | ci.yml has a corpus job (name: corpus tests) on every PR and on main: the corpora are pinned in xtask/src/corpus_pins.rs, sha256-verified after both a cache restore and a download, and a missing corpus fails the tests. The cache key follows the pins, so it is cached by pin as this row asks (#190) |
| FILE-216-2 | Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests | done | [#216](https://github.com/storytold/photocraft/issues/216) | Generated at test time by psd fixtures.rs (feature testgen; never committed). io tests/large_files.rs: layered_20_mp_psd_opens_and_saves (5472x3648, 5 layers in 2 groups, mask, drop shadow) and layout_with_150_layers_opens_and_saves (A4 300 ppi, 150 layers in 15 groups, masks, hidden layers, closed groups, drop shadows) run in every cargo test (~2 s each); opt-in psd_just_under_2_gb_opens_and_saves (17,000 px, 2.02 GB PSD) and psb_of_4_gb_opens_and_saves (25,000 px, 4.38 GB PSB, data past 4 GiB), each opened, pixel-checked, saved and re-opened; plus large_psb.rs (2.5 GB merged image). Open times: ui-egui large_file_bench (P20, P63). The 2 GB and 4 GB files are not run in CI (6.4 GB on disk; ~12 GB of memory for the PSD test, ~25 GB for the PSB test) (#5107) |
| FILE-216-3 | TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported | partial | [#216](https://github.com/storytold/photocraft/issues/216) | EXR: corpus/exr (the OpenEXR deep test images, pinned) is read by codecs tests/deep_exr.rs in the CI corpus job, against the ID manifests of the upstream sidecars. corpus/tiff and corpus/raw are still copied in by hand and no test reads them; no per-camera RAW report |
| FILE-216-2 | Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests | done | [#216](https://github.com/storytold/photocraft/issues/216) | Generated at test time by psd fixtures.rs (feature testgen; never committed). io tests/it/large_files.rs: layered_20_mp_psd_opens_and_saves (5472x3648, 5 layers in 2 groups, mask, drop shadow) and layout_with_150_layers_opens_and_saves (A4 300 ppi, 150 layers in 15 groups, masks, hidden layers, closed groups, drop shadows) run in every cargo test (~2 s each); opt-in psd_just_under_2_gb_opens_and_saves (17,000 px, 2.02 GB PSD) and psb_of_4_gb_opens_and_saves (25,000 px, 4.38 GB PSB, data past 4 GiB), each opened, pixel-checked, saved and re-opened; plus large_psb.rs (2.5 GB merged image). Open times: ui-egui large_file_bench (P20, P63). The 2 GB and 4 GB files are not run in CI (6.4 GB on disk; ~12 GB of memory for the PSD test, ~25 GB for the PSB test) (#5107) |
| FILE-216-3 | TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported | partial | [#216](https://github.com/storytold/photocraft/issues/216) | EXR: corpus/exr (the OpenEXR deep test images, pinned) is read by codecs tests/it/deep_exr.rs in the CI corpus job, against the ID manifests of the upstream sidecars. corpus/tiff and corpus/raw are still copied in by hand and no test reads them; no per-camera RAW report |
| FILE-216-4 | Exports re-open in a third-party oracle editor | missing | [#216](https://github.com/storytold/photocraft/issues/216) | The round trip is our export -> our import |
| FILE-216-5 | Save-path tests: layered TIFF, PDF, SVG, ACO/ASE, .shc, Smart Filters, atomic-save failure | partial | [#216](https://github.com/storytold/photocraft/issues/216) | Every save path that exists is tested by saving and reading back: layered TIFF io tests/tiff_layers.rs (every_compression_keeps_the_layers, advanced_blending_survives_layered_tiff_in_both_byte_orders), print PDF engine print_cmds/tests.rs (print_dry_run_renders_a_pdf_and_reports_the_platform_backend decodes the image back), ACO/ASE engine swatch_cmds/tests.rs (aco_round_trip_keeps_16_bit_values_and_books, ase_round_trip_keeps_groups_floats_and_kinds), .shc psd shc.rs shc_round_trips_and_rejects_hostile_input and engine layer_style.rs contour_presets_and_custom_curves_apply_in_one_undo_step, Smart Filters engine tests/smart_psd.rs (smart_filters_survive_psd_in_every_colour_model), atomic-save failure format/src/atomic/tests.rs (BUG-203-6). There is no SVG writer (SVG opens only) and no Photoshop PDF save (FILE-215-10), so those have nothing to test yet; the row stays partial until they exist |
| FILE-216-6 | 30-50 end-to-end workflow acceptance tasks driven through MCP or commands | partial | [#216](https://github.com/storytold/photocraft/issues/216) | automation/tests/agent_tasks.rs, agent_tasks_extended.rs and agent_tasks_more.rs: 36 MCP tasks |
| FILE-216-5 | Save-path tests: layered TIFF, PDF, SVG, ACO/ASE, .shc, Smart Filters, atomic-save failure | partial | [#216](https://github.com/storytold/photocraft/issues/216) | Every save path that exists is tested by saving and reading back: layered TIFF io tests/it/tiff_layers.rs (every_compression_keeps_the_layers, advanced_blending_survives_layered_tiff_in_both_byte_orders), print PDF engine print_cmds/tests.rs (print_dry_run_renders_a_pdf_and_reports_the_platform_backend decodes the image back), ACO/ASE engine swatch_cmds/tests.rs (aco_round_trip_keeps_16_bit_values_and_books, ase_round_trip_keeps_groups_floats_and_kinds), .shc psd shc.rs shc_round_trips_and_rejects_hostile_input and engine layer_style.rs contour_presets_and_custom_curves_apply_in_one_undo_step, Smart Filters engine tests/it/smart_psd.rs (smart_filters_survive_psd_in_every_colour_model), atomic-save failure format/src/atomic/tests.rs (BUG-203-6). There is no SVG writer (SVG opens only) and no Photoshop PDF save (FILE-215-10), so those have nothing to test yet; the row stays partial until they exist |
| FILE-216-6 | 30-50 end-to-end workflow acceptance tasks driven through MCP or commands | partial | [#216](https://github.com/storytold/photocraft/issues/216) | automation/tests/it/agent_tasks.rs, agent_tasks_extended.rs and agent_tasks_more.rs: 36 MCP tasks |
## Tools
@@ -220,7 +220,7 @@ Unread: `general.color_picker`, `workspace.auto_show_hidden_panels`, `workspace.
| TYPE-218-3 | Thai/Khmer/Indic fallback fonts; golden shaping tests for complex scripts and RTL | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Fallback fonts are covered for Thai, Khmer, Lao, Myanmar (THAI_FAMILIES, KHMER_FAMILIES, LAO_FAMILIES, MYANMAR_FAMILIES in fonts.rs; #1909, #3259), the nine Indic scripts (INDIC_FAMILIES: Nirmala UI, Kohinoor and Sangam MN, Noto Sans) and Arabic and Hebrew. Still missing: per-script golden shaping tests against reference output (only smoke tests that an installed font shapes without .notdef, plus a bidi test) |
| TYPE-218-4 | Variable font axis sliders from the font's axes; opsz follows the point size | done | [#218](https://github.com/storytold/photocraft/issues/218) | One slider per axis of the style's font in the Character section (Properties and Window > Character), setting type.setStyle variations; opsz follows the type size unless set (layout.rs variation_list); a style or weight resets the wght axis (#2985). PSD does not round-trip axis values yet |
| TYPE-218-5 | OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus | partial | [#218](https://github.com/storytold/photocraft/issues/218) | Type > OpenType toggles (type_extra_cmds.rs:512). Character panel (and the Properties Character section) has Photoshop's eight OpenType buttons: Standard Ligatures, Contextual Alternates, Discretionary Ligatures, Swash, Stylistic Alternates, Titling Alternates, Ordinals, Fractions, pressed while the feature is on at the selection and dispatching the type.openType.* commands (type_tool.rs opentype_row; character_panel_opentype_buttons_toggle_and_show_the_feature, #3752). Still missing: the Numerals and Stylistic Sets submenus, and greying out features the font lacks |
| TYPE-218-6 | Manual pair kerning (Alt+arrows), optical kerning rendered, both round-trip PSD (#206) | done | [#218](https://github.com/storytold/photocraft/issues/218) | Alt+Left/Right at a collapsed caret kerns the pair (type_tool.rs kern_pair, 20 per step and 100 with Cmd; type_tool_alt_arrows_kern_the_pair). Optical kerning is computed from the glyph outlines in text/src/optical.rs and applied by layout.rs when the run's mode is Optical (optical_matches_photoshop, optical_kerning_tightens_open_pairs). Both modes and manual values round-trip through PSD (io/tests/psd_kerning.rs; the optical mode travels in Txt2, txt2_carries_optical_kerning) |
| TYPE-218-6 | Manual pair kerning (Alt+arrows), optical kerning rendered, both round-trip PSD (#206) | done | [#218](https://github.com/storytold/photocraft/issues/218) | Alt+Left/Right at a collapsed caret kerns the pair (type_tool.rs kern_pair, 20 per step and 100 with Cmd; type_tool_alt_arrows_kern_the_pair). Optical kerning is computed from the glyph outlines in text/src/optical.rs and applied by layout.rs when the run's mode is Optical (optical_matches_photoshop, optical_kerning_tightens_open_pairs). Both modes and manual values round-trip through PSD (io/tests/it/psd_kerning.rs; the optical mode travels in Txt2, txt2_carries_optical_kerning) |
| TYPE-218-7 | Warp Text dialog previews live and opens with the layer's current warp | done | [#218](https://github.com/storytold/photocraft/issues/218) | Type > Warp Text opens with the shown layer's warp, or None, never remembered values (filter_dialog.rs:503 warp_defaults; warp_text_dialog_tests.rs:80 warp_text_opens_with_the_type_layers_current_warp, :99 warp_text_opens_at_none_for_a_layer_without_warp, :108 remembered_warp_values_never_override_the_layers_warp). The dialog previews live without editing the document, Cancel leaves no step and OK is one undoable step (:138 warp_text_dialog_previews_the_warp_without_editing_the_document, :160 cancelling_warp_text_leaves_the_document_and_history_untouched, :179 confirming_warp_text_is_one_undoable_step); the reduced-proxy preview matches the full-size result (filter_dialog.rs:2150 warp_text_preview_matches_the_full_size_result_on_a_reduced_proxy) |
| TYPE-218-8 | Text editing: select all, double/triple/quadruple-click selection, paste, with tests | done | [#218](https://github.com/storytold/photocraft/issues/218) | Like Photoshop, 2 clicks select the word, 3 the laid-out line (a column in vertical type), 4 the paragraph up to the hard return, 5 all text, and the next quick click starts over (#3493; type_tool.rs next_click_count, click_selection, multi_click). Tested through real canvas clicks in point, paragraph and vertical type, with paste replacing the selection and Cmd+A selecting all (type_tool_tests.rs multi_click_selects_word_line_paragraph_and_all_in_point_text, multi_click_separates_wrapped_lines_from_paragraphs, click_counts_cycle_and_restart, click_selection_without_layout_and_on_hostile_input, multi_click_without_a_session_does_nothing) |
| TYPE-218-9 | Distort/Perspective/Warp on live type as in Photoshop (was: offer 'Rasterize and continue?'; Photoshop doesn't prompt) | done | [#218](https://github.com/storytold/photocraft/issues/218) | Photoshop 2026 prompts for none of these: Edit › Transform › Warp on live type stays enabled with no rasterize prompt and offers only the warp styles (the mesh can't be dragged), so the type stays editable; Distort, Perspective and the split warps are greyed. PhotoCraft does the same: Warp on type commits the layer's Warp Text in one step (transform_tool.rs enter_type_warp, warp_cmds.rs warp_type_layer, #3004; warp_on_type_takes_a_style_and_keeps_the_type_editable), Distort/Perspective greyed (enable_rules.rs, #2701) |
@@ -241,7 +241,7 @@ Menu wiring (`docs/parity-checklist.md`): 631 of 631 menu items live (100.0%). T
| AUTO-219-7 | Local ONNX models behind a flag, managed in Preferences; subject select, background removal | missing | [#219](https://github.com/storytold/photocraft/issues/219) | No ONNX runtime dependency |
| AUTO-219-8 | xtask check that every panel button routes through a command id | missing | [#219](https://github.com/storytold/photocraft/issues/219) | No such xtask check |
| AUTO-219-9 | Headless commands for in-canvas type editing, Liquify/Puppet strokes, transform handles | done | [#219](https://github.com/storytold/photocraft/issues/219) | type.hitTest, type.caret, type.navigate; filter.liquify strokes, edit.puppetWarp pins, type.edit, edit.transform |
| AUTO-219-10 | Agent acceptance suite grown to 30-50 workflows | partial | [#219](https://github.com/storytold/photocraft/issues/219) | automation/tests/agent_tasks.rs runs 10 workflows over MCP |
| AUTO-219-10 | Agent acceptance suite grown to 30-50 workflows | partial | [#219](https://github.com/storytold/photocraft/issues/219) | automation/tests/it/agent_tasks.rs runs 10 workflows over MCP |
## Reliability
@@ -288,17 +288,17 @@ Never-crash attribute on 30 of 30 crate roots.
| BUG-204-3 | RAW prefs and ask_before_saving_layered_tiff read, or hidden | done | [#204](https://github.com/storytold/photocraft/issues/204) | ask_before_saving_layered_tiff drives the TIFF Options prompt (ui-egui tiff_options_ui.rs); raw_defaults.open_in_camera_raw drives the Camera Raw dialog (camera_raw_ui.rs); the other raw_defaults fields are in prefs::HIDDEN_UNTIL_IMPLEMENTED |
| BUG-204-4 | Recent files persisted through prefs across restarts | done | [#204](https://github.com/storytold/photocraft/issues/204) | crates/ui-egui/src/lib.rs::push_recent stores the list in preferences; prefs_ui::load restores it and recent_files_survive_a_restart_and_honour_the_count verifies reload |
| BUG-204-5 | Recent-files list honours recent_file_count (default 20) | done | [#204](https://github.com/storytold/photocraft/issues/204) | lib.rs push_recent caps the list with recent_cap (recent_file_count); menus.rs covers default and custom limits |
| BUG-204-6 | Test: every Prefs field is read somewhere or on a hidden allowlist | done | [#204](https://github.com/storytold/photocraft/issues/204) | crates/engine/tests/prefs_usage.rs every_preference_is_read_or_hidden: each field is read outside prefs.rs or listed in prefs::HIDDEN_UNTIL_IMPLEMENTED, never both |
| BUG-204-6 | Test: every Prefs field is read somewhere or on a hidden allowlist | done | [#204](https://github.com/storytold/photocraft/issues/204) | crates/engine/tests/it/prefs_usage.rs every_preference_is_read_or_hidden: each field is read outside prefs.rs or listed in prefs::HIDDEN_UNTIL_IMPLEMENTED, never both |
| BUG-205-1 | Free Transform with a selection moves only the selected part of a linked mask | done | [#205](https://github.com/storytold/photocraft/issues/205) | linked_mask_with_a_selection_moves_only_the_selected_region (transform_cmds.rs; 5a5facc, #242) |
| BUG-205-2 | Live transform preview draws the layer with its mask applied | done | [#205](https://github.com/storytold/photocraft/issues/205) | transform_tool.rs preview_mask + preview_applies_the_linked_mask (5a5facc, #242) |
| BUG-205-3 | edit.transform honours target mask: transforms only a targeted, unlinked mask | done | [#205](https://github.com/storytold/photocraft/issues/205) | targeted_unlinked_mask_transforms_alone and targeted_linked_mask_moves_with_its_layer (transform_cmds.rs) plus the through-the-box tests (transform_tool.rs; 5a5facc, #242) |
| BUG-205-4 | Test: a linked raster mask with no selection moves with the pixels | done | [#205](https://github.com/storytold/photocraft/issues/205) | linked_raster_mask_moves_with_the_pixels (transform_cmds.rs): a reveal-all mask with a hidden half, linked, no selection; asserts the hidden half travels with the pixels and nothing hidden is left behind |
| BUG-205-5 | Tests: linked mask + selection stays aligned; unlinked stays; targeted mask only | done | [#205](https://github.com/storytold/photocraft/issues/205) | All three in transform_cmds.rs: linked_mask_with_a_selection_moves_only_the_selected_region checks the mask value under each moved pixel, unlinked_mask_stays_put covers it with and without a selection, targeted_unlinked_mask_transforms_alone moves the mask while the layer bounds stay |
| BUG-206-1 | CharStyle models per-character manual kerning separately from the auto-kern mode | done | [#206](https://github.com/storytold/photocraft/issues/206) | doc/src/text.rs CharStyle carries both: kerning: Kerning for the automatic mode (Metrics/Optical/Off) and kern: f32 for manual kerning in 1/1000 em between this character and the next, added on top of the pair's automatic kerning |
| BUG-206-2 | PSD import keeps Optical and manual Kerning values | done | [#206](https://github.com/storytold/photocraft/issues/206) | io/tests/psd_kerning.rs manual_and_off_kerning_round_trip_through_psd and optical_kerning_round_trips_through_psd import an exported layer and compare every character's (mode, kern) pair; the optical mode travels in the document Txt2 block (#1348) |
| BUG-206-3 | PSD export writes the real AutoKerning mode and Kerning values | done | [#206](https://github.com/storytold/photocraft/issues/206) | manual_and_off_kerning_round_trip_through_psd (io/tests/psd_kerning.rs) reads the AutoKerning flag and Kerning value of every character back out of the written TySh: (false, 100) after A, (true, -50) after T, and automatic kerning off where the style says so |
| BUG-206-2 | PSD import keeps Optical and manual Kerning values | done | [#206](https://github.com/storytold/photocraft/issues/206) | io/tests/it/psd_kerning.rs manual_and_off_kerning_round_trip_through_psd and optical_kerning_round_trips_through_psd import an exported layer and compare every character's (mode, kern) pair; the optical mode travels in the document Txt2 block (#1348) |
| BUG-206-3 | PSD export writes the real AutoKerning mode and Kerning values | done | [#206](https://github.com/storytold/photocraft/issues/206) | manual_and_off_kerning_round_trip_through_psd (io/tests/it/psd_kerning.rs) reads the AutoKerning flag and Kerning value of every character back out of the written TySh: (false, 100) after A, (true, -50) after T, and automatic kerning off where the style says so |
| BUG-206-4 | Alt+Left/Right at a collapsed caret adjusts pair kerning; Character panel field | done | [#206](https://github.com/storytold/photocraft/issues/206) | type_tool.rs: Alt+Left/Right at a collapsed caret calls kern_pair (20 per step, 100 with Cmd); the Character panel field takes Metrics, Optical or a manual value (kerning_label / parse_kerning). Covered by type_tool_alt_arrows_kern_the_pair and kerning_field_values (input_tests.rs) |
| BUG-206-5 | Tests: PSD round trip of manual and Optical kerning, a real file in the oracle corpus | partial | [#206](https://github.com/storytold/photocraft/issues/206) | Round trips covered by io/tests/psd_kerning.rs (manual, Off and Optical, plus hostile_kerning_values and txt2_extras_survive_a_resave_of_unchanged_text); corpus_text_layers reads real PSDs and applies their Txt2 optical kerning, but no oracle file is dedicated to kerning values |
| BUG-206-5 | Tests: PSD round trip of manual and Optical kerning, a real file in the oracle corpus | partial | [#206](https://github.com/storytold/photocraft/issues/206) | Round trips covered by io/tests/it/psd_kerning.rs (manual, Off and Optical, plus hostile_kerning_values and txt2_extras_survive_a_resave_of_unchanged_text); corpus_text_layers reads real PSDs and applies their Txt2 optical kerning, but no oracle file is dedicated to kerning values |
| BUG-207-1 | Retouch tools respect the paint target (mask, alpha channel, Quick Mask) | done | [#207](https://github.com/storytold/photocraft/issues/207) | run_stroke paints channel_cmds::target_surface; retouch_strokes_paint_the_targeted_mask (retouch_ui.rs) and engine retouch_cmds/tests.rs (e2444ce, #234) |
| BUG-207-2 | Sample All Layers applies to Blur, Sharpen and Smudge | done | [#207](https://github.com/storytold/photocraft/issues/207) | retouch_ui.rs passes sampleAllLayers to paint.blur/sharpen/smudge; sample_all_layers_reaches_the_command (e2444ce, #234) |
| BUG-207-3 | Smudge mixes premultiplied RGBA (no dark fringe at transparent edges) | done | [#207](https://github.com/storytold/photocraft/issues/207) | paint/src/retouch.rs premultiplies; smudge_into_transparency_leaves_no_dark_fringe (e2444ce, #234) |
+2 -2
View File
@@ -438,10 +438,10 @@ filled bar. This extends UI reachability and does not claim Photoshop-authored s
|---|---|---|---|
| Menu wiring | 626/626 menu items dispatch a command (`parity-checklist.md`) | high but shallow | Says nothing about behaviour. |
| PSD fidelity (rendering) | Corpus oracle 115/170 (68%): 30 differ, 26 have no usable reference, 1 import error. 2026-10-07: io corpus 146/170, psd-tools corpus 236/309 (was 229: Advanced Blending knockouts), Photoshop oracles 132/258 | medium | Push to 170/170 under way (effects/strokes, multi-instance effects, 16/32-bit and colour modes, references for skipped files). |
| PSD round trip | 169/169 re-import identically; every adjustment layer and blend mode round-trips | high (within corpus) | Floors in `crates/io/tests/corpus.rs`; raise, never lower. Corpora: `cargo xtask corpus --all` (ours: https://github.com/storytold/photocraft-corpus). |
| PSD round trip | 169/169 re-import identically; every adjustment layer and blend mode round-trips | high (within corpus) | Floors in `crates/io/tests/it/corpus.rs`; raise, never lower. Corpora: `cargo xtask corpus --all` (ours: https://github.com/storytold/photocraft-corpus). |
| OpenRaster read/write | 2026-10-09: 34 Krita 5.2.9-authored `.ora` files (27 blend modes, groups, pass-through, offsets, opacity, visibility, masks, 16-bit, gray) render within 3/255 of Krita's merged image (30) or differ for known reasons (4: Krita's lighter/darker-colour tie-break, Hard Mix at exactly 1, a linear-light 16-bit document); Krita re-opens our exports and renders them identically to its originals (33/34; Soft Light is written as `svg:soft-light`) | medium (one authoring app) | MyPaint and GIMP files untested. Masks are applied to pixels and layer styles dropped on save (reported). See [OpenRaster](ora.md). |
| Paint.NET import | 2026-10-09: 14/14 local PDN3 textures preserve editable layers exactly through `.pcraft` and render within 2.142/255 of full-size previews; a supplied 5.x document (1280×720, 11 layers) passes editing/history/native saves and matches its thumbnail within 0.418/255 mean; all 14 blend modes exercised synthetically | medium (limited corpus) | Import only; metadata omitted. More current-version files and full-size exports needed. See [PDN support](pdn.md). |
| Smart filters / text / effect shapes in PSDs | Measured on our Photoshop-authored set (https://github.com/storytold/photocraft-corpus, `corpus/photoshop`, 258 files): see the per-group floors in `crates/io/tests/corpus.rs` and `crates/engine/tests/photoshop_oracles.rs`. Smart objects and smart filters now survive PSD save and open (41 corpus files, 206 smart objects, round trip strict; Photoshop opens our exports with live filters); re-rendering Photoshop's smart filters with ours matches 5/30 (was 1/30) | low–medium | Remaining re-render gaps are filter maths (Gaussian/Motion Blur, Unsharp Mask, Emboss, Add Noise RNG) and bicubic placement. |
| Smart filters / text / effect shapes in PSDs | Measured on our Photoshop-authored set (https://github.com/storytold/photocraft-corpus, `corpus/photoshop`, 258 files): see the per-group floors in `crates/io/tests/it/corpus.rs` and `crates/engine/tests/it/photoshop_oracles.rs`. Smart objects and smart filters now survive PSD save and open (41 corpus files, 206 smart objects, round trip strict; Photoshop opens our exports with live filters); re-rendering Photoshop's smart filters with ours matches 5/30 (was 1/30) | low–medium | Remaining re-render gaps are filter maths (Gaussian/Motion Blur, Unsharp Mask, Emboss, Add Noise RNG) and bicubic placement. |
| Core editing (layers, masks, selections, adjustments, filters, transforms) | Broad engine coverage; many interaction bugs fixed after 0.2.0 (adjustment dialogs, Curves, crop, Move/Transform modifiers, gesture origin) | medium | Fixes not yet user-validated. |
| UI / UX polish | Shortcut audit 214 → 0 failures; dock, Layers rows and menus reworked; first visual-QA sweep found 14 defects (#147–#157). 2026-10-07: the keyboard-only shortcuts with no menu item (⌥[ ⌥] ⌥, ⌥. layer navigation, ⇧⌥[ ⇧⌥] to extend the selection, 1–0 for opacity and ⇧ for flow or fill, ⇧[ ⇧] hardness, ⌥⌘T to transform a copy and ⌥⇧⌘T to step and repeat), ⌥-click colour sampling with painting tools, double-click a Layers row for Layer Style, File › New from Clipboard, a centred main window and remembered Liquify settings (#352, #417, #350, #368, #419, #418). 2026-10-09: canvas zoom is physical at any display scale — 100% is one document pixel per physical display pixel, and every screen-space overlay, cursor, scrollbar, navigator and fit follows it (#1943) | low–medium | Needs recurring visual QA with realistic documents. |
| Tools | 2026-10-10: Pencil, Mixer Brush, Color Replacement, Patch, Content-Aware Move, Vertical Type, Pattern Stamp and Rotate View are toolbar tools. The only tool still missing is Art History Brush: Frame landed (#2914), Red Eye is a live toolbar tool (`paint.redEye`, TOOL-213-17) and Perspective Crop landed (2026-10-10, #1043, `image.perspectiveCrop`) | low–medium | Patch has a live healing preview. Content-Aware Move is done (`TOOL-213-4`): Move/Extend, Structure/Color, Sample All Layers, Transform On Drop, and a live result preview for every selection the command takes (coarse first for large ones, then exact; 2026-10-16, #6680). Magic/Background Eraser added; live gradients in progress (#180). Magnetic Lasso added 2026-10-08 (live-wire edge tracing, Width/Contrast/Frequency, `select.magneticLasso`). Pattern Stamp added 2026-10-08 (S flyout, `paint.patternStamp`, Aligned / Impressionist). Rotate View (2026-10-09) turns the canvas camera around its centre without rewriting pixels; Reset View and Match Rotation copy the angle. Crop rotation (2026-10-09, #1792): dragging outside the crop frame turns it (⇧ 15° steps, angle readout), and ↵ rotates the document and crops in one undo step (`image.crop` `angle`); the frame turns over the image as in Photoshop's Classic Mode, and the options-bar Straighten is still missing. |
+5 -5
View File
@@ -116,7 +116,7 @@ Triangle and Curvature Pen (minus legacy Rounded Rectangle and Targeted Adjustme
| Single-pixel rows and columns kept on release | partial | #2391 |
| Esc deselects / ⌘Z undoes the last selection | partial | #2674, #1432 |
| Select › Modify live preview | missing | #1752 |
| Marching ants as Photoshop draws them: device pixels, every edge equally thick (1 px at 100 %, 2 px up to 200 %), black/white dashes 4× the thickness, slanted dash ends, same look for drag previews and committed selections | done | `ants.rs`, `tests/marching_ants_pixels.rs` (#7489) |
| Marching ants as Photoshop draws them: device pixels, every edge equally thick (1 px at 100 %, 2 px up to 200 %), black/white dashes 4× the thickness, slanted dash ends, same look for drag previews and committed selections | done | `ants.rs`, `tests/it/marching_ants_pixels.rs` (#7489) |
## Pen, paths and shapes
@@ -141,16 +141,16 @@ Triangle and Curvature Pen (minus legacy Rounded Rectangle and Targeted Adjustme
|---|---|---|
| Drag outside rotates (⇧ 15°), angle readout; ↵ rotates and crops in one undo step | done | TOOL-214-18 |
| Straighten button and ⌘-drag line | done | TOOL-214-21 |
| Default-mode resize snaps handles without camera feedback; image and frame use the same camera in each rendered frame | done | `crop_mode.rs::a_live_resize_snaps_the_handle_without_feedback_from_the_camera`, `tests/crop_default_mode.rs::a_resize_renders_the_frame_and_image_with_the_same_camera` (2026-10-10) |
| Ratio presets and W × H changes immediately fit the current frame around its centre; Swap button and X share the fitted ratio, Enter needs no resize | done | `crop_ui.rs::choosing_a_ratio_fits_and_commits_without_a_resize`, `crop_size.rs::choosing_a_size_with_the_canvas_ratio_commits_without_a_resize`, `tests/crop_default_mode.rs` (2026-10-10) |
| Default-mode resize snaps handles without camera feedback; image and frame use the same camera in each rendered frame | done | `crop_mode.rs::a_live_resize_snaps_the_handle_without_feedback_from_the_camera`, `tests/it/crop_default_mode.rs::a_resize_renders_the_frame_and_image_with_the_same_camera` (2026-10-10) |
| Ratio presets and W × H changes immediately fit the current frame around its centre; Swap button and X share the fitted ratio, Enter needs no resize | done | `crop_ui.rs::choosing_a_ratio_fits_and_commits_without_a_resize`, `crop_size.rs::choosing_a_size_with_the_canvas_ratio_commits_without_a_resize`, `tests/it/crop_default_mode.rs` (2026-10-10) |
| Roomy Crop options bar does not scroll; narrow tool bars retain horizontal scrolling | done | `panels.rs::a_wide_crop_options_bar_does_not_scroll`, `panels.rs::narrow_tool_options_can_scroll_to_the_last_control` (2026-10-10) |
| Overlays (thirds, grid, diagonal, triangle, golden ratio/spiral), O / ⇧O | done | TOOL-214-20 |
| Crop shield, Show Cropped Area (H) | done | TOOL-214-22 |
| Default mode: the image turns and moves behind an upright box; Auto Center Preview; Use Classic Mode | done | TOOL-214-23 |
| W × H × Resolution with units, built-in size presets, Front Image | done | TOOL-214-9 (`crop_size.rs`) |
| Saved custom crop presets (New / Delete Crop Preset, kept with the preferences; moving them to another computer is not offered) | done | TOOL-214-9 (`crop_presets.rs`, `tool.presets.*` with tool `crop`, #2723) |
| Fill: Content-Aware Fill: a turned or extending crop fills the uncovered corners and strips from the image (each piece with its own sampling window), on the Background or else the active pixel layer, in the crop's one undo step; Generative Expand not offered | done | `tests/crop_content_aware.rs`, `crop_ui.rs::commit_passes_the_fill_choice` (#3823, 2026-10-12) |
| Delete Cropped Pixels off: the crop only hides the pixels outside the frame; editing the frame again shows them past the canvas and a recrop handle snaps to their edge | done | `image.crop` `deleteCroppedPixels`, the options-bar checkbox (`panels.rs`), `tests/crop_beyond_canvas.rs::editing_a_crop_frame_shows_the_pixels_past_the_canvas`, `crop_mode.rs::a_recrop_handle_snaps_exactly_to_the_hidden_pixels_edge` (#3638, #3640) |
| Fill: Content-Aware Fill: a turned or extending crop fills the uncovered corners and strips from the image (each piece with its own sampling window), on the Background or else the active pixel layer, in the crop's one undo step; Generative Expand not offered | done | `tests/it/crop_content_aware.rs`, `crop_ui.rs::commit_passes_the_fill_choice` (#3823, 2026-10-12) |
| Delete Cropped Pixels off: the crop only hides the pixels outside the frame; editing the frame again shows them past the canvas and a recrop handle snaps to their edge | done | `image.crop` `deleteCroppedPixels`, the options-bar checkbox (`panels.rs`), `tests/it/crop_beyond_canvas.rs::editing_a_crop_frame_shows_the_pixels_past_the_canvas`, `crop_mode.rs::a_recrop_handle_snaps_exactly_to_the_hidden_pixels_edge` (#3638, #3640) |
## Snapping, guides, rulers, precision
+1 -1
View File
@@ -86,7 +86,7 @@ channels, the colour map, parasites, limits and malformed input.
Real files come from GIMP 3.2.6 itself: `gimp/` of [photocraft-corpus](https://github.com/storytold/photocraft-corpus)
(generator `tools/gimp-xcf/xcf_fixtures.py`, run inside GIMP), fetched into `corpus/gimp/` by
`cargo xtask corpus --gimp` (28 files, 251 KB): procedural layers the tests regenerate sample for
sample, and for each file GIMP's own merged rendering as a PNG. `crates/io/tests/xcf.rs` (feature
sample, and for each file GIMP's own merged rendering as a PNG. `crates/io/tests/it/xcf.rs` (feature
`corpus`, run by `cargo xtask test-corpus -p io`) checks the layer data against the formulas and our
composite against GIMP's:
+1 -1
View File
@@ -79,4 +79,4 @@ id = "AUTO-219-10"
target = "Agent acceptance suite grown to 30-50 workflows"
status = "partial"
issue = 219
note = "automation/tests/agent_tasks.rs runs 10 workflows over MCP"
note = "automation/tests/it/agent_tasks.rs runs 10 workflows over MCP"
+4 -4
View File
@@ -88,7 +88,7 @@ id = "BUG-204-6"
target = "Test: every Prefs field is read somewhere or on a hidden allowlist"
status = "done"
issue = 204
note = "crates/engine/tests/prefs_usage.rs every_preference_is_read_or_hidden: each field is read outside prefs.rs or listed in prefs::HIDDEN_UNTIL_IMPLEMENTED, never both"
note = "crates/engine/tests/it/prefs_usage.rs every_preference_is_read_or_hidden: each field is read outside prefs.rs or listed in prefs::HIDDEN_UNTIL_IMPLEMENTED, never both"
[[item]]
id = "BUG-205-1"
@@ -137,14 +137,14 @@ id = "BUG-206-2"
target = "PSD import keeps Optical and manual Kerning values"
status = "done"
issue = 206
note = "io/tests/psd_kerning.rs manual_and_off_kerning_round_trip_through_psd and optical_kerning_round_trips_through_psd import an exported layer and compare every character's (mode, kern) pair; the optical mode travels in the document Txt2 block (#1348)"
note = "io/tests/it/psd_kerning.rs manual_and_off_kerning_round_trip_through_psd and optical_kerning_round_trips_through_psd import an exported layer and compare every character's (mode, kern) pair; the optical mode travels in the document Txt2 block (#1348)"
[[item]]
id = "BUG-206-3"
target = "PSD export writes the real AutoKerning mode and Kerning values"
status = "done"
issue = 206
note = "manual_and_off_kerning_round_trip_through_psd (io/tests/psd_kerning.rs) reads the AutoKerning flag and Kerning value of every character back out of the written TySh: (false, 100) after A, (true, -50) after T, and automatic kerning off where the style says so"
note = "manual_and_off_kerning_round_trip_through_psd (io/tests/it/psd_kerning.rs) reads the AutoKerning flag and Kerning value of every character back out of the written TySh: (false, 100) after A, (true, -50) after T, and automatic kerning off where the style says so"
[[item]]
id = "BUG-206-4"
@@ -158,7 +158,7 @@ id = "BUG-206-5"
target = "Tests: PSD round trip of manual and Optical kerning, a real file in the oracle corpus"
status = "partial"
issue = 206
note = "Round trips covered by io/tests/psd_kerning.rs (manual, Off and Optical, plus hostile_kerning_values and txt2_extras_survive_a_resave_of_unchanged_text); corpus_text_layers reads real PSDs and applies their Txt2 optical kerning, but no oracle file is dedicated to kerning values"
note = "Round trips covered by io/tests/it/psd_kerning.rs (manual, Off and Optical, plus hostile_kerning_values and txt2_extras_survive_a_resave_of_unchanged_text); corpus_text_layers reads real PSDs and applies their Txt2 optical kerning, but no oracle file is dedicated to kerning values"
[[item]]
id = "BUG-207-1"
+9 -9
View File
@@ -1,5 +1,5 @@
# Scorecard checklist: file compatibility (#215 formats, #216 measurement).
# Corpus numbers come from crates/io/tests/corpus.rs, not from here.
# Corpus numbers come from crates/io/tests/it/corpus.rs, not from here.
# Statuses must be true on main. Verified against 4b53f62 on 2026-10-05 (smart objects: #215 PR).
area = "file"
title = "File compatibility"
@@ -10,14 +10,14 @@ id = "FILE-215-1"
target = "Smart Objects export as embedded Smart Objects (SoLd + lnk2), not flat pixels"
status = "done"
issue = 215
note = "psd_export.rs smart_blocks: PlLd + SoLd, source in lnk2 (.pcraft -> PSB); engine tests/smart_psd.rs; Photoshop 2026 opens 8/16-bit exports as live smart objects. Linked files are embedded"
note = "psd_export.rs smart_blocks: PlLd + SoLd, source in lnk2 (.pcraft -> PSB); engine tests/it/smart_psd.rs; Photoshop 2026 opens 8/16-bit exports as live smart objects. Linked files are embedded"
[[item]]
id = "FILE-215-2"
target = "Smart Filters survive PSD save and re-open (filterFX descriptor read and written)"
status = "partial"
issue = 215
note = "smart_map.rs: 41 filters plus Hue/Saturation, Perspective Warp, Filter Gallery (Chrome) and a verified Camera Raw subset mapped both ways, others kept verbatim, mask in FEid; io tests/smart_corpus.rs. PhotoCraft-only filters (Twirl, Liquify…) and unmapped Camera Raw settings are left out of filterFX with a warning"
note = "smart_map.rs: 41 filters plus Hue/Saturation, Perspective Warp, Filter Gallery (Chrome) and a verified Camera Raw subset mapped both ways, others kept verbatim, mask in FEid; io tests/it/smart_corpus.rs. PhotoCraft-only filters (Twirl, Liquify…) and unmapped Camera Raw settings are left out of filterFX with a warning"
[[item]]
id = "FILE-215-3"
@@ -94,7 +94,7 @@ id = "FILE-215-13"
target = "Video layer frames persist in .pcraft; PSD video layers mapped"
status = "partial"
issue = 215
note = "format convert.rs:89 + tests/roundtrip.rs:282 persist frames; no PSD video mapping"
note = "format convert.rs:89 + tests/it/roundtrip.rs:282 persist frames; no PSD video mapping"
[[item]]
id = "FILE-215-14"
@@ -129,7 +129,7 @@ id = "FILE-215-18"
target = "Save for Web per-slice settings remembered; Export As remembers and offers nearest scaling"
status = "done"
issue = 215
note = "Save for Web settings survive a restart through the remembered dialog settings in prefs.dialogs (web_cmds::remember_web_settings stores them on a complete export, never the destination; file_ui.rs open_web seeds from the session's last export, else the preferences). export_dialog seeds Export As the same way (seed_remembered: format, quality, transparency, scale, resample, metadata, TGA bits) and confirm remembers them as used. Export As › Image Size › Resample offers Photoshop's six methods (Bicubic Automatic default, Nearest Neighbor included; export_preset_cmds::RESAMPLE, unknown values are errors) for the exported copy only; also layer.exportAs `resample` and export presets (#5838). Tests: export_dialog nearest_neighbor_doubles_each_pixel_into_an_exact_block (8/16-bit), resample_choices_are_checked, resample_is_remembered_and_a_bad_saved_value_falls_back; layer_menu_cmds export_layer_resamples_with_the_chosen_method; tests/export_as_scaling.rs export_as_resample_menu_writes_nearest_neighbor_blocks"
note = "Save for Web settings survive a restart through the remembered dialog settings in prefs.dialogs (web_cmds::remember_web_settings stores them on a complete export, never the destination; file_ui.rs open_web seeds from the session's last export, else the preferences). export_dialog seeds Export As the same way (seed_remembered: format, quality, transparency, scale, resample, metadata, TGA bits) and confirm remembers them as used. Export As › Image Size › Resample offers Photoshop's six methods (Bicubic Automatic default, Nearest Neighbor included; export_preset_cmds::RESAMPLE, unknown values are errors) for the exported copy only; also layer.exportAs `resample` and export presets (#5838). Tests: export_dialog nearest_neighbor_doubles_each_pixel_into_an_exact_block (8/16-bit), resample_choices_are_checked, resample_is_remembered_and_a_bad_saved_value_falls_back; layer_menu_cmds export_layer_resamples_with_the_chosen_method; tests/it/export_as_scaling.rs export_as_resample_menu_writes_nearest_neighbor_blocks"
[[item]]
id = "FILE-215-19"
@@ -164,14 +164,14 @@ id = "FILE-216-2"
target = "Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests"
status = "done"
issue = 216
note = "Generated at test time by psd fixtures.rs (feature testgen; never committed). io tests/large_files.rs: layered_20_mp_psd_opens_and_saves (5472x3648, 5 layers in 2 groups, mask, drop shadow) and layout_with_150_layers_opens_and_saves (A4 300 ppi, 150 layers in 15 groups, masks, hidden layers, closed groups, drop shadows) run in every cargo test (~2 s each); opt-in psd_just_under_2_gb_opens_and_saves (17,000 px, 2.02 GB PSD) and psb_of_4_gb_opens_and_saves (25,000 px, 4.38 GB PSB, data past 4 GiB), each opened, pixel-checked, saved and re-opened; plus large_psb.rs (2.5 GB merged image). Open times: ui-egui large_file_bench (P20, P63). The 2 GB and 4 GB files are not run in CI (6.4 GB on disk; ~12 GB of memory for the PSD test, ~25 GB for the PSB test) (#5107)"
note = "Generated at test time by psd fixtures.rs (feature testgen; never committed). io tests/it/large_files.rs: layered_20_mp_psd_opens_and_saves (5472x3648, 5 layers in 2 groups, mask, drop shadow) and layout_with_150_layers_opens_and_saves (A4 300 ppi, 150 layers in 15 groups, masks, hidden layers, closed groups, drop shadows) run in every cargo test (~2 s each); opt-in psd_just_under_2_gb_opens_and_saves (17,000 px, 2.02 GB PSD) and psb_of_4_gb_opens_and_saves (25,000 px, 4.38 GB PSB, data past 4 GiB), each opened, pixel-checked, saved and re-opened; plus large_psb.rs (2.5 GB merged image). Open times: ui-egui large_file_bench (P20, P63). The 2 GB and 4 GB files are not run in CI (6.4 GB on disk; ~12 GB of memory for the PSD test, ~25 GB for the PSB test) (#5107)"
[[item]]
id = "FILE-216-3"
target = "TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported"
status = "partial"
issue = 216
note = "EXR: corpus/exr (the OpenEXR deep test images, pinned) is read by codecs tests/deep_exr.rs in the CI corpus job, against the ID manifests of the upstream sidecars. corpus/tiff and corpus/raw are still copied in by hand and no test reads them; no per-camera RAW report"
note = "EXR: corpus/exr (the OpenEXR deep test images, pinned) is read by codecs tests/it/deep_exr.rs in the CI corpus job, against the ID manifests of the upstream sidecars. corpus/tiff and corpus/raw are still copied in by hand and no test reads them; no per-camera RAW report"
[[item]]
id = "FILE-216-4"
@@ -185,11 +185,11 @@ id = "FILE-216-5"
target = "Save-path tests: layered TIFF, PDF, SVG, ACO/ASE, .shc, Smart Filters, atomic-save failure"
status = "partial"
issue = 216
note = "Every save path that exists is tested by saving and reading back: layered TIFF io tests/tiff_layers.rs (every_compression_keeps_the_layers, advanced_blending_survives_layered_tiff_in_both_byte_orders), print PDF engine print_cmds/tests.rs (print_dry_run_renders_a_pdf_and_reports_the_platform_backend decodes the image back), ACO/ASE engine swatch_cmds/tests.rs (aco_round_trip_keeps_16_bit_values_and_books, ase_round_trip_keeps_groups_floats_and_kinds), .shc psd shc.rs shc_round_trips_and_rejects_hostile_input and engine layer_style.rs contour_presets_and_custom_curves_apply_in_one_undo_step, Smart Filters engine tests/smart_psd.rs (smart_filters_survive_psd_in_every_colour_model), atomic-save failure format/src/atomic/tests.rs (BUG-203-6). There is no SVG writer (SVG opens only) and no Photoshop PDF save (FILE-215-10), so those have nothing to test yet; the row stays partial until they exist"
note = "Every save path that exists is tested by saving and reading back: layered TIFF io tests/it/tiff_layers.rs (every_compression_keeps_the_layers, advanced_blending_survives_layered_tiff_in_both_byte_orders), print PDF engine print_cmds/tests.rs (print_dry_run_renders_a_pdf_and_reports_the_platform_backend decodes the image back), ACO/ASE engine swatch_cmds/tests.rs (aco_round_trip_keeps_16_bit_values_and_books, ase_round_trip_keeps_groups_floats_and_kinds), .shc psd shc.rs shc_round_trips_and_rejects_hostile_input and engine layer_style.rs contour_presets_and_custom_curves_apply_in_one_undo_step, Smart Filters engine tests/it/smart_psd.rs (smart_filters_survive_psd_in_every_colour_model), atomic-save failure format/src/atomic/tests.rs (BUG-203-6). There is no SVG writer (SVG opens only) and no Photoshop PDF save (FILE-215-10), so those have nothing to test yet; the row stays partial until they exist"
[[item]]
id = "FILE-216-6"
target = "30-50 end-to-end workflow acceptance tasks driven through MCP or commands"
status = "partial"
issue = 216
note = "automation/tests/agent_tasks.rs, agent_tasks_extended.rs and agent_tasks_more.rs: 36 MCP tasks"
note = "automation/tests/it/agent_tasks.rs, agent_tasks_extended.rs and agent_tasks_more.rs: 36 MCP tasks"
+1 -1
View File
@@ -44,7 +44,7 @@ id = "TYPE-218-6"
target = "Manual pair kerning (Alt+arrows), optical kerning rendered, both round-trip PSD (#206)"
status = "done"
issue = 218
note = "Alt+Left/Right at a collapsed caret kerns the pair (type_tool.rs kern_pair, 20 per step and 100 with Cmd; type_tool_alt_arrows_kern_the_pair). Optical kerning is computed from the glyph outlines in text/src/optical.rs and applied by layout.rs when the run's mode is Optical (optical_matches_photoshop, optical_kerning_tightens_open_pairs). Both modes and manual values round-trip through PSD (io/tests/psd_kerning.rs; the optical mode travels in Txt2, txt2_carries_optical_kerning)"
note = "Alt+Left/Right at a collapsed caret kerns the pair (type_tool.rs kern_pair, 20 per step and 100 with Cmd; type_tool_alt_arrows_kern_the_pair). Optical kerning is computed from the glyph outlines in text/src/optical.rs and applied by layout.rs when the run's mode is Optical (optical_matches_photoshop, optical_kerning_tightens_open_pairs). Both modes and manual values round-trip through PSD (io/tests/it/psd_kerning.rs; the optical mode travels in Txt2, txt2_carries_optical_kerning)"
[[item]]
id = "TYPE-218-7"
+4 -4
View File
@@ -4,7 +4,7 @@
//! - `perf/budgets.toml` + `perf/baseline.json`: the performance table (the baseline's full-run
//! numbers against the budgets). A fresh `cargo xtask perf` result in `target/` is not used:
//! publish it with `cargo xtask perf --update-baseline`.
//! - `crates/io/tests/corpus.rs`: the corpus floors (`Source { .. }` constants).
//! - `crates/io/tests/it/corpus.rs`: the corpus floors (`Source { .. }` constants).
//! - `scorecard/*.toml`: per-area checklists (id, target, status, issue, note).
//! - the prefs audit: `Preferences` fields that no code outside `prefs.rs` / `prefs_ui.rs` reads.
//! - counted from the tree: never-crash attribute coverage, `docs/parity-checklist.md`'s live count.
@@ -21,7 +21,7 @@ use serde_json::Value;
use crate::perf;
pub const OUT: &str = "docs/scorecard.md";
const CORPUS_RS: &str = "crates/io/tests/corpus.rs";
const CORPUS_RS: &str = "crates/io/tests/it/corpus.rs";
const PREFS_RS: &str = "crates/engine/src/prefs.rs";
// ---- checklists ------------------------------------------------------------------------------
@@ -522,7 +522,7 @@ pub fn render(inp: &Inputs) -> String {
let mut s = String::from(
"# PhotoCraft scorecard\n\nGenerated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.json`, \
`crates/io/tests/corpus.rs`, `scorecard/*.toml` and the source tree. Do not edit by hand: change the sources and \
`crates/io/tests/it/corpus.rs`, `scorecard/*.toml` and the source tree. Do not edit by hand: change the sources and \
regenerate (CI runs `cargo xtask scorecard --check`). Checklist statuses are verified against the code; numbers \
here supersede estimates elsewhere.\n\n## Summary\n\n| Area | Done | Partial | Missing | Key numbers |\n|---|---:|---:|---:|---|\n",
);
@@ -589,7 +589,7 @@ rows are targets the code doesn't meet yet: they are reported, and only their re
s.push_str(&format!("\n## {}\n\n", c.title));
match c.area.as_str() {
"file" => {
s.push_str("Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus tests (cargo feature `corpus`, `cargo xtask test-corpus`, always run in CI) fail below them.\n\n| Corpus | Directory | Files | Oracle pass floor | Round-trip floor |\n|---|---|---:|---:|---:|\n");
s.push_str("Corpus floors (`crates/io/tests/it/corpus.rs`): the enforced minimums; the corpus tests (cargo feature `corpus`, `cargo xtask test-corpus`, always run in CI) fail below them.\n\n| Corpus | Directory | Files | Oracle pass floor | Round-trip floor |\n|---|---|---:|---:|---:|\n");
for k in &inp.corpora {
let n = |v: Option<u32>| v.map_or_else(|| "–".into(), |x| x.to_string());
s.push_str(&format!("| {} | `{}` | {} | {} | {} |\n", k.label, k.dir, n(k.files), n(k.pass_floor), n(k.roundtrip_floor)));