vectorcraft/crates/pdf/src/tests_blend.rs

100 lines
3.8 KiB
Rust

//! Blend modes and transparency groups in PDF export.
use vectorcraft_color::{BlendMode, Color, Paint};
use vectorcraft_doc::{Appearance, Document, Node, NodeId};
use vectorcraft_geom::{Rect, shapes};
use crate::*;
fn rect(r: Rect, rgb: (f32, f32, f32)) -> Node {
Node::path(NodeId(0), shapes::rectangle(r), Appearance::basic(Paint::solid(Color::rgb(rgb.0, rgb.1, rgb.2)), Paint::None, 0.0))
}
fn add(d: &mut Document, mut n: Node) {
n.id = d.alloc_id();
let l = d.layers[0].id;
d.insert(Some(l), usize::MAX, n).unwrap();
}
/// The file's text with spaces removed (content streams uncompressed).
fn text(d: &Document) -> String {
let bytes = export(d, &PdfOptions::uncompressed()).unwrap();
String::from_utf8_lossy(&bytes).replace(' ', "")
}
#[test]
fn every_blend_mode_is_written_under_its_pdf_name_and_read_back() {
for mode in BlendMode::ALL.into_iter().filter(|m| *m != BlendMode::Normal) {
let mut d = Document::new(100.0, 100.0);
add(&mut d, rect(Rect::new(0.0, 0.0, 100.0, 100.0), (0.2, 0.5, 0.9)));
let mut top = rect(Rect::new(20.0, 20.0, 80.0, 80.0), (0.6, 0.25, 0.5));
top.blend = mode;
add(&mut d, top);
let name = mode.label().replace(' ', "");
assert!(text(&d).contains(&format!("/BM/{name}")), "{mode:?}");
let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap();
let mut found = BlendMode::Normal;
back.walk(|n| {
if n.blend != BlendMode::Normal {
found = n.blend;
}
});
assert_eq!(found, mode);
}
}
/// The backdrop and a 50% group (isolated or not) holding a Multiply square.
fn half_group(isolate: bool) -> Document {
let mut d = Document::new(100.0, 100.0);
add(&mut d, rect(Rect::new(0.0, 0.0, 100.0, 100.0), (0.8, 0.6, 0.2)));
let mut m = rect(Rect::new(20.0, 20.0, 80.0, 80.0), (0.5, 0.5, 1.0));
(m.id, m.blend) = (d.alloc_id(), BlendMode::Multiply);
let mut g = Node::group(NodeId(0), vec![std::sync::Arc::new(m)]);
(g.opacity, g.isolate) = (0.5, isolate);
add(&mut d, g);
d
}
#[test]
fn a_group_is_written_non_isolated_unless_it_isolates_blending() {
let open = text(&half_group(false));
// A transparency group without /I, its 50% carried by a constant alpha soft mask.
assert!(!open.contains("/Itrue"), "{open}");
assert!(open.contains("/SMask") && open.contains("/Alpha") && open.contains("/BM/Multiply"), "{open}");
let isolated = text(&half_group(true));
assert!(isolated.contains("/Itrue") && isolated.contains("/ca0.5"), "{isolated}");
assert!(!isolated.contains("/Alpha"), "{isolated}");
// Either way the child's blend mode reads back.
for d in [half_group(false), half_group(true)] {
let back = import(&export(&d, &PdfOptions::default()).unwrap()).unwrap();
let mut bm = BlendMode::Normal;
back.walk(|n| {
if n.blend != BlendMode::Normal {
bm = n.blend;
}
});
assert_eq!(bm, BlendMode::Multiply);
}
}
#[test]
fn groups_without_blending_inside_stay_isolated_groups() {
let mut d = half_group(false);
let g = d.layers[0].children().unwrap()[1].id;
let m = d.node(g).unwrap().children().unwrap()[0].id;
d.node_mut(m).unwrap().blend = BlendMode::Normal;
let t = text(&d);
assert!(t.contains("/Itrue") && t.contains("/ca0.5") && !t.contains("/SMask"), "{t}");
}
#[test]
fn a_knockout_group_with_blending_elements_is_non_isolated_unless_isolated() {
for isolate in [false, true] {
let mut d = half_group(isolate);
let g = d.layers[0].children().unwrap()[1].id;
let n = d.node_mut(g).unwrap();
(n.opacity, n.knockout) = (1.0, vectorcraft_doc::Knockout::On);
assert_eq!(text(&d).contains("/Itrue"), isolate, "isolate {isolate}");
}
}