mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-26 19:07:06 -06:00
155 lines
8.1 KiB
Rust
155 lines
8.1 KiB
Rust
//! Indexed Color and Duotone documents in flat exports.
|
|
|
|
use photocraft_color::{Color, ColorMode, SampleType};
|
|
use photocraft_doc::{ColorTable, Document, Duotone, DuotoneInk, Size};
|
|
|
|
fn doc(mode: ColorMode) -> Document {
|
|
let mut d = Document::with_background("m", Size::new(4, 2), ColorMode::Rgb, SampleType::U8, Color::rgba(1.0, 0.0, 0.0, 1.0));
|
|
d.mode = mode;
|
|
d
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_is_palette_png() {
|
|
let mut d = doc(ColorMode::Indexed);
|
|
d.color_table = Some(ColorTable { colors: vec![[0, 0, 0], [255, 0, 0]], transparent: None });
|
|
let r = photocraft_io::export(&d, "x.png", &Default::default()).unwrap();
|
|
// IHDR colour type 3 = indexed; a PLTE chunk follows.
|
|
assert_eq!(r.bytes[25], 3);
|
|
assert!(r.bytes.windows(4).any(|w| w == b"PLTE"));
|
|
let back = photocraft_io::import("x.png", &r.bytes).unwrap().document;
|
|
let px = back.layers[0].surface().unwrap().rgba(1, 1);
|
|
assert_eq!(&px[..3], &[1.0, 0.0, 0.0]);
|
|
// Other formats keep the expanded pixels.
|
|
let j = photocraft_io::export(&d, "x.tif", &Default::default()).unwrap();
|
|
assert!(!j.bytes.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_keeps_duplicate_palette_colors_opaque() {
|
|
// PLTE entries may have identical RGB but different tRNS alpha. RGB-only lookup used to
|
|
// choose the first red entry for both pixels, making the opaque one disappear on save.
|
|
let palette = [[255, 0, 0], [0, 255, 0], [255, 0, 0], [0, 0, 255]];
|
|
for transparent in [0, 2] {
|
|
let png = photocraft_codecs::encode_png_indexed(4, 1, &[0, 1, 2, 3], &palette, Some(transparent), None).unwrap();
|
|
let d = photocraft_io::import("in.png", &png).unwrap().document;
|
|
assert_eq!(d.mode, ColorMode::Indexed);
|
|
let before = d.color_table.clone();
|
|
let r = photocraft_io::export(&d, "out.png", &Default::default()).unwrap();
|
|
let table = photocraft_codecs::png_palette(&r.bytes).unwrap().unwrap();
|
|
assert_eq!(table.colors, palette, "palette order must not change");
|
|
assert_eq!(table.transparent, Some(transparent));
|
|
assert_eq!(photocraft_codecs::decode(&r.bytes).unwrap().data(), photocraft_codecs::decode(&png).unwrap().data());
|
|
let back = photocraft_io::import("out.png", &r.bytes).unwrap().document;
|
|
assert_eq!(back.color_table, before);
|
|
assert_eq!(d.color_table, before, "export must not edit the document's table");
|
|
for x in 0..4 {
|
|
assert_eq!(back.layers[0].surface().unwrap().rgba(x, 0)[3], if x == i32::from(transparent) { 0.0 } else { 1.0 });
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_uses_the_nearest_opaque_color() {
|
|
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
|
for transparent in [0, 1, 2] {
|
|
let mut palette = vec![[0, 0, 255], [255, 128, 0]];
|
|
palette.insert(usize::from(transparent), [255, 0, 0]);
|
|
let mut d = Document::with_background("m", Size::new(1, 1), ColorMode::Rgb, depth, Color::rgba(1.0, 0.0, 0.0, 1.0));
|
|
d.mode = ColorMode::Indexed;
|
|
d.color_table = Some(ColorTable { colors: palette.clone(), transparent: Some(transparent) });
|
|
let r = photocraft_io::export(&d, "out.png", &Default::default()).unwrap();
|
|
let image = photocraft_codecs::decode(&r.bytes).unwrap();
|
|
assert_eq!(image.data(), &[255, 128, 0, 255], "{depth:?}, transparent = {transparent}");
|
|
let table = photocraft_codecs::png_palette(&r.bytes).unwrap().unwrap();
|
|
assert_eq!(table.colors, palette);
|
|
assert_eq!(table.transparent, Some(transparent));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_breaks_opaque_color_ties_in_palette_order() {
|
|
// Both opaque entries are equally far from magenta; the transparent entry is an exact
|
|
// match. Filtering transparency must still choose the earlier original opaque entry.
|
|
for opaque in [[[255, 0, 0], [0, 0, 255]], [[0, 0, 255], [255, 0, 0]]] {
|
|
let mut d = Document::with_background("m", Size::new(1, 1), ColorMode::Rgb, SampleType::U8, Color::rgba(1.0, 0.0, 1.0, 1.0));
|
|
d.mode = ColorMode::Indexed;
|
|
d.color_table = Some(ColorTable { colors: vec![[255, 0, 255], opaque[0], opaque[1]], transparent: Some(0) });
|
|
let r = photocraft_io::export(&d, "out.png", &Default::default()).unwrap();
|
|
let image = photocraft_codecs::decode(&r.bytes).unwrap();
|
|
assert_eq!(image.data(), &[opaque[0][0], opaque[0][1], opaque[0][2], 255]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_keeps_the_alpha_threshold() {
|
|
for depth in [SampleType::U8, SampleType::U16, SampleType::F32] {
|
|
let mut d = Document::with_background("m", Size::new(3, 1), ColorMode::Rgb, depth, Color::rgba(1.0, 0.0, 0.0, 1.0));
|
|
d.mode = ColorMode::Indexed;
|
|
d.color_table = Some(ColorTable { colors: vec![[255, 0, 0], [255, 0, 0]], transparent: Some(0) });
|
|
let surface = d.layers[0].surface_mut().unwrap();
|
|
surface.write_pixel(0, 0, &[1.0, 0.0, 0.0, 0.49]);
|
|
surface.write_pixel(1, 0, &[1.0, 0.0, 0.0, 0.5]);
|
|
let r = photocraft_io::export(&d, "out.png", &Default::default()).unwrap();
|
|
assert_eq!(photocraft_codecs::decode(&r.bytes).unwrap().data(), &[255, 0, 0, 0, 255, 0, 0, 255, 255, 0, 0, 255], "{depth:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_keeps_the_last_palette_index() {
|
|
let mut palette = vec![[0, 0, 255]; 256];
|
|
palette[0] = [255, 0, 0];
|
|
palette[255] = [255, 0, 0];
|
|
for transparent in [0, 255] {
|
|
let png = photocraft_codecs::encode_png_indexed(2, 1, &[0, 255], &palette, Some(transparent), None).unwrap();
|
|
let d = photocraft_io::import("in.png", &png).unwrap().document;
|
|
let r = photocraft_io::export(&d, "out.png", &Default::default()).unwrap();
|
|
assert_eq!(photocraft_codecs::png_palette(&r.bytes).unwrap(), photocraft_codecs::png_palette(&png).unwrap());
|
|
assert_eq!(photocraft_codecs::decode(&r.bytes).unwrap().data(), photocraft_codecs::decode(&png).unwrap().data());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_keeps_a_single_transparent_entry() {
|
|
let png = photocraft_codecs::encode_png_indexed(2, 1, &[0, 0], &[[255, 0, 0]], Some(0), None).unwrap();
|
|
let d = photocraft_io::import("in.png", &png).unwrap().document;
|
|
let r = photocraft_io::export(&d, "out.png", &Default::default()).unwrap();
|
|
assert_eq!(photocraft_codecs::png_palette(&r.bytes).unwrap(), photocraft_codecs::png_palette(&png).unwrap());
|
|
assert_eq!(photocraft_codecs::decode(&r.bytes).unwrap().data(), photocraft_codecs::decode(&png).unwrap().data());
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_rejects_opaque_pixels_without_an_opaque_entry() {
|
|
let mut d = doc(ColorMode::Indexed);
|
|
d.color_table = Some(ColorTable { colors: vec![[255, 0, 0]], transparent: Some(0) });
|
|
let err = photocraft_io::export(&d, "out.png", &Default::default()).unwrap_err();
|
|
assert!(matches!(err, photocraft_io::IoError::Unsupported(_)), "{err}");
|
|
assert!(err.to_string().contains("no opaque entry"), "{err}");
|
|
}
|
|
|
|
#[test]
|
|
fn duotone_exports_the_inks_as_rgb() {
|
|
// Mid gray background.
|
|
let mut d = Document::with_background("m", Size::new(4, 2), ColorMode::Grayscale, SampleType::U8, Color::rgba(0.5, 0.5, 0.5, 1.0));
|
|
d.mode = ColorMode::Duotone;
|
|
d.duotone = Some(Duotone { inks: vec![DuotoneInk::new("Black", [0.0; 3]), DuotoneInk::new("Orange", [1.0, 0.5, 0.0])], psd_raw: None });
|
|
let r = photocraft_io::export(&d, "x.png", &Default::default()).unwrap();
|
|
assert!(r.warnings.iter().any(|w| w.contains("Duotone")), "{:?}", r.warnings);
|
|
let back = photocraft_io::import("x.png", &r.bytes).unwrap().document;
|
|
let px = back.layers[0].surface().unwrap().rgba(0, 0);
|
|
assert!(px[0] > px[2] + 0.05, "warm: {px:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn indexed_png_keeps_the_document_resolution() {
|
|
// #3047: the palette PNG lost its pHYs chunk and reopened at 72 ppi.
|
|
let mut d = doc(ColorMode::Indexed);
|
|
d.resolution_dpi = 300.0;
|
|
d.color_table = Some(ColorTable { colors: vec![[0, 0, 0], [255, 0, 0]], transparent: None });
|
|
let r = photocraft_io::export(&d, "x.png", &Default::default()).unwrap();
|
|
assert_eq!(r.bytes[25], 3, "still a palette PNG");
|
|
let back = photocraft_io::import("x.png", &r.bytes).unwrap().document;
|
|
assert!((back.resolution_dpi - 300.0).abs() < 0.01, "{}", back.resolution_dpi);
|
|
}
|