Files
photocraft/crates/automation/tests/krita.rs
David MannandClaude Opus 5.5 977cf5f6de Krita: uncompressed tiles are read as interleaved pixels (#7082) (#7083)
* Krita: uncompressed tiles are read as interleaved pixels (#7082)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* kra: the agent-surface test writes its raw tile interleaved, like Krita (#7082)

The krita.rs fixture wrote its flag-0 tile as byte planes, which only the old
decoder read back correctly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:33:19 -04:00

70 lines
3.6 KiB
Rust

//! Krita documents through the agent surface (`doc.open`, `engine.execute`, `doc.save`, as MCP
//! and the CLI use it): open a .kra with its layers, edit it, and save it as OpenRaster.
use photocraft_automation::Headless;
use serde_json::json;
/// A tiled pixel file with one raw 64x64 tile of `bgra` at (0, 0). Krita writes a raw tile
/// (flag 0) as plain interleaved pixels; only LZF tiles are split into byte planes.
fn tiles(bgra: [u8; 4]) -> Vec<u8> {
let n = 64 * 64;
let mut out = b"VERSION 2\nTILEWIDTH 64\nTILEHEIGHT 64\nPIXELSIZE 4\nDATA 1\n".to_vec();
out.extend(format!("0,0,LZF,{}\n", 1 + n * 4).into_bytes());
out.push(0);
for _ in 0..n {
out.extend(bgra);
}
out
}
fn kra() -> Vec<u8> {
let maindoc = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE DOC PUBLIC '-//KDE//DTD krita 2.0//EN' 'http://www.calligra.org/DTD/krita-2.0.dtd'>
<DOC xmlns="http://www.calligra.org/DTD/krita" syntaxVersion="2.0"><IMAGE name="art" width="32" height="24" colorspacename="RGBA" x-res="72" y-res="72"><layers>
<layer nodetype="grouplayer" name="Inks" filename="layer1" compositeop="normal" opacity="255" visible="1"><layers>
<layer nodetype="paintlayer" name="Line" colorspacename="RGBA" filename="layer2" compositeop="multiply" opacity="204" visible="1"/>
</layers></layer>
<layer nodetype="shapelayer" name="Speech bubble" filename="layer3"/>
<layer nodetype="paintlayer" name="Paper" colorspacename="RGBA" filename="layer4" compositeop="normal" opacity="255" visible="1"/>
</layers></IMAGE></DOC>"#;
let mut z = photocraft_format::zip::ZipWriter::new();
z.add("mimetype", b"application/x-krita").unwrap();
z.add("maindoc.xml", maindoc.as_bytes()).unwrap();
z.add("art/layers/layer2", &tiles([0, 0, 255, 255])).unwrap();
z.add("art/layers/layer4", &tiles([255, 255, 255, 255])).unwrap();
z.finish().unwrap()
}
fn exec(h: &mut Headless, command: &str, params: serde_json::Value) -> serde_json::Value {
h.handle("engine.execute", json!({"command": command, "params": params})).unwrap()
}
#[test]
fn krita_documents_open_with_layers_and_save_as_openraster() {
let dir = std::env::temp_dir().join(format!("pc-krita-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("art.kra");
std::fs::write(&path, kra()).unwrap();
let mut h = Headless::trusted_local();
let opened = h.handle("doc.open", json!({"path": path.to_str().unwrap()})).unwrap();
let warnings = opened["warnings"].to_string();
assert!(warnings.contains("vector layers") && warnings.contains("Speech bubble"), "{opened}");
let doc = &h.session.active().unwrap().doc;
let names: Vec<&str> = doc.layers.iter().map(|l| l.name.as_str()).collect();
assert_eq!(names, ["Paper", "Inks"]);
let line = doc.layers[1].children().unwrap()[0].clone();
assert_eq!((line.name.as_str(), line.blend), ("Line", photocraft_doc::BlendMode::Multiply));
let flat = photocraft_compose::flatten(doc);
assert!((flat.px[0][1] - 0.2).abs() < 0.01, "80% red multiplied over white: {:?}", flat.px[0]);
// Read-only source: the file itself is never overwritten as Krita.
assert!(h.handle("doc.save", json!({"path": path.to_str().unwrap()})).is_err());
exec(&mut h, "layer.setProps", json!({"layer": line.id.0, "opacity": 0.5}));
let ora = dir.join("art.ora");
h.handle("doc.save", json!({"path": ora.to_str().unwrap()})).unwrap();
let back = photocraft_io::import("art.ora", &std::fs::read(&ora).unwrap()).unwrap().document;
assert_eq!(back.layers.len(), 2);
assert_eq!(back.layers[1].children().unwrap()[0].opacity, 0.5);
let _ = std::fs::remove_dir_all(&dir);
}