mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
- 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.
98 lines
5.1 KiB
Rust
98 lines
5.1 KiB
Rust
//! Krita-authored oracles: OpenRaster (`.ora`) and Krita (`.kra`) files whose archives carry
|
|
//! 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 it -- krita_corpus:: --ignored --nocapture`.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn files(dir: &Path, ext: &str) -> Vec<PathBuf> {
|
|
let mut out: Vec<PathBuf> =
|
|
std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().path()).filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case(ext))).collect();
|
|
out.sort();
|
|
out
|
|
}
|
|
|
|
/// Krita's merged image, read straight out of the archive.
|
|
fn merged(bytes: &[u8]) -> photocraft_doc::Document {
|
|
let zip = photocraft_format::zip::ZipReader::new(bytes).unwrap();
|
|
let png = zip.read_by_name("mergedimage.png", 1 << 28).unwrap();
|
|
photocraft_io::import("mergedimage.png", &png).unwrap().document
|
|
}
|
|
|
|
/// Largest premultiplied channel difference (0..1) between our composite and Krita's, and
|
|
/// where it is (x, y, ours, Krita's).
|
|
fn max_error(doc: &photocraft_doc::Document, oracle: &photocraft_doc::Document, tolerance: f32) -> (f32, f32, String) {
|
|
assert_eq!(doc.size, oracle.size);
|
|
let ours = photocraft_compose::flatten(doc);
|
|
let theirs = photocraft_compose::flatten(oracle);
|
|
let w = doc.size.width as usize;
|
|
let mut worst = (0.0f32, String::new());
|
|
let mut over = 0usize;
|
|
for (i, (a, b)) in ours.px.iter().zip(&theirs.px).enumerate() {
|
|
let e = (0..4).map(|c| if c == 3 { (a[3] - b[3]).abs() } else { (a[c] * a[3] - b[c] * b[3]).abs() }).fold(0.0f32, f32::max);
|
|
over += usize::from(e > tolerance);
|
|
if e > worst.0 {
|
|
let q = |p: &[f32; 4]| p.map(|v| (v * 255.0).round() as i32);
|
|
worst = (e, format!("at ({}, {}): ours {:?}, Krita {:?}", i % w, i / w, q(a), q(b)));
|
|
}
|
|
}
|
|
(worst.0, over as f32 / ours.px.len().max(1) as f32, worst.1)
|
|
}
|
|
|
|
/// Files whose difference is known: the file, the share of pixels allowed outside the tolerance,
|
|
/// and why. Everything else must match within the tolerance everywhere.
|
|
const KNOWN: &[(&str, f32, &str)] = &[
|
|
("blend-darker_color.ora", 0.02, "Krita weighs the channels differently when it picks the darker colour; near-ties pick the other pixel"),
|
|
("blend-lighter_color.ora", 0.02, "Krita weighs the channels differently when it picks the lighter colour; near-ties pick the other pixel"),
|
|
("blend-hard_mix_photoshop.ora", 0.02, "a channel sum of exactly 1 goes up in Photoshop's Hard Mix (ours) and down in Krita's"),
|
|
("depth-rgba-u16.ora", 1.0, "Krita blends this linear-light 16-bit document in linear light, but its ORA export stores sRGB 8-bit layers"),
|
|
("blend-darker_color.kra", 0.02, "as for the .ora"),
|
|
("blend-lighter_color.kra", 0.02, "as for the .ora"),
|
|
("blend-hard_mix_photoshop.kra", 0.02, "as for the .ora"),
|
|
("nodes-vector-unsupported.kra", 0.2, "vector layers are left out of .kra imports (reported); Krita's merged image has the shape"),
|
|
];
|
|
|
|
fn run(ext: &str, tolerance: f32) {
|
|
let root = std::env::var_os("KRITA_TEST_DIR").expect("set KRITA_TEST_DIR");
|
|
let paths = files(Path::new(&root), ext);
|
|
assert!(!paths.is_empty(), "no .{ext} files found");
|
|
let (mut passed, mut failed) = (0, Vec::new());
|
|
for path in &paths {
|
|
let bytes = std::fs::read(path).unwrap();
|
|
let name = path.file_name().unwrap().to_string_lossy().into_owned();
|
|
let r = photocraft_io::import(&name, &bytes).unwrap_or_else(|e| panic!("{name}: {e}"));
|
|
let (error, share, at) = max_error(&r.document, &merged(&bytes), tolerance);
|
|
// Saving as .pcraft keeps every imported layer exactly.
|
|
let native = photocraft_io::export(&r.document, "copy.pcraft", &Default::default()).unwrap();
|
|
assert_eq!(r.document.layers, photocraft_io::import("copy.pcraft", &native.bytes).unwrap().document.layers, "{name}");
|
|
let known = KNOWN.iter().find(|(n, ..)| *n == name);
|
|
eprintln!(
|
|
"{name}: {} layers, max error {:.2}/255 {at}{}{}",
|
|
r.document.layers.len(),
|
|
error * 255.0,
|
|
known.map(|(_, _, w)| format!(" ({:.1}% of pixels; known: {w})", share * 100.0)).unwrap_or_default(),
|
|
if r.warnings.is_empty() { String::new() } else { format!("\n warnings: {}", r.warnings.join("; ")) }
|
|
);
|
|
if error <= tolerance {
|
|
passed += 1;
|
|
} else if known.is_none_or(|(_, allowed, _)| share > *allowed) {
|
|
failed.push(format!("{name}: {:.2}/255 on {:.2}% of pixels", error * 255.0, share * 100.0));
|
|
}
|
|
}
|
|
eprintln!("{passed}/{} .{ext} files within {:.1}/255", paths.len(), tolerance * 255.0);
|
|
assert!(failed.is_empty(), "outside tolerance: {failed:#?}");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires KRITA_TEST_DIR with Krita-authored .ora files"]
|
|
fn krita_ora_files_match_their_merged_image() {
|
|
run("ora", 3.0 / 255.0);
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires KRITA_TEST_DIR with Krita-authored .kra files"]
|
|
fn krita_kra_files_match_their_merged_image() {
|
|
run("kra", 3.0 / 255.0);
|
|
}
|