use std::sync::Arc;
use tiny_skia::{FillRule, Mask, Pixmap, Transform};
use crate::content::graphics_state::Matrix;
use crate::error::Result;
use super::backend::PaintBackend;
use super::ink::{InkAction, InkRouter};
use super::intent::{PaintKind, PaintSide};
use super::resolved::{InkName, ResolvedPaintCmd};
pub(crate) struct SeparationSurface<'a> {
pub(crate) pixmaps: &'a mut [Pixmap],
pub(crate) inks: &'a [InkName],
pub(crate) base_transform: Transform,
}
pub(crate) struct SeparationBackend {
router: InkRouter,
}
impl SeparationBackend {
pub(crate) const fn new() -> Self {
Self {
router: InkRouter::new(),
}
}
}
impl PaintBackend for SeparationBackend {
type Surface<'s>
= SeparationSurface<'s>
where
Self: 's;
fn paint(&mut self, cmd: &ResolvedPaintCmd, surface: Self::Surface<'_>) -> Result<()> {
let shared_clip: Option<&Mask> = match &cmd.clip {
super::resolved::ClipPlan::None => None,
super::resolved::ClipPlan::Mask(arc) => Some(arc.as_ref()),
};
let device_has_spot_plate = match &cmd.overprint.spot_source {
Some(spot) => surface.inks.iter().any(|i| i == &spot.ink),
None => false,
};
let fallback_plan;
let effective_plan: &super::resolved::OverprintPlan =
if cmd.overprint.spot_source.is_some() && !device_has_spot_plate {
let alt = cmd.overprint.alt_cmyk_fallback.unwrap_or([0.0; 4]);
let mut v = smallvec::SmallVec::new();
for (j, name) in ["Cyan", "Magenta", "Yellow", "Black"].iter().enumerate() {
v.push(super::resolved::ParticipatingChannel {
ink: InkName::new(*name),
value: alt[j],
});
}
fallback_plan = super::resolved::OverprintPlan {
enabled: cmd.overprint.enabled,
mode: cmd.overprint.mode,
participating: v,
selector: cmd.overprint.selector,
all_tint: cmd.overprint.all_tint,
spot_source: None,
alt_cmyk_fallback: None,
};
&fallback_plan
} else {
&cmd.overprint
};
for (plate_idx, ink) in surface.inks.iter().enumerate() {
let gs = crate::content::graphics_state::GraphicsState::new();
let action = self.router.route(&gs, ink, &cmd.color, effective_plan);
let tint = match action {
InkAction::Skip => continue,
InkAction::Paint(t) => t,
};
let pixmap = &mut surface.pixmaps[plate_idx];
paint_one_plate(pixmap, cmd, surface.base_transform, tint, shared_clip);
}
Ok(())
}
}
fn paint_one_plate(
pixmap: &mut Pixmap,
cmd: &ResolvedPaintCmd,
base_transform: Transform,
tint: f32,
clip: Option<&Mask>,
) {
let transform = combine_transforms(base_transform, &cmd.ctm);
match cmd.kind {
PaintKind::Path { path, fill_rule } => match cmd.side {
PaintSide::Fill => fill_plate(pixmap, path, transform, tint, fill_rule, clip),
PaintSide::Stroke => {
let stroke = tiny_skia::Stroke::default();
stroke_plate(pixmap, path, transform, &stroke, tint, clip);
},
},
PaintKind::ColorOnly => {},
PaintKind::Glyph { .. } | PaintKind::Image { .. } | PaintKind::Shading { .. } => {},
}
}
fn fill_plate(
pixmap: &mut Pixmap,
path: &tiny_skia::Path,
transform: Transform,
tint: f32,
fill_rule: FillRule,
clip: Option<&Mask>,
) {
let gray = (tint.clamp(0.0, 1.0) * 255.0).round() as u8;
let color = tiny_skia::Color::from_rgba8(gray, gray, gray, 255);
let mut paint = tiny_skia::Paint::default();
paint.set_color(color);
paint.anti_alias = true;
paint.blend_mode = tiny_skia::BlendMode::SourceOver;
pixmap.fill_path(path, &paint, fill_rule, transform, clip);
}
fn stroke_plate(
pixmap: &mut Pixmap,
path: &tiny_skia::Path,
transform: Transform,
stroke: &tiny_skia::Stroke,
tint: f32,
clip: Option<&Mask>,
) {
let gray = (tint.clamp(0.0, 1.0) * 255.0).round() as u8;
let color = tiny_skia::Color::from_rgba8(gray, gray, gray, 255);
let mut paint = tiny_skia::Paint::default();
paint.set_color(color);
paint.anti_alias = true;
pixmap.stroke_path(path, &paint, stroke, transform, clip);
}
fn combine_transforms(base: Transform, ctm: &Matrix) -> Transform {
base.pre_concat(Transform::from_row(ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f))
}
const _: Option<Arc<Mask>> = None;
#[cfg(test)]
mod tests {
use super::*;
use smallvec::{smallvec, SmallVec};
use super::super::intent::{PaintKind, PaintSide};
use super::super::resolved::{
BlendPlan, ClipPlan, InkSelector, OverprintPlan, ParticipatingChannel, ResolvedColor,
ResolvedPaintCmd,
};
fn rect_path() -> tiny_skia::Path {
let mut pb = tiny_skia::PathBuilder::new();
pb.move_to(0.0, 0.0);
pb.line_to(10.0, 0.0);
pb.line_to(10.0, 10.0);
pb.line_to(0.0, 10.0);
pb.close();
pb.finish().expect("non-empty path")
}
fn fresh_pixmap() -> Pixmap {
Pixmap::new(16, 16).expect("16x16 pixmap allocates")
}
fn cmyk_cmd<'a>(
path: &'a tiny_skia::Path,
c: f32,
m: f32,
y: f32,
k: f32,
) -> ResolvedPaintCmd<'a> {
ResolvedPaintCmd {
kind: PaintKind::Path {
path,
fill_rule: FillRule::Winding,
},
side: PaintSide::Fill,
color: ResolvedColor::Cmyk { c, m, y, k, a: 1.0 },
overprint: OverprintPlan {
enabled: false,
mode: 0,
participating: smallvec![
ParticipatingChannel {
ink: InkName::new("Cyan"),
value: c,
},
ParticipatingChannel {
ink: InkName::new("Magenta"),
value: m,
},
ParticipatingChannel {
ink: InkName::new("Yellow"),
value: y,
},
ParticipatingChannel {
ink: InkName::new("Black"),
value: k,
},
],
selector: InkSelector::Listed,
all_tint: 0.0,
spot_source: None,
alt_cmyk_fallback: None,
},
blend: BlendPlan::Native(tiny_skia::BlendMode::SourceOver),
clip: ClipPlan::None,
ctm: Matrix::identity(),
}
}
#[test]
fn fill_routes_cmyk_to_matching_plates() {
let path = rect_path();
let cmd = cmyk_cmd(&path, 0.5, 0.25, 0.0, 1.0);
let mut plates = vec![
fresh_pixmap(),
fresh_pixmap(),
fresh_pixmap(),
fresh_pixmap(),
];
let inks = [
InkName::new("Cyan"),
InkName::new("Magenta"),
InkName::new("Yellow"),
InkName::new("Black"),
];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
let sample = |p: &Pixmap| p.data()[(5 * 16 + 5) * 4];
assert_eq!(sample(&plates[0]), 128, "Cyan tint ≈ 0.5");
assert_eq!(sample(&plates[1]), 64, "Magenta tint ≈ 0.25");
assert_eq!(sample(&plates[2]), 0, "Yellow tint = 0.0 knock-out");
assert_eq!(sample(&plates[3]), 255, "Black tint = 1.0 full ink");
}
#[test]
fn fill_skips_spot_plates_when_overprint_enabled() {
let path = rect_path();
let mut cmd = cmyk_cmd(&path, 0.5, 0.0, 0.0, 0.0);
cmd.overprint.enabled = true;
let mut plates = vec![fresh_pixmap(), fresh_pixmap()];
let sentinel = tiny_skia::Color::from_rgba8(200, 0, 0, 255);
let mut spot_paint = tiny_skia::Paint::default();
spot_paint.set_color(sentinel);
let full_rect = tiny_skia::Rect::from_xywh(0.0, 0.0, 16.0, 16.0).unwrap();
plates[1].fill_path(
&tiny_skia::PathBuilder::from_rect(full_rect),
&spot_paint,
FillRule::Winding,
Transform::identity(),
None,
);
let inks = [InkName::new("Cyan"), InkName::new("PANTONE 185 C")];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
assert_eq!(plates[0].data()[(5 * 16 + 5) * 4], 128);
assert_eq!(plates[1].data()[(5 * 16 + 5) * 4], 200);
}
#[test]
fn per_channel_devicen_routes_named_plates() {
let path = rect_path();
let cmd = ResolvedPaintCmd {
kind: PaintKind::Path {
path: &path,
fill_rule: FillRule::Winding,
},
side: PaintSide::Fill,
color: ResolvedColor::PerChannel {
channels: Box::new(smallvec![
(InkName::new("PANTONE 185 C"), 0.75),
(InkName::new("Dieline"), 0.1),
]),
a: 1.0,
},
overprint: OverprintPlan {
enabled: false,
mode: 0,
participating: smallvec![
ParticipatingChannel {
ink: InkName::new("PANTONE 185 C"),
value: 0.75,
},
ParticipatingChannel {
ink: InkName::new("Dieline"),
value: 0.1,
},
],
selector: InkSelector::Listed,
all_tint: 0.0,
spot_source: None,
alt_cmyk_fallback: None,
},
blend: BlendPlan::Native(tiny_skia::BlendMode::SourceOver),
clip: ClipPlan::None,
ctm: Matrix::identity(),
};
let mut plates = vec![fresh_pixmap(), fresh_pixmap()];
let inks = [InkName::new("PANTONE 185 C"), InkName::new("Dieline")];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
assert_eq!(plates[0].data()[(5 * 16 + 5) * 4], 191);
assert_eq!(plates[1].data()[(5 * 16 + 5) * 4], 26);
}
#[test]
fn rgb_color_routes_to_no_plates() {
let path = rect_path();
let cmd = ResolvedPaintCmd {
kind: PaintKind::Path {
path: &path,
fill_rule: FillRule::Winding,
},
side: PaintSide::Fill,
color: ResolvedColor::Rgba {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
overprint: OverprintPlan {
enabled: false,
mode: 0,
participating: SmallVec::new(),
selector: InkSelector::Listed,
all_tint: 0.0,
spot_source: None,
alt_cmyk_fallback: None,
},
blend: BlendPlan::Native(tiny_skia::BlendMode::SourceOver),
clip: ClipPlan::None,
ctm: Matrix::identity(),
};
let mut plates = vec![fresh_pixmap()];
let inks = [InkName::new("Cyan")];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
assert_eq!(plates[0].data()[(5 * 16 + 5) * 4], 0);
}
#[test]
fn opm1_zero_component_on_cmyk_skips_matching_plate() {
let path = rect_path();
let mut cmd = cmyk_cmd(&path, 0.5, 0.0, 0.0, 0.0);
cmd.overprint.enabled = true;
cmd.overprint.mode = 1;
let mut plates = vec![fresh_pixmap(), fresh_pixmap()];
let sentinel = tiny_skia::Color::from_rgba8(99, 0, 0, 255);
let mut spot_paint = tiny_skia::Paint::default();
spot_paint.set_color(sentinel);
let full_rect = tiny_skia::Rect::from_xywh(0.0, 0.0, 16.0, 16.0).unwrap();
plates[1].fill_path(
&tiny_skia::PathBuilder::from_rect(full_rect),
&spot_paint,
FillRule::Winding,
Transform::identity(),
None,
);
let inks = [InkName::new("Cyan"), InkName::new("Magenta")];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
assert_eq!(plates[0].data()[(5 * 16 + 5) * 4], 128);
assert_eq!(plates[1].data()[(5 * 16 + 5) * 4], 99);
}
#[test]
fn color_only_intent_paints_nothing() {
let cmd = ResolvedPaintCmd {
kind: PaintKind::ColorOnly,
side: PaintSide::Fill,
color: ResolvedColor::Cmyk {
c: 1.0,
m: 0.0,
y: 0.0,
k: 0.0,
a: 1.0,
},
overprint: OverprintPlan {
enabled: false,
mode: 0,
participating: smallvec![ParticipatingChannel {
ink: InkName::new("Cyan"),
value: 1.0,
}],
selector: InkSelector::Listed,
all_tint: 0.0,
spot_source: None,
alt_cmyk_fallback: None,
},
blend: BlendPlan::Native(tiny_skia::BlendMode::SourceOver),
clip: ClipPlan::None,
ctm: Matrix::identity(),
};
let mut plates = vec![fresh_pixmap()];
let inks = [InkName::new("Cyan")];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
assert_eq!(plates[0].data()[(5 * 16 + 5) * 4], 0);
}
fn assert_backend_matches_inline(
path: &tiny_skia::Path,
ctm: Matrix,
cmd: ResolvedPaintCmd<'_>,
inks: &[InkName],
tints: &[f32],
fill_rule: FillRule,
) {
assert_eq!(inks.len(), tints.len());
let mut backend_plates: Vec<Pixmap> = (0..inks.len()).map(|_| fresh_pixmap()).collect();
let surface = SeparationSurface {
pixmaps: &mut backend_plates,
inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
let transform = combine_transforms(Transform::identity(), &ctm);
let mut inline_plates: Vec<Pixmap> = (0..inks.len()).map(|_| fresh_pixmap()).collect();
for (i, &tint) in tints.iter().enumerate() {
crate::rendering::separation_renderer::fill_separation(
&mut inline_plates[i],
path,
transform,
tint,
fill_rule,
None,
);
}
for (i, ink) in inks.iter().enumerate() {
assert_eq!(
backend_plates[i].data(),
inline_plates[i].data(),
"plate {:?} (index {i}) must match separation_renderer::fill_separation byte-for-byte",
ink.as_str(),
);
}
}
#[test]
fn all_inks_paints_every_plate_at_same_tint() {
let path = rect_path();
let cmd = ResolvedPaintCmd {
kind: PaintKind::Path {
path: &path,
fill_rule: FillRule::Winding,
},
side: PaintSide::Fill,
color: ResolvedColor::Rgba {
r: 0.6,
g: 0.6,
b: 0.6,
a: 1.0,
},
overprint: OverprintPlan {
enabled: false,
mode: 0,
participating: SmallVec::new(),
selector: InkSelector::All,
all_tint: 0.6,
spot_source: None,
alt_cmyk_fallback: None,
},
blend: BlendPlan::Native(tiny_skia::BlendMode::SourceOver),
clip: ClipPlan::None,
ctm: Matrix::identity(),
};
let mut plates = vec![
fresh_pixmap(),
fresh_pixmap(),
fresh_pixmap(),
fresh_pixmap(),
];
let inks = [
InkName::new("Cyan"),
InkName::new("Magenta"),
InkName::new("PANTONE 185 C"),
InkName::new("Dieline"),
];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
for (i, ink) in inks.iter().enumerate() {
assert_eq!(
plates[i].data()[(5 * 16 + 5) * 4],
153,
"/All must paint plate {:?} at the single tint",
ink.as_str(),
);
}
}
#[test]
fn none_inks_paints_no_plates() {
let path = rect_path();
let cmd = ResolvedPaintCmd {
kind: PaintKind::Path {
path: &path,
fill_rule: FillRule::Winding,
},
side: PaintSide::Fill,
color: ResolvedColor::Rgba {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
},
overprint: OverprintPlan {
enabled: false,
mode: 0,
participating: SmallVec::new(),
selector: InkSelector::None,
all_tint: 0.0,
spot_source: None,
alt_cmyk_fallback: None,
},
blend: BlendPlan::Native(tiny_skia::BlendMode::SourceOver),
clip: ClipPlan::None,
ctm: Matrix::identity(),
};
let mut plates = vec![fresh_pixmap(), fresh_pixmap()];
let inks = [InkName::new("Cyan"), InkName::new("PANTONE 185 C")];
let surface = SeparationSurface {
pixmaps: &mut plates,
inks: &inks,
base_transform: Transform::identity(),
};
let mut backend = SeparationBackend::new();
backend.paint(&cmd, surface).unwrap();
assert_eq!(plates[0].data()[(5 * 16 + 5) * 4], 0);
assert_eq!(plates[1].data()[(5 * 16 + 5) * 4], 0);
}
#[test]
fn cmyk_cyan_only_matches_fill_separation_byte_for_byte() {
let path = rect_path();
let cmd = cmyk_cmd(&path, 0.5, 0.0, 0.0, 0.0);
let inks = [
InkName::new("Cyan"),
InkName::new("Magenta"),
InkName::new("Yellow"),
InkName::new("Black"),
];
let tints = [0.5, 0.0, 0.0, 0.0];
assert_backend_matches_inline(
&path,
Matrix::identity(),
cmd,
&inks,
&tints,
FillRule::Winding,
);
}
#[test]
fn cmyk_mixed_fill_matches_fill_separation_byte_for_byte() {
let path = rect_path();
let cmd = cmyk_cmd(&path, 0.5, 0.25, 0.0, 0.7);
let inks = [
InkName::new("Cyan"),
InkName::new("Magenta"),
InkName::new("Yellow"),
InkName::new("Black"),
];
let tints = [0.5, 0.25, 0.0, 0.7];
assert_backend_matches_inline(
&path,
Matrix::identity(),
cmd,
&inks,
&tints,
FillRule::Winding,
);
}
#[test]
fn cmyk_rotated_ctm_matches_fill_separation_byte_for_byte() {
let path = rect_path();
let theta = 30.0_f32.to_radians();
let (s, c) = theta.sin_cos();
let rotation = Matrix {
a: c,
b: s,
c: -s,
d: c,
e: 0.0,
f: 0.0,
};
let mut cmd = cmyk_cmd(&path, 0.5, 0.25, 0.0, 0.7);
cmd.ctm = rotation;
let inks = [
InkName::new("Cyan"),
InkName::new("Magenta"),
InkName::new("Yellow"),
InkName::new("Black"),
];
let tints = [0.5, 0.25, 0.0, 0.7];
assert_backend_matches_inline(&path, rotation, cmd, &inks, &tints, FillRule::Winding);
}
}