107 lines
5.2 KiB
Rust
107 lines
5.2 KiB
Rust
//! TIFF export through `document.export`: the options reach the files, impossible
|
||
//! ones are refused, and the files open again.
|
||
|
||
use serde_json::{Value, json};
|
||
|
||
use super::*;
|
||
|
||
/// A 60 × 40 pt document with a black (100 % K) rectangle on a transparent corner.
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 60, "height": 40})).unwrap();
|
||
s.execute("paint.setStroke", &json!({"none": true})).unwrap();
|
||
s.execute("paint.setFill", &json!({"color": {"c": 0, "m": 0, "y": 0, "k": 1}})).unwrap();
|
||
s.execute("shape.rectangle", &json!({"x": 20, "y": 0, "width": 40, "height": 40})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn export(s: &mut Session, p: Value) -> Vec<u8> {
|
||
let r = s.execute("document.export", &p).unwrap();
|
||
vectorcraft_format::base64_decode(r["dataBase64"].as_str().expect("dataBase64")).unwrap()
|
||
}
|
||
|
||
/// A TIFF's first directory: tag → (type, count, value or offset), in the file's byte order.
|
||
fn tags(f: &[u8]) -> std::collections::HashMap<u16, (u16, u32, u32)> {
|
||
let big = &f[..2] == b"MM";
|
||
let u16_at = |at: usize| {
|
||
let b = [f[at], f[at + 1]];
|
||
if big { u16::from_be_bytes(b) } else { u16::from_le_bytes(b) }
|
||
};
|
||
let u32_at = |at: usize| {
|
||
let b = f[at..at + 4].try_into().unwrap();
|
||
if big { u32::from_be_bytes(b) } else { u32::from_le_bytes(b) }
|
||
};
|
||
let dir = u32_at(4) as usize;
|
||
(0..u16_at(dir) as usize)
|
||
.map(|i| {
|
||
let e = dir + 2 + i * 12;
|
||
let (ty, count) = (u16_at(e + 2), u32_at(e + 4));
|
||
// A lone SHORT sits in the first half of the value field.
|
||
let value = if ty == 3 && count == 1 { u32::from(u16_at(e + 8)) } else { u32_at(e + 8) };
|
||
(u16_at(e), (ty, count, value))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
#[test]
|
||
fn tiff_options_reach_the_file() {
|
||
let mut s = session();
|
||
let tiff = |s: &mut Session, p: Value| export(s, merge(json!({"format": "tiff"}), p));
|
||
let rgb = tiff(&mut s, json!({}));
|
||
assert_eq!(&rgb[..4], b"II*\0", "little-endian by default");
|
||
let t = tags(&rgb);
|
||
assert_eq!((t[&259].2, t[&262].2, t[&277].2), (5, 2, 4), "LZW, RGB, an alpha channel for the transparent corner");
|
||
assert!(t.contains_key(&34675), "the sRGB profile");
|
||
let img = image::load_from_memory_with_format(&rgb, image::ImageFormat::Tiff).unwrap().to_rgba8();
|
||
assert_eq!(img.dimensions(), (60, 40));
|
||
assert_eq!(img.get_pixel(5, 5).0[3], 0, "transparent where nothing is drawn");
|
||
let black = img.get_pixel(40, 20).0;
|
||
assert!(black[3] == 255 && black[0] < 80, "{black:?}");
|
||
|
||
let mm = tiff(&mut s, json!({"byteOrder": "big", "lzw": false, "embedIcc": false, "background": "white", "ppi": 300}));
|
||
assert_eq!(&mm[..4], b"MM\0*");
|
||
let t = tags(&mm);
|
||
assert_eq!((t[&259].2, t[&277].2, t[&296].2), (1, 3, 2), "uncompressed, opaque RGB, inches");
|
||
assert!(!t.contains_key(&34675));
|
||
let x_res = t[&282].2 as usize;
|
||
assert_eq!(&mm[x_res..x_res + 8], [300u32.to_be_bytes(), 1u32.to_be_bytes()].concat(), "300/1 ppi");
|
||
assert!(tiff(&mut s, json!({"background": "white"})).len() < mm.len() / 3 * 4, "LZW is smaller");
|
||
|
||
// CMYK: four samples of ink; the black rectangle is black ink alone, the rest no ink.
|
||
let cmyk = tiff(&mut s, json!({"colorModel": "cmyk", "lzw": false}));
|
||
let t = tags(&cmyk);
|
||
assert_eq!((t[&262].2, t[&277].2, t[&332].2), (5, 4, 1));
|
||
let strip = t[&273];
|
||
let first = if strip.1 == 1 { strip.2 } else { u32::from_le_bytes(cmyk[strip.2 as usize..][..4].try_into().unwrap()) } as usize;
|
||
let px = |x: usize, y: usize| &cmyk[first + (y * 60 + x) * 4..][..4];
|
||
assert_eq!((px(40, 0), px(5, 0)), (&[0, 0, 0, 255][..], &[0, 0, 0, 0][..]));
|
||
|
||
let gray = tiff(&mut s, json!({"colorModel": "gray"}));
|
||
assert_eq!((tags(&gray)[&262].2, tags(&gray)[&277].2), (1, 1), "grey, no alpha");
|
||
assert!(s.execute("document.export", &json!({"format": "tiff", "byteOrder": "middle"})).is_err());
|
||
// The file opens again.
|
||
let r = s.execute("document.open", &json!({"name": "back.tif", "dataBase64": vectorcraft_format::base64_encode(&mm)})).unwrap();
|
||
assert_eq!(r["format"], "tiff");
|
||
for file in [&cmyk, &gray, &rgb] {
|
||
s.execute("document.open", &json!({"name": "back.tif", "dataBase64": vectorcraft_format::base64_encode(file)})).unwrap();
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_formats_are_listed_and_write_one_file_per_artboard() {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 30, "height": 20, "artboards": 2})).unwrap();
|
||
let r = s.execute("document.formats", &json!({})).unwrap();
|
||
let find = |id: &str| r["formats"].as_array().unwrap().iter().find(|f| f["id"] == id).unwrap().clone();
|
||
let tiff = find("tiff");
|
||
assert_eq!((tiff["write"].as_bool(), tiff["read"].as_bool()), (Some(true), Some(true)));
|
||
for (f, keys) in [(&tiff, &["colorModel", "lzw", "byteOrder", "embedIcc", "ppi", "antiAlias"][..])] {
|
||
for k in keys {
|
||
assert!(f["options"].get(*k).is_some(), "{} takes {k}", f["id"]);
|
||
}
|
||
}
|
||
let r = s.execute("document.export", &json!({"format": "tiff", "useArtboards": true})).unwrap();
|
||
let files: Vec<&str> = r["files"].as_array().unwrap().iter().map(|f| f["name"].as_str().unwrap()).collect();
|
||
assert_eq!(files, ["Untitled-1-Artboard-1.tif", "Untitled-1-Artboard-2.tif"]);
|
||
}
|