use crate::objects::tree::{CmykChannel, ObjectTree, PageObject, PdfColor};
#[derive(Clone, Debug, PartialEq)]
pub enum InkSelector {
Separation(String),
CmykChannel(CmykChannel),
}
pub fn filter_by_ink<'a>(tree: &'a ObjectTree, selector: &InkSelector) -> Vec<&'a PageObject> {
tree.objects
.iter()
.filter(|obj| {
color_matches(obj.fill_color.as_ref(), selector)
|| color_matches(obj.stroke_color.as_ref(), selector)
})
.collect()
}
fn color_matches(color: Option<&PdfColor>, selector: &InkSelector) -> bool {
let Some(color) = color else { return false };
match selector {
InkSelector::Separation(target) => {
matches!(color, PdfColor::Separation { name, .. } if name == target)
}
InkSelector::CmykChannel(ch) => channel_nonzero(color, ch),
}
}
fn channel_nonzero(color: &PdfColor, channel: &CmykChannel) -> bool {
match color {
PdfColor::DeviceCmyk(c, m, y, k) => {
let v = match channel {
CmykChannel::Cyan => *c,
CmykChannel::Magenta => *m,
CmykChannel::Yellow => *y,
CmykChannel::Black => *k,
};
v > 0.0
}
PdfColor::Separation { tint, .. } => *tint > 0.0,
PdfColor::DeviceGray(_) | PdfColor::DeviceRgb(..) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{Matrix, Rect};
use crate::objects::tree::{
CmykChannel, ObjectKind, ObjectTree, OverprintState, PageObject, PdfColor,
};
fn cmyk_fill_object(c: f64, m: f64, y: f64, k: f64) -> PageObject {
PageObject {
bbox: Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 },
ctm: Matrix::identity(),
kind: ObjectKind::Fill,
fill_color: Some(PdfColor::DeviceCmyk(c, m, y, k)),
stroke_color: None,
stroke_width: 0.0,
overprint: OverprintState::default(),
subpaths: vec![],
}
}
fn spot_fill_object(name: &str, tint: f64) -> PageObject {
PageObject {
bbox: Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 },
ctm: Matrix::identity(),
kind: ObjectKind::Fill,
fill_color: Some(PdfColor::Separation { name: name.to_string(), tint }),
stroke_color: None,
stroke_width: 0.0,
overprint: OverprintState::default(),
subpaths: vec![],
}
}
fn image_object() -> PageObject {
PageObject {
bbox: Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 },
ctm: Matrix::identity(),
kind: ObjectKind::Image,
fill_color: None,
stroke_color: None,
stroke_width: 0.0,
overprint: OverprintState::default(),
subpaths: vec![],
}
}
#[test]
fn filter_by_separation_name_matches_exact() {
let tree = ObjectTree {
objects: vec![
spot_fill_object("PANTONE 485 C", 1.0),
spot_fill_object("PANTONE Cool Gray 9 C", 1.0),
],
};
let hits = filter_by_ink(&tree, &InkSelector::Separation("PANTONE 485 C".to_string()));
assert_eq!(hits.len(), 1);
assert!(matches!(
&hits[0].fill_color,
Some(PdfColor::Separation { name, .. }) if name == "PANTONE 485 C"
));
}
#[test]
fn filter_by_separation_zero_tint_still_matches() {
let tree = ObjectTree {
objects: vec![spot_fill_object("Die Cut", 0.0)],
};
let hits = filter_by_ink(&tree, &InkSelector::Separation("Die Cut".to_string()));
assert_eq!(hits.len(), 1);
}
#[test]
fn filter_by_cmyk_channel_cyan() {
let tree = ObjectTree {
objects: vec![
cmyk_fill_object(1.0, 0.0, 0.0, 0.0), cmyk_fill_object(0.0, 1.0, 0.0, 0.0), ],
};
let hits = filter_by_ink(&tree, &InkSelector::CmykChannel(CmykChannel::Cyan));
assert_eq!(hits.len(), 1);
assert!(matches!(
hits[0].fill_color,
Some(PdfColor::DeviceCmyk(c, ..)) if c > 0.0
));
}
#[test]
fn filter_by_cmyk_channel_excludes_rgb() {
let tree = ObjectTree {
objects: vec![PageObject {
bbox: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
ctm: Matrix::identity(),
kind: ObjectKind::Fill,
fill_color: Some(PdfColor::DeviceRgb(1.0, 0.0, 0.0)),
stroke_color: None,
stroke_width: 0.0,
overprint: OverprintState::default(),
subpaths: vec![],
}],
};
let hits = filter_by_ink(&tree, &InkSelector::CmykChannel(CmykChannel::Cyan));
assert!(
hits.is_empty(),
"RGB objects must not match CMYK channel selectors"
);
}
#[test]
fn filter_excludes_images_with_no_color() {
let tree = ObjectTree {
objects: vec![image_object()],
};
let hits = filter_by_ink(&tree, &InkSelector::Separation("Any".to_string()));
assert!(hits.is_empty());
}
#[test]
fn filter_checks_stroke_color_too() {
let obj = PageObject {
bbox: Rect {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
},
ctm: Matrix::identity(),
kind: ObjectKind::Stroke,
fill_color: None,
stroke_color: Some(PdfColor::Separation {
name: "Die Cut".to_string(),
tint: 1.0,
}),
stroke_width: 1.0,
overprint: OverprintState::default(),
subpaths: vec![],
};
let tree = ObjectTree { objects: vec![obj] };
let hits = filter_by_ink(&tree, &InkSelector::Separation("Die Cut".to_string()));
assert_eq!(hits.len(), 1);
}
#[test]
fn filter_paint_order_preserved() {
let tree = ObjectTree {
objects: vec![
cmyk_fill_object(1.0, 0.0, 0.0, 0.0),
image_object(),
cmyk_fill_object(0.5, 0.0, 0.0, 0.0),
],
};
let hits = filter_by_ink(&tree, &InkSelector::CmykChannel(CmykChannel::Cyan));
assert_eq!(hits.len(), 2);
assert!(
matches!(hits[0].fill_color, Some(PdfColor::DeviceCmyk(c, ..)) if (c - 1.0).abs() < 1e-10)
);
assert!(
matches!(hits[1].fill_color, Some(PdfColor::DeviceCmyk(c, ..)) if (c - 0.5).abs() < 1e-10)
);
}
}