mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 11:16:53 -04:00
Corpus: pin the deep OpenEXR test images and check them in CI (#881)
The deep EXR decoder (#642) was verified against the OpenEXR project's deep test images, but those lived in a hand-copied `corpus/exr/` and the test skipped when they were absent, so CI never read them. `cargo xtask corpus --exr` (and `--all`) now fetches the five `*.deep.exr` files of openexr v3.5.2 `src/test/bin/test_images`, sha256-verified against `xtask/exr-corpus.sha256`. `real_deep_files_decode` is behind the `corpus` feature and fails when the corpus is missing. It checks each file's sample total, largest per-pixel count, cumulative counts and channel lengths, and that the decoded u32 ID channels hold exactly the IDs listed by the upstream `.txt` sidecars (an independent oracle), plus the flattened decode. FILE-216-3 moves to partial (EXR read in CI; TIFF and RAW still are not).
This commit is contained in:
1 parent
05b405aab0
commit
964a41dc02
8 files changed
+121
-28
No files matched your search
@@ -79,6 +79,7 @@ next to the files:
|
||||
| `corpus/psd-tools/` (309 files) | psd-tools test set | Kota Yamaguchi and contributors | [psd-tools `tests/psd_files`](https://github.com/psd-tools/psd-tools/tree/main/tests/psd_files) | MIT, Copyright (c) 2019 Kota Yamaguchi |
|
||||
| `corpus/psd/` (170 files) | Small selection of the psd-tools and ag-psd test files | Kota Yamaguchi; Agamnentzar | psd-tools (above) and [ag-psd `test/`](https://github.com/Agamnentzar/ag-psd/tree/master/test) | MIT (both) |
|
||||
| `corpus/heif/` (9 files, 0.1 MB) | HEIC/HEIF test files: `heic-rs/` checkerboards, RGB strips and a grid-tiled photo with EXIF and XMP (synthetic pixels encoded by macOS `sips`, each `.ref.png` Apple's decode); `pillow-heif/` the 10-bit RGBA `RGBA_10__29x100.heif` and its source `RGBA_16__29x100.png` | Thomas Braun (heic-rs); Pillow-Heif contributors | [heic-rs `tests/fixtures`](https://github.com/tbraun96/heic-rs/tree/main/tests/fixtures), [pillow-heif `tests/images`](https://github.com/bigcat88/pillow_heif/tree/master/tests/images) | MIT OR Apache-2.0 (heic-rs); BSD-3-Clause (pillow-heif) |
|
||||
| `corpus/exr/` (5 files, 2.3 MB) | The deep OpenEXR test images `11`, `42`, `64`, `multivariate` and `objectid.deep.exr` | Contributors to the OpenEXR Project | [openexr `src/test/bin/test_images`](https://github.com/AcademySoftwareFoundation/openexr/tree/main/src/test/bin/test_images) | BSD-3-Clause |
|
||||
| `corpus/pngsuite/` | PngSuite | Willem van Schaik | <http://www.schaik.com/pngsuite/> | Public domain |
|
||||
|
||||
Files copied into `corpus/` by hand (tiff, exr, raw) must be MIT, BSD or CC0.
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Deep EXR: a synthetic generator builds deep chunks per the OpenEXR file layout
|
||||
//! specification (NONE, RLE and ZIPS × scanline and tile), the oracle tests check the
|
||||
//! compositing math against hand-computed values, and malformed inputs must error, never
|
||||
//! panic. Real files from `corpus/exr/` (copied in by hand, not pinned) are checked when
|
||||
//! they exist.
|
||||
//! panic. With the `corpus` feature, the OpenEXR project's deep test images (`corpus/exr/`,
|
||||
//! pinned) are checked against the ID manifests their sidecars list.
|
||||
|
||||
use photocraft_codecs::CodecError;
|
||||
use photocraft_codecs::f16;
|
||||
@@ -637,26 +637,75 @@ fn inconsistent_count_table_errors() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Real reference files (corpus/exr, copied in by hand from the OpenEXR repository's test
|
||||
// images; not pinned by xtask). Skipped with a message when the folder is absent.
|
||||
// Real reference files: the deep test images of the OpenEXR repository, pinned in
|
||||
// `xtask/src/corpus_pins.rs` and fetched into `corpus/exr/` by `cargo xtask corpus --exr`
|
||||
// (cargo feature `corpus`; `cargo xtask test-corpus`). Their `.txt` sidecars upstream list the
|
||||
// ID manifest of each file, an independent oracle for the u32 ID channels.
|
||||
|
||||
/// One pinned file: name, total samples, most samples in one pixel, and per ID channel the
|
||||
/// manifest IDs its sidecar lists.
|
||||
#[cfg(feature = "corpus")]
|
||||
type RealFile = (&'static str, u64, u64, &'static [(&'static str, &'static [u32])]);
|
||||
|
||||
#[cfg(feature = "corpus")]
|
||||
const MULTIVARIATE_IDS: &[u32] = &[
|
||||
127035619, 147085119, 319332210, 409096490, 523461226, 678492822, 683859976, 1058109025, 1193275200, 1503246798, 1552124883, 1613501244, 1672208113,
|
||||
1687665471, 1741895317, 1841370478, 1863854195, 1865183722, 2056745377, 2202049807, 2303547305, 2369798380, 2433329986, 2439705590, 2505186565, 2698909330,
|
||||
2862333315, 2898564507, 2979371527, 3007860936, 3182339638, 3236172513, 3497437340, 3626859046, 3656820528, 3693645783, 3739501011, 3936563811, 4048943790,
|
||||
];
|
||||
|
||||
#[cfg(feature = "corpus")]
|
||||
const REAL_FILES: &[RealFile] = &[
|
||||
(
|
||||
"11.deep.exr",
|
||||
6056,
|
||||
2,
|
||||
&[("id", &[523461226, 683859976, 1552124883, 2202049807, 2303547305, 2433329986, 2698909330, 3182339638, 3236172513, 3626859046, 3739501011])],
|
||||
),
|
||||
(
|
||||
"42.deep.exr",
|
||||
67605,
|
||||
8,
|
||||
&[
|
||||
("modelid", &[147148503, 1252544961, 1917926828, 3170049167, 3338960663, 3411631724]),
|
||||
("materialid", &[489905694, 507819813, 2340321182, 3683581971, 3905155331, 3947898621, 4179825792]),
|
||||
],
|
||||
),
|
||||
// 64-bit IDs split over `.id0`/`.id1`: only the sample counts are checked.
|
||||
("64.deep.exr", 64543, 6, &[]),
|
||||
("multivariate.deep.exr", 64543, 6, &[("id", MULTIVARIATE_IDS)]),
|
||||
// The same samples as multivariate; its sidecar has no manifest.
|
||||
("objectid.deep.exr", 64543, 6, &[]),
|
||||
];
|
||||
|
||||
#[cfg(feature = "corpus")]
|
||||
#[test]
|
||||
fn real_deep_files_decode() {
|
||||
let dir = std::path::Path::new("../../corpus/exr");
|
||||
let entries: Vec<_> =
|
||||
std::fs::read_dir(dir).into_iter().flatten().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|e| e == "exr")).collect();
|
||||
if entries.is_empty() {
|
||||
eprintln!("skipped: no files in corpus/exr (copy the *.deep.exr test images there)");
|
||||
return;
|
||||
}
|
||||
for path in entries {
|
||||
let bytes = std::fs::read(&path).expect("read");
|
||||
let deep = decode_deep_exr(&bytes, &Limits::none()).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
|
||||
assert_eq!((deep.width, deep.height), (img.width(), img.height()));
|
||||
assert!(deep.total_samples() > 0, "{}: no samples", path.display());
|
||||
let flat = img.to_f32_samples().expect("f32");
|
||||
assert_eq!(flat.len(), deep.width as usize * deep.height as usize * 4);
|
||||
assert!(img.warnings.iter().any(|w| matches!(w, photocraft_codecs::DecodeWarning::DeepFlattened { .. })));
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../corpus/exr");
|
||||
for &(name, total, max_per_pixel, ids) in REAL_FILES {
|
||||
let path = dir.join(name);
|
||||
let bytes = std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e} (run `cargo xtask corpus --exr`)", path.display()));
|
||||
let deep = decode_deep_exr(&bytes, &Limits::none()).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!((deep.width, deep.height), (256, 256), "{name}");
|
||||
assert_eq!(deep.total_samples(), total, "{name}");
|
||||
assert_eq!(deep.counts.len(), 256 * 256 + 1, "{name}");
|
||||
assert!(deep.counts.windows(2).all(|w| w[0] <= w[1]), "{name}: counts must be cumulative");
|
||||
assert_eq!(deep.counts.windows(2).map(|w| w[1] - w[0]).max(), Some(max_per_pixel), "{name}");
|
||||
for c in &deep.channels {
|
||||
assert_eq!(c.samples.len() as u64, total, "{name}: channel {}", c.name);
|
||||
}
|
||||
// Every decoded ID is one the sidecar lists, and every listed ID occurs. Samples hold
|
||||
// u32 IDs as f32 (exact only to 2^24), so compare at f32 precision.
|
||||
for &(channel, manifest) in ids {
|
||||
let c = deep.channel(channel).unwrap_or_else(|| panic!("{name}: no channel {channel}"));
|
||||
let want: std::collections::BTreeSet<u32> = manifest.iter().map(|&id| (id as f32).to_bits()).collect();
|
||||
let got: std::collections::BTreeSet<u32> = c.samples.iter().map(|v| v.to_bits()).collect();
|
||||
assert_eq!(got, want, "{name}: channel {channel}");
|
||||
}
|
||||
// The flat decode composites the same samples.
|
||||
let img = decode(&bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!((img.width(), img.height()), (deep.width, deep.height), "{name}");
|
||||
assert_eq!(img.to_f32_samples().expect("f32").len(), 256 * 256 * 4, "{name}");
|
||||
assert!(img.warnings.iter().any(|w| matches!(w, photocraft_codecs::DecodeWarning::DeepFlattened { .. })), "{name}");
|
||||
}
|
||||
}
|
||||
@@ -335,6 +335,7 @@ against committed **sha256 manifests**. All pins are in one place:
|
||||
| `corpus/psd/` | 170 small psd-tools and ag-psd files, the mix most PSD tests use | psd-tools and ag-psd upstreams (MIT) | `xtask/psd-corpus.sha256` |
|
||||
| `corpus/psd-tools/` | the complete psd-tools test set (309 files) | psd-tools upstream (MIT) | `xtask/psd-tools-corpus.sha256` |
|
||||
| `corpus/heif/` | 9 small HEIC/HEIF files (checkerboards, RGB strips, a grid-tiled photo with EXIF/XMP, each with Apple's decode as `.ref.png`; a 10-bit RGBA file with its source PNG), for the `heif` feature | heic-rs (MIT OR Apache-2.0) and pillow-heif (BSD-3-Clause) upstreams | `xtask/heif-corpus.sha256` |
|
||||
| `corpus/exr/` | the 5 deep OpenEXR test images (scanline deep data with half colour and u32 ID channels; 2.3 MB), checked against the ID manifests of their upstream sidecars | OpenEXR upstream at v3.5.2 (BSD-3-Clause) | `xtask/exr-corpus.sha256` |
|
||||
| `corpus/pngsuite/` | PngSuite | schaik.com release archive (public domain) | (fixed archive) |
|
||||
|
||||
```sh
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
|
||||
| Area | Done | Partial | Missing | Key numbers |
|
||||
|---|---:|---:|---:|---|
|
||||
| [Performance](#performance) | 3 | 22 | 13 | 47 scenarios: 3 within budget, 22 over budget or failing, 15 tracked only, 6 not run, 7 not measurable yet (baseline 2026-10-05) |
|
||||
| [File compatibility](#file-compatibility) | 3 | 9 | 15 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 229/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
|
||||
| [File compatibility](#file-compatibility) | 3 | 10 | 14 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 229/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
|
||||
| [Tools](#tools) | 6 | 15 | 9 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 0 | 5 | 18 | settings that do nothing: 57 of 137 (target 0) |
|
||||
| [Type](#type) | 0 | 3 | 6 | |
|
||||
@@ -112,7 +112,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
||||
| 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 | missing | [#216](https://github.com/storytold/photocraft/issues/216) | No corpus step in ci.yml; in progress in #190 |
|
||||
| FILE-216-2 | Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests | partial | [#216](https://github.com/storytold/photocraft/issues/216) | 4-5 GB PSB fixtures: psd example big_psb writes them (raw and RLE, planes past 2 GiB), io example big_psb_check opens and checks them; opt-in io tests/large_psb.rs (2.5 GB merged image). No 2 GB PSD, 150-layer or 20 MP fixture, nothing in CI |
|
||||
| FILE-216-3 | TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported | missing | [#216](https://github.com/storytold/photocraft/issues/216) | corpus/tiff, raw and exr only listed by xtask corpus; no test reads them |
|
||||
| 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-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 | missing | [#216](https://github.com/storytold/photocraft/issues/216) | Features absent; no atomic-save failure-injection test |
|
||||
| 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: 10 MCP tasks |
|
||||
|
||||
@@ -169,9 +169,9 @@ note = "4-5 GB PSB fixtures: psd example big_psb writes them (raw and RLE, plane
|
||||
[[item]]
|
||||
id = "FILE-216-3"
|
||||
target = "TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported"
|
||||
status = "missing"
|
||||
status = "partial"
|
||||
issue = 216
|
||||
note = "corpus/tiff, raw and exr only listed by xtask corpus; no test reads them"
|
||||
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"
|
||||
|
||||
[[item]]
|
||||
id = "FILE-216-4"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
800ec94ffb2085da9a730dcda2f9611f5fb62f922a3f2aef77287e36106b9667 11.deep.exr
|
||||
439907f3448480894264f0c8847f6c572f645441db34a5ad9f36f3f020d101b5 42.deep.exr
|
||||
4d9bfa646323b80630933e114a3112095488e8c008cdf908e210979638e65fe4 64.deep.exr
|
||||
2335ed6052258fe693cb795f2bea57f22ba0564281691c82f2987d31d96e9e74 multivariate.deep.exr
|
||||
af8940912bfa856b2650d2005462fd348656385161ccf0745b91b143e934c5f6 objectid.deep.exr
|
||||
+11
-3
@@ -4,7 +4,7 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::corpus_pins::{self, AG_PSD_COMMIT, HEIC_RS_COMMIT, PHOTOCRAFT_CORPUS_COMMIT, PILLOW_HEIF_COMMIT, PNGSUITE_URL, PSD_TOOLS_COMMIT};
|
||||
use crate::corpus_pins::{self, AG_PSD_COMMIT, HEIC_RS_COMMIT, OPENEXR_COMMIT, PHOTOCRAFT_CORPUS_COMMIT, PILLOW_HEIF_COMMIT, PNGSUITE_URL, PSD_TOOLS_COMMIT};
|
||||
use crate::pinned::USER_AGENT;
|
||||
use crate::{cargo, root, run};
|
||||
|
||||
@@ -17,7 +17,7 @@ const HEIF_CRATES: &[&str] = &["photocraft-codecs", "photocraft-io"];
|
||||
/// Paths whose changes make `test-corpus --changed` run (the file-format and rendering crates).
|
||||
const CRITICAL: &[&str] = &["crates/psd/", "crates/io/", "crates/codecs/", "crates/heif/", "crates/compose/", "crates/gpu/", "crates/text/", "crates/format/"];
|
||||
|
||||
/// `cargo xtask corpus [--all | --pngsuite | --download | --psd | --psd-tools | --heif | --photoshop [--local]] [--update-manifest]`
|
||||
/// `cargo xtask corpus [--all | --pngsuite | --download | --psd | --psd-tools | --heif | --exr | --photoshop [--local]] [--update-manifest]`
|
||||
pub fn cmd(args: &[&str]) -> Result<(), String> {
|
||||
let update = args.contains(&"--update-manifest");
|
||||
let mut did = false;
|
||||
@@ -40,6 +40,10 @@ pub fn cmd(args: &[&str]) -> Result<(), String> {
|
||||
corpus_pins::HEIF.fetch(update)?;
|
||||
did = true;
|
||||
}
|
||||
if args.contains(&"--exr") {
|
||||
corpus_pins::EXR.fetch(update)?;
|
||||
did = true;
|
||||
}
|
||||
if args.contains(&"--photoshop") {
|
||||
if args.contains(&"--local") {
|
||||
corpus_pins::PHOTOSHOP.fetch_local(&local_clone()?, update)?;
|
||||
@@ -105,8 +109,10 @@ fn list() {
|
||||
manifest xtask/psd-tools-corpus.sha256. Fetch: --psd-tools
|
||||
corpus/heif/ [{}] a few HEIC/HEIF files from heic-rs@{} (MIT OR Apache-2.0) and
|
||||
pillow-heif@{} (BSD-3-Clause), manifest xtask/heif-corpus.sha256. Fetch: --heif
|
||||
corpus/exr/ [{}] the deep OpenEXR test images (BSD-3-Clause), openexr@{}
|
||||
manifest xtask/exr-corpus.sha256. Fetch: --exr
|
||||
corpus/pngsuite/ [{}] PngSuite (public domain), {PNGSUITE_URL}. Fetch: --pngsuite
|
||||
corpus/tiff/, corpus/exr/, corpus/raw/ optional, copied in by hand
|
||||
corpus/tiff/, corpus/raw/ optional, copied in by hand
|
||||
|
||||
Pins: xtask/src/corpus_pins.rs. Moving one: change it, then --<name> --update-manifest.",
|
||||
corpus.display(),
|
||||
@@ -118,6 +124,8 @@ Pins: xtask/src/corpus_pins.rs. Moving one: change it, then --<name> --update-ma
|
||||
status(corpus_pins::HEIF.is_current()),
|
||||
&HEIC_RS_COMMIT[..12],
|
||||
&PILLOW_HEIF_COMMIT[..12],
|
||||
status(corpus_pins::EXR.is_current()),
|
||||
&OPENEXR_COMMIT[..12],
|
||||
status(png_ok),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ pub const AG_PSD_COMMIT: &str = "387049670cb89b88fb8fe1b7c01aeacf98dd2e3b";
|
||||
pub const HEIC_RS_COMMIT: &str = "4f0d4df474c773dfc3b14fa80e7219be6898866e";
|
||||
/// https://github.com/bigcat88/pillow_heif (BSD-3-Clause): `tests/images` (master, 2026-09-25).
|
||||
pub const PILLOW_HEIF_COMMIT: &str = "b16be1196dfa465a342d68894696e685ca3655cb";
|
||||
/// https://github.com/AcademySoftwareFoundation/openexr (BSD-3-Clause): `src/test/bin/test_images`
|
||||
/// at the v3.5.2 tag (2026-10-03).
|
||||
pub const OPENEXR_COMMIT: &str = "69b2604fc76e370615438bdc8d2cd95b9349c12e";
|
||||
/// PngSuite (public domain), a fixed release archive.
|
||||
pub const PNGSUITE_URL: &str = "http://www.schaik.com/pngsuite/PngSuite-2017jul19.tgz";
|
||||
|
||||
@@ -134,5 +137,31 @@ pub const HEIF: PinnedCorpus = PinnedCorpus {
|
||||
},
|
||||
};
|
||||
|
||||
/// `corpus/exr`: the deep OpenEXR test images (scanline and tiled deep data, several channel
|
||||
/// types) the deep decoder was verified against. The manifest selects the files.
|
||||
pub const EXR: PinnedCorpus = PinnedCorpus {
|
||||
name: "exr",
|
||||
dest: "exr",
|
||||
upstreams: &[Upstream {
|
||||
prefix: "",
|
||||
repo: "AcademySoftwareFoundation/openexr",
|
||||
commit: OPENEXR_COMMIT,
|
||||
subdir: "src/test/bin/test_images",
|
||||
extras: &[("LICENSE.md", "LICENSE.md")],
|
||||
}],
|
||||
manifest: "xtask/exr-corpus.sha256",
|
||||
exts: &["exr"],
|
||||
subset: true,
|
||||
sources_md: |c| {
|
||||
format!(
|
||||
"# Deep OpenEXR test corpus
|
||||
|
||||
The `*.deep.exr` files of https://github.com/AcademySoftwareFoundation/openexr/tree/{OPENEXR_COMMIT}/src/test/bin/test_images, unmodified. BSD-3-Clause, Copyright Contributors to the OpenEXR Project (see `LICENSE.md`). Selected by and sha256-verified against `{}` by `cargo xtask corpus --exr`. Gitignored; never commit these files.
|
||||
",
|
||||
c.manifest
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
/// Every pinned corpus, in fetch order.
|
||||
pub const ALL: &[&PinnedCorpus] = &[&PSD_MIXED, &PSD_TOOLS, &PHOTOSHOP, &HEIF];
|
||||
pub const ALL: &[&PinnedCorpus] = &[&PSD_MIXED, &PSD_TOOLS, &PHOTOSHOP, &HEIF, &EXR];
|
||||
Reference in new issue
Block a user