100 lines
3.8 KiB
Rust
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}");
|
|
}
|
|
}
|