mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 23:59:16 +00:00
prepress: to_pdf_color writes ink colors as DeviceCMYK, Separation or DeviceN
Process tints become DeviceCMYK, one spot a Separation with its alternate, other mixes a DeviceN space with process inks first, registration and None the All and None separations. Lab spot alternates convert through the printing condition, as resolve does.
This commit is contained in:
1 parent
4026931196
commit
e6d2a24641
5 files changed
+208
-8
No files matched your search
@@ -11,6 +11,6 @@ rust-version.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-pdf = { workspace = true, features = ["raster"] }
|
||||
craft-pdf = { workspace = true, features = ["raster", "write"] }
|
||||
craft-cms = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -6,7 +6,8 @@ Prepress for the Craft apps: separations, ink limits and the ink manager.
|
||||
(always first, in plate order) and the spot inks of a job, and is the job's ink manager
|
||||
(aliases, spots to process); `InkColor` is a color as ink tints (`Separation`, `DeviceN`,
|
||||
registration or none); `Overprint` holds `op`, `OP` and `OPM`. Spot names are compared as
|
||||
PDF names: `/Gold#20Foil` and `Gold Foil` are one ink.
|
||||
PDF names: `/Gold#20Foil` and `Gold Foil` are one ink. `to_pdf_color` writes an `InkColor`
|
||||
as a krilla color for export.
|
||||
- `plates`: one 8-bit plane per ink (255 = 100% ink) rendered from a PDF page.
|
||||
- `coverage`: ink coverage at a pixel and over a page, total area coverage (TAC) per pixel,
|
||||
and the pixels and regions over a TAC limit.
|
||||
@@ -131,15 +132,42 @@ alternate needs the printing condition to become CMYK.
|
||||
|
||||
Apply it in two places:
|
||||
|
||||
- **Export.** The writer calls `resolve` on every color it emits, so the PDF carries the
|
||||
result: DeviceCMYK when `as_cmyk` is `Some`, else a `Separation` or `DeviceN` of the
|
||||
resolved inks (the aliased spot's name, not the original). An overprinting color that
|
||||
became CMYK is written with `OPM 1`, so its zero components leave the plates under it as
|
||||
they are.
|
||||
- **Export.** The writer calls `resolve` on every color it emits, then
|
||||
`to_pdf_color(&resolved, &inks, &cms)` for the krilla color to paint with (see
|
||||
[Export](#export)). An overprinting color that became CMYK is written with `OPM 1`, so its
|
||||
zero components leave the plates under it as they are.
|
||||
- **Preview.** The preview separates that exported PDF with `plates` and shows
|
||||
`composite` and the coverage readings. Plates are the truth: an ink manager change shows
|
||||
after the next export, and the preview never recomputes overprint from the document.
|
||||
|
||||
## Export
|
||||
|
||||
`to_pdf_color(&color, &inks, &cms)` gives the krilla color (`craft_pdf::write::backend::color::Color`)
|
||||
for an `InkColor`, choosing the color space from the inks it names:
|
||||
|
||||
| Color | Written as |
|
||||
|---|---|
|
||||
| Tints of process inks only | DeviceCMYK; unlisted process inks are 0 |
|
||||
| One spot ink | `Separation` with the spot's alternate as DeviceCMYK fallback |
|
||||
| Any other mix | `DeviceN`: process inks first in plate order, then spots in `InkSet` order |
|
||||
| `All` | `Separation /All` (registration) |
|
||||
| `None` | `Separation /None` (marks no plate) |
|
||||
|
||||
A spot's alternate is `spot_cmyk`: a CMYK alternate as given, a Lab one converted through
|
||||
`cms` into the printing condition, so a spot has one alternate in every space that names it.
|
||||
`None` when the color names an ink not in the set or a `DeviceN` space would need more than
|
||||
32 colorants.
|
||||
|
||||
```rust
|
||||
use craft_prepress::{CmsConversion, InkColor, to_pdf_color};
|
||||
|
||||
let cms = CmsConversion::fogra39();
|
||||
let printed = inks.resolve(&InkColor::Tints(vec![(gold, 1.0), (black, 0.5)]), &cms);
|
||||
if let Some(color) = to_pdf_color(&printed, &inks, &cms) {
|
||||
surface.set_fill(Some(Fill { paint: color.into(), ..Default::default() })); // DeviceN [Black Gold]
|
||||
}
|
||||
```
|
||||
|
||||
## In DesignCraft and VectorCraft
|
||||
|
||||
- **Separations Preview**: export the spread to PDF
|
||||
|
||||
@@ -5,6 +5,9 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use craft_cms::math;
|
||||
use craft_pdf::interpret::color::CmykConversion;
|
||||
use craft_pdf::write::backend::color::device_n::{self, Colorant, DeviceNSpace};
|
||||
use craft_pdf::write::backend::color::separation::{self, SeparationColorant, SeparationSpace};
|
||||
use craft_pdf::write::backend::color::{Color as PdfColor, RegularColor, cmyk};
|
||||
|
||||
use crate::cms::lab_to_srgb8;
|
||||
|
||||
@@ -365,6 +368,84 @@ impl InkColor {
|
||||
}
|
||||
}
|
||||
|
||||
/// `color` as a krilla fill or stroke color, for the PDF writer. Pass the color as it prints,
|
||||
/// after [`InkSet::resolve`].
|
||||
///
|
||||
/// - Tints of process inks only: DeviceCMYK, unlisted inks 0 (so with `OPM 1` an overprinting
|
||||
/// paint leaves their plates as they are).
|
||||
/// - One spot ink alone: a `Separation` space with the spot's alternate as DeviceCMYK fallback.
|
||||
/// - Any other mix: a `DeviceN` space, process inks first in plate order, then spot inks in
|
||||
/// set order. Each spot keeps the alternate its `Separation` space has.
|
||||
/// - `All`: the registration color, a `Separation /All` space; `None`: a `Separation /None`
|
||||
/// space, which marks no plate.
|
||||
///
|
||||
/// Spot alternates are [`InkSet::spot_cmyk`]: a CMYK alternate as given, a Lab one through
|
||||
/// `cms` into the printing condition. A tint listed twice combines as overlapping halftones,
|
||||
/// as in `resolve`. `None` when a tint's ink is not in `inks`, a spot's Lab alternate can't be
|
||||
/// converted, a spot name is empty, or a `DeviceN` space would have more than 32 colorants.
|
||||
pub fn to_pdf_color(color: &InkColor, inks: &InkSet, cms: &dyn CmykConversion) -> Option<PdfColor> {
|
||||
let tints = match color {
|
||||
InkColor::All(tint) => return Some(separation(SeparationColorant::AllColorants, *tint, cmyk::Color::new(255, 255, 255, 255))),
|
||||
InkColor::None => return Some(separation(SeparationColorant::NoColorant, 0.0, cmyk::Color::new(0, 0, 0, 0))),
|
||||
InkColor::Tints(tints) => tints,
|
||||
};
|
||||
// Sorted by id: process inks first, in plate order, then spots in set order.
|
||||
let mut merged: BTreeMap<InkId, f32> = BTreeMap::new();
|
||||
for &(id, tint) in tints {
|
||||
inks.get(id)?;
|
||||
let t = merged.entry(id).or_insert(0.0);
|
||||
*t = 1.0 - (1.0 - *t) * (1.0 - unit(tint));
|
||||
}
|
||||
if merged.keys().all(|id| id.index() < Process::ALL.len()) {
|
||||
let mut cmyk = [0_u8; 4];
|
||||
for (id, tint) in &merged {
|
||||
if let Some(c) = cmyk.get_mut(id.index()) {
|
||||
*c = byte(*tint);
|
||||
}
|
||||
}
|
||||
let [c, m, y, k] = cmyk;
|
||||
return Some(RegularColor::Cmyk(cmyk::Color::new(c, m, y, k)).into());
|
||||
}
|
||||
let spot = |id: InkId| -> Option<(String, cmyk::Color)> {
|
||||
let name = inks.get(id)?.name();
|
||||
let [c, m, y, k] = inks.spot_cmyk(id, cms)?.map(byte);
|
||||
(!name.is_empty()).then(|| (name.to_owned(), cmyk::Color::new(c, m, y, k)))
|
||||
};
|
||||
if let [(&id, &tint)] = merged.iter().collect::<Vec<_>>()[..] {
|
||||
let (name, alternate) = spot(id)?;
|
||||
return Some(separation(SeparationColorant::Custom(name), tint, alternate));
|
||||
}
|
||||
let mut colorants = Vec::with_capacity(merged.len());
|
||||
for &id in merged.keys() {
|
||||
colorants.push(match Process::ALL.get(id.index()) {
|
||||
Some(p) => Colorant::Process(match p {
|
||||
Process::Cyan => device_n::Process::Cyan,
|
||||
Process::Magenta => device_n::Process::Magenta,
|
||||
Process::Yellow => device_n::Process::Yellow,
|
||||
Process::Black => device_n::Process::Black,
|
||||
}),
|
||||
None => {
|
||||
let (name, alternate) = spot(id)?;
|
||||
Colorant::Spot { name, alternate }
|
||||
}
|
||||
});
|
||||
}
|
||||
let space = DeviceNSpace::new(colorants)?;
|
||||
Some(device_n::Color::new(merged.values().map(|t| byte(*t)).collect(), space)?.into())
|
||||
}
|
||||
|
||||
fn unit(v: f32) -> f32 {
|
||||
if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 }
|
||||
}
|
||||
|
||||
fn byte(v: f32) -> u8 {
|
||||
(unit(v) * 255.0).round() as u8
|
||||
}
|
||||
|
||||
fn separation(colorant: SeparationColorant, tint: f32, fallback: cmyk::Color) -> PdfColor {
|
||||
separation::Color::new(byte(tint), SeparationSpace::new(colorant, RegularColor::Cmyk(fallback))).into()
|
||||
}
|
||||
|
||||
/// Overprint parameters of a paint (ISO 32000-2 §8.6.7).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Overprint {
|
||||
|
||||
@@ -12,5 +12,5 @@ pub mod plates;
|
||||
|
||||
pub use cms::{CmsConversion, ProfileError};
|
||||
pub use coverage::{Above, Coverage, InkStats, MAX_REGIONS, Region, Stats};
|
||||
pub use ink::{Alternate, Ink, InkColor, InkId, InkSet, Overprint, Process, Spot, normalize_name};
|
||||
pub use ink::{Alternate, Ink, InkColor, InkId, InkSet, Overprint, Process, Spot, normalize_name, to_pdf_color};
|
||||
pub use plates::{Conversion, Error, Fallback, MAX_PLATE_PIXELS, MAX_SPOT_INKS, Options, Plates, plates};
|
||||
@@ -158,3 +158,94 @@ fn lab_colors_convert_through_the_profile_and_lab_white_is_paper() {
|
||||
assert!(near(at(10), expected), "{:?}, FOGRA39 prints {expected:?}", at(10));
|
||||
assert_eq!(at(30), [0, 0, 0, 0], "a Lab space's own white prints as paper");
|
||||
}
|
||||
|
||||
/// A 60 × 10 pt page written by the PDF writer, with a 10 pt wide fill of each color.
|
||||
fn written(colors: Vec<craft_pdf::write::backend::color::Color>) -> Vec<u8> {
|
||||
use craft_pdf::write::backend::{geom, page::PageSettings, paint::Fill};
|
||||
let mut writer = craft_pdf::write::Writer::new(Default::default(), Default::default());
|
||||
let mut page = writer.document_mut().start_page_with(PageSettings::new(geom::Size::from_wh(60.0, 10.0).unwrap()));
|
||||
let mut surface = page.surface();
|
||||
for (i, color) in colors.into_iter().enumerate() {
|
||||
let mut path = geom::PathBuilder::new();
|
||||
path.push_rect(geom::Rect::from_xywh(10.0 * i as f32, 0.0, 10.0, 10.0).unwrap());
|
||||
surface.set_fill(Some(Fill { paint: color.into(), ..Default::default() }));
|
||||
surface.draw_path(&path.finish().unwrap());
|
||||
}
|
||||
surface.finish();
|
||||
page.finish();
|
||||
writer.finish().unwrap()
|
||||
}
|
||||
|
||||
/// The page's color space resources as (family, colorant names), sorted.
|
||||
fn color_spaces(pdf: &[u8]) -> Vec<(String, Vec<String>)> {
|
||||
use craft_pdf::syntax::object::{Array, Name, Object};
|
||||
let pdf = craft_pdf::syntax::Pdf::new(Arc::new(pdf.to_vec())).unwrap();
|
||||
let page = pdf.pages().iter().next().unwrap();
|
||||
let spaces = &page.resources().color_spaces;
|
||||
let mut out: Vec<(String, Vec<String>)> = spaces
|
||||
.keys()
|
||||
.map(|key| {
|
||||
let array = spaces.get::<Array<'_>>(key.as_ref()).unwrap();
|
||||
let items: Vec<Object<'_>> = array.iter::<Object<'_>>().collect();
|
||||
let Some(Object::Name(family)) = items.first() else { panic!("a color space without a family") };
|
||||
let names = match items.get(1) {
|
||||
Some(Object::Name(n)) => vec![n.as_str().to_owned()],
|
||||
Some(Object::Array(a)) => a.iter::<Name<'_>>().map(|n| n.as_str().to_owned()).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
(family.as_str().to_owned(), names)
|
||||
})
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_colors_write_as_cmyk_separation_or_device_n_and_separate_onto_their_plates() {
|
||||
use craft_prepress::{InkColor, InkSet, Spot, to_pdf_color};
|
||||
let mut inks = InkSet::new();
|
||||
let gold = inks.add(Ink::Spot(Spot { name: "Gold".into(), alternate: Alternate::Cmyk([0.0, 0.3, 1.0, 0.0]), solidity: None })).unwrap();
|
||||
let teal = inks.add(Ink::Spot(Spot { name: "Teal".into(), alternate: Alternate::Lab([55.0, -40.0, -10.0]), solidity: None })).unwrap();
|
||||
let (c, k) = (InkId::process(Process::Cyan), black());
|
||||
let cms = CmsConversion::fogra39();
|
||||
let colors = [
|
||||
InkColor::Tints(vec![(k, 0.4), (c, 0.6)]),
|
||||
InkColor::Tints(vec![(gold, 0.5)]),
|
||||
InkColor::Tints(vec![(gold, 1.0), (k, 0.5)]),
|
||||
InkColor::Tints(vec![(teal, 1.0)]),
|
||||
InkColor::All(1.0),
|
||||
InkColor::None,
|
||||
];
|
||||
let pdf = written(colors.iter().map(|color| to_pdf_color(color, &inks, &cms).unwrap()).collect());
|
||||
let names = |n: &[&str]| n.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
let mut expected = vec![
|
||||
("DeviceN".to_owned(), names(&["Black", "Gold"])),
|
||||
("Separation".to_owned(), names(&["All"])),
|
||||
("Separation".to_owned(), names(&["Gold"])),
|
||||
("Separation".to_owned(), names(&["None"])),
|
||||
("Separation".to_owned(), names(&["Teal"])),
|
||||
];
|
||||
expected.sort();
|
||||
assert_eq!(color_spaces(&pdf), expected, "process only is DeviceCMYK, which needs no resource");
|
||||
|
||||
let p = plates(&pdf, 0, 72.0, &Options::default()).unwrap();
|
||||
let names: Vec<&str> = p.inks.iter().map(|(_, ink)| ink.name()).collect();
|
||||
assert_eq!(names, ["Cyan", "Magenta", "Yellow", "Black", "Gold", "Teal"]);
|
||||
let (gold, teal) = (p.inks.lookup("Gold").unwrap(), p.inks.lookup("Teal").unwrap());
|
||||
let at = |x: u32| -> Vec<u8> { p.inks.iter().map(|(id, _)| p.tint(id, x, 5).unwrap()).collect() };
|
||||
assert_eq!(at(5), [153, 0, 0, 102, 0, 0]);
|
||||
assert_eq!(at(15), [0, 0, 0, 0, 128, 0]);
|
||||
assert_eq!(at(25), [0, 0, 0, 128, 255, 0]);
|
||||
assert_eq!(at(35), [0, 0, 0, 0, 0, 255]);
|
||||
assert_eq!(at(45), [255; 6], "registration");
|
||||
assert_eq!(at(55), [0; 6]);
|
||||
assert_eq!((p.tint(gold, 25, 5), p.tint(teal, 35, 5)), (Some(255), Some(255)));
|
||||
|
||||
let unknown = InkSet::new();
|
||||
assert_eq!(to_pdf_color(&colors[1], &unknown, &cms), None, "an ink not in the set");
|
||||
let mut many = InkSet::new();
|
||||
let tints: Vec<(InkId, f32)> = (0..33)
|
||||
.map(|i| (many.add(Ink::Spot(Spot { name: format!("S{i}"), alternate: Alternate::Cmyk([0.0; 4]), solidity: None })).unwrap(), 1.0))
|
||||
.collect();
|
||||
assert_eq!(to_pdf_color(&InkColor::Tints(tints), &many, &cms), None, "more than 32 colorants");
|
||||
}
|
||||
Reference in new issue
Block a user