mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-13 22:12:12 +00:00
* prepress: printer's marks Marks::new lays out crop marks, bleed marks, registration targets and process color bars around a trim box, with the sheet that holds them and room for a page-information line. Marks::draw paints them onto a krilla surface: lines in the registration color (Separation /All), bars in DeviceCMYK. ISO 32000-2 14.11.3, 14.11.2, 8.6.6.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * prepress: keep the info line clear of the bottom target Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * prepress: clear targets only when drawn; cap line width at a third of the mark Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: KenM76 <KenM76> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
8.5 KiB
Rust
173 lines
8.5 KiB
Rust
//! Printer's marks: layout from `Marks::new`, and `Marks::draw` rendered back through `plates`.
|
||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||
|
||
use craft_pdf::write::backend::SerializeSettings;
|
||
use craft_pdf::write::backend::geom::Size;
|
||
use craft_pdf::write::backend::page::PageSettings;
|
||
use craft_pdf::write::{Metadata, Writer};
|
||
use craft_prepress::marks::{BAR_PATCHES, Rect};
|
||
use craft_prepress::{InkId, MarkOptions, Marks, MarksError, Options, Process, plates};
|
||
|
||
/// A 200 × 100 pt trim at (100, 100).
|
||
const TRIM: Rect = Rect { x0: 100.0, y0: 100.0, x1: 300.0, y1: 200.0 };
|
||
|
||
fn only(f: impl FnOnce(&mut MarkOptions)) -> MarkOptions {
|
||
let mut o = MarkOptions { crop: false, bleed: false, registration: false, color_bars: false, ..MarkOptions::default() };
|
||
f(&mut o);
|
||
o
|
||
}
|
||
|
||
fn inside(r: Rect, (x, y): (f64, f64)) -> bool {
|
||
(r.x0..=r.x1).contains(&x) && (r.y0..=r.y1).contains(&y)
|
||
}
|
||
|
||
#[test]
|
||
fn the_sheet_holds_the_marks_and_clears_the_bleed() {
|
||
let m = Marks::new(TRIM, TRIM, &MarkOptions::default()).unwrap();
|
||
assert_eq!((m.gap, m.sheet), (6.0, Rect::new(76.0, 76.0, 324.0, 224.0)));
|
||
|
||
let bleed = Rect::new(91.0, 100.0, 300.0, 200.0);
|
||
let m = Marks::new(TRIM, bleed, &MarkOptions::default()).unwrap();
|
||
assert_eq!((m.gap, m.sheet), (9.0, Rect::new(73.0, 73.0, 327.0, 227.0)), "the gap widens to clear a 9 pt bleed");
|
||
for p in m.lines.iter().flatten() {
|
||
assert!((inside(m.sheet, *p) && !inside(m.trim, *p)) || on_cross(&m, *p), "{p:?} sits in the slug");
|
||
}
|
||
for p in &m.patches {
|
||
assert!(inside(m.sheet, (p.rect.x0, p.rect.y0)) && inside(m.sheet, (p.rect.x1, p.rect.y1)));
|
||
assert!(p.rect.y1 <= m.bleed.y0 - m.gap + 1e-9, "patches stay clear of the bleed");
|
||
}
|
||
}
|
||
|
||
/// Registration crosses lie in the slug too; this only lets their arms touch the sheet edge.
|
||
fn on_cross(m: &Marks, p: (f64, f64)) -> bool {
|
||
m.circles.iter().any(|((x, y), r)| (p.0 - x).abs() <= r * 1.4 + 1e-9 && (p.1 - y).abs() <= r * 1.4 + 1e-9)
|
||
}
|
||
|
||
#[test]
|
||
fn crop_marks_run_outward_from_the_gap_on_the_trim_lines() {
|
||
let m = Marks::new(TRIM, TRIM, &only(|o| o.crop = true)).unwrap();
|
||
assert_eq!(m.lines.len(), 8);
|
||
assert!(m.lines.contains(&[(94.0, 100.0), (76.0, 100.0)]), "top left, horizontal");
|
||
assert!(m.lines.contains(&[(100.0, 94.0), (100.0, 76.0)]), "top left, vertical");
|
||
assert!(m.lines.contains(&[(306.0, 200.0), (324.0, 200.0)]), "bottom right, horizontal");
|
||
assert!(m.lines.contains(&[(300.0, 206.0), (300.0, 224.0)]), "bottom right, vertical");
|
||
assert!(m.circles.is_empty() && m.patches.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn bleed_marks_appear_only_when_the_bleed_passes_the_trim() {
|
||
let opts = only(|o| o.bleed = true);
|
||
assert!(Marks::new(TRIM, TRIM, &opts).unwrap().lines.is_empty());
|
||
let m = Marks::new(TRIM, grown(TRIM, 9.0), &opts).unwrap();
|
||
assert_eq!(m.lines.len(), 8);
|
||
assert!(m.lines.contains(&[(91.0, 91.0), (82.0, 91.0)]), "half-length, on the bleed line: {:?}", m.lines);
|
||
assert!(m.lines.contains(&[(309.0, 209.0), (309.0, 218.0)]));
|
||
}
|
||
|
||
fn grown(r: Rect, by: f64) -> Rect {
|
||
Rect::new(r.x0 - by, r.y0 - by, r.x1 + by, r.y1 + by)
|
||
}
|
||
|
||
#[test]
|
||
fn a_registration_target_sits_mid_band_on_each_side() {
|
||
let m = Marks::new(TRIM, TRIM, &only(|o| o.registration = true)).unwrap();
|
||
let centres: Vec<_> = m.circles.iter().map(|(c, r)| (*c, *r)).collect();
|
||
assert_eq!(centres, [((200.0, 85.0), 6.0), ((200.0, 215.0), 6.0), ((85.0, 150.0), 6.0), ((315.0, 150.0), 6.0)]);
|
||
assert_eq!(m.lines.len(), 8, "a cross through each");
|
||
}
|
||
|
||
#[test]
|
||
fn colour_bars_fit_between_the_crop_marks_and_the_middle() {
|
||
// Half the 200 pt width, less a mark length: 82 pt, so 7 patches of 10.8.
|
||
assert_eq!(Marks::new(TRIM, TRIM, &only(|o| o.color_bars = true)).unwrap().patches.len(), 7);
|
||
// Less the top target's arm too: 73.2 pt, so 6.
|
||
let m = Marks::new(TRIM, TRIM, &only(|o| (o.color_bars, o.registration) = (true, true))).unwrap();
|
||
assert_eq!(m.patches.len(), 6);
|
||
let first = m.patches.first().unwrap();
|
||
assert_eq!(first.cmyk, BAR_PATCHES[0]);
|
||
assert!((first.rect.x0 - 118.0).abs() < 1e-9 && (first.rect.width() - 10.8).abs() < 1e-9);
|
||
assert!(m.patches.iter().all(|p| p.rect.y0 >= m.sheet.y0 && p.rect.y1 <= TRIM.y0 - m.gap));
|
||
assert!(m.patches.last().unwrap().rect.x1 < 200.0 - 6.0 * 1.4, "short of the top target");
|
||
|
||
let narrow = Rect::new(0.0, 0.0, 40.0, 40.0);
|
||
assert!(Marks::new(narrow, narrow, &only(|o| o.color_bars = true)).unwrap().patches.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn the_info_line_sits_in_the_bottom_band() {
|
||
let m = Marks::new(TRIM, TRIM, &MarkOptions::default()).unwrap();
|
||
let (x, y) = m.info.origin;
|
||
assert_eq!((x, m.info.size), (118.0, 7.0));
|
||
assert!(y > TRIM.y1 + m.gap && y < m.sheet.y1);
|
||
let ((cx, _), r) = m.circles[1];
|
||
assert!(x + m.info.max_width <= cx - r * 1.4 + 1e-9, "the line stops short of the bottom target");
|
||
let crop_only = Marks::new(TRIM, TRIM, &only(|o| o.crop = true)).unwrap();
|
||
assert_eq!(crop_only.info.max_width, 82.0, "with no target, the line may reach the middle");
|
||
}
|
||
|
||
#[test]
|
||
fn bad_options_and_boxes_are_errors() {
|
||
let e = |o: MarkOptions| Marks::new(TRIM, TRIM, &o).unwrap_err();
|
||
assert_eq!(e(only(|_| {})), MarksError::NoMarks);
|
||
for (o, what) in [
|
||
(MarkOptions { offset: -1.0, ..MarkOptions::default() }, "offset"),
|
||
(MarkOptions { length: 0.0, ..MarkOptions::default() }, "length"),
|
||
(MarkOptions { length: f64::NAN, ..MarkOptions::default() }, "length"),
|
||
(MarkOptions { line_width: 145.0, ..MarkOptions::default() }, "line width"),
|
||
(MarkOptions { line_width: 6.5, ..MarkOptions::default() }, "line width"),
|
||
] {
|
||
assert!(matches!(e(o), MarksError::Size { what: w, .. } if w == what), "{what}");
|
||
}
|
||
let opts = MarkOptions::default();
|
||
assert_eq!(Marks::new(Rect::new(10.0, 0.0, 0.0, 10.0), TRIM, &opts).unwrap_err(), MarksError::Box("trim"));
|
||
assert_eq!(Marks::new(TRIM, Rect::new(0.0, f64::INFINITY, 1.0, 1.0), &opts).unwrap_err(), MarksError::Box("bleed"));
|
||
let huge = Rect::new(0.0, 0.0, 1e30, 10.0);
|
||
assert_eq!(Marks::new(huge, huge, &opts).unwrap_err(), MarksError::Box("sheet"));
|
||
}
|
||
|
||
/// `m` drawn on a sheet-sized page, rendered to plates at 72 dpi (one pixel per point).
|
||
fn render(m: &Marks) -> craft_prepress::Plates {
|
||
let mut writer = Writer::new(SerializeSettings::default(), Metadata::default());
|
||
let mut page = writer.document_mut().start_page_with(PageSettings::new(Size::from_wh(m.sheet.width() as f32, m.sheet.height() as f32).unwrap()));
|
||
let mut surface = page.surface();
|
||
m.draw(&mut surface);
|
||
surface.finish();
|
||
page.finish();
|
||
plates(&writer.finish().unwrap(), 0, 72.0, &Options::default()).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn drawn_marks_print_on_every_plate_and_patches_on_their_own() {
|
||
let m = Marks::new(TRIM, TRIM, &MarkOptions { line_width: 3.0, ..MarkOptions::default() }).unwrap();
|
||
let p = render(&m);
|
||
let px = |(x, y): (f64, f64)| ((x - m.sheet.x0) as u32, (y - m.sheet.y0) as u32);
|
||
let ink = |id: Process, at: (f64, f64)| {
|
||
let (x, y) = px(at);
|
||
p.tint(InkId::process(id), x, y).unwrap()
|
||
};
|
||
let all = [Process::Cyan, Process::Magenta, Process::Yellow, Process::Black];
|
||
|
||
let crop = (85.0, 100.0);
|
||
assert!(all.iter().all(|&c| ink(c, crop) > 200), "a crop mark inks every plate");
|
||
let target = (200.0, 85.0);
|
||
assert!(all.iter().all(|&c| ink(c, target) > 200), "the registration cross inks every plate");
|
||
let cyan = m.patches.first().unwrap().rect;
|
||
let mid = ((cyan.x0 + cyan.x1) / 2.0, (cyan.y0 + cyan.y1) / 2.0);
|
||
assert!(ink(Process::Cyan, mid) > 250, "the cyan patch inks cyan");
|
||
assert!([Process::Magenta, Process::Yellow, Process::Black].iter().all(|&c| ink(c, mid) == 0), "and nothing else");
|
||
assert!(all.iter().all(|&c| ink(c, (200.0, 150.0)) == 0), "the trim area is left alone");
|
||
}
|
||
|
||
/// A trim at the origin puts the sheet origin at negative coordinates; a bleed moves the marks out.
|
||
#[test]
|
||
fn drawn_marks_follow_the_sheet_origin_and_the_bleed() {
|
||
let trim = Rect::new(0.0, 0.0, 200.0, 100.0);
|
||
let m = Marks::new(trim, grown(trim, 9.0), &MarkOptions { line_width: 3.0, ..MarkOptions::default() }).unwrap();
|
||
assert_eq!((m.sheet.x0, m.sheet.y0), (-27.0, -27.0));
|
||
let p = render(&m);
|
||
let tint = |id: Process, (x, y): (f64, f64)| p.tint(InkId::process(id), (x - m.sheet.x0) as u32, (y - m.sheet.y0) as u32).unwrap();
|
||
assert!(tint(Process::Black, (-18.0, 0.0)) > 200, "the top-left crop mark, past the bleed");
|
||
assert!(tint(Process::Black, (-13.0, -9.0)) > 200, "the top-left bleed mark");
|
||
assert_eq!(tint(Process::Black, (-5.0, 0.0)), 0, "nothing inside the bleed");
|
||
}
|