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:
Tanish Manku authored and GitHub committed 2026-10-08 16:08:56 -04:00
1 parent 05b405aab0
commit 964a41dc02
8 files changed
+121 -28

No files matched your search

+1
View File
@@ -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.
+69 -20
View File
@@ -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}");
}
}
+1
View File
@@ -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
View File
@@ -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 |
+2 -2
View File
@@ -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"
+5
View File
@@ -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
View File
@@ -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),
);
}
+30 -1
View File
@@ -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];