mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Colour: craft-cms ace3622 (Result APIs, device white is no ink)
This commit is contained in:
Generated
+1
-1
@@ -811,7 +811,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-cms"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/aiongg/craft-libs.git?rev=6710c2353842eab1be23840aafaf16a20811d25c#6710c2353842eab1be23840aafaf16a20811d25c"
|
||||
source = "git+https://github.com/aiongg/craft-libs.git?rev=ace3622d427387cb197e7997ce116241ea290ed9#ace3622d427387cb197e7997ce116241ea290ed9"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ indd = { git = "https://github.com/aiongg/indd-utils", branch = "main" }
|
||||
craft-print = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" }
|
||||
|
||||
# ICC colour management: working spaces, transforms, black point compensation (craft-libs).
|
||||
craft-cms = { git = "https://github.com/aiongg/craft-libs.git", rev = "6710c2353842eab1be23840aafaf16a20811d25c" }
|
||||
craft-cms = { git = "https://github.com/aiongg/craft-libs.git", rev = "ace3622d427387cb197e7997ce116241ea290ed9" }
|
||||
|
||||
# ui
|
||||
# Shared frontend components; keep this dependency above the engine boundary.
|
||||
|
||||
@@ -53,7 +53,7 @@ fn eval(x: &Transform, src: &[f32], outputs: usize) -> Option<Vec<f32>> {
|
||||
}
|
||||
let src: Vec<f32> = src.iter().map(|v| if v.is_finite() { v.clamp(0.0, 1.0) } else { 0.0 }).collect();
|
||||
let mut out = vec![0.0f32; outputs];
|
||||
x.eval(&src, &mut out);
|
||||
x.eval(&src, &mut out).ok()?;
|
||||
out.iter().all(|o| o.is_finite()).then(|| out.into_iter().map(|v| v.clamp(0.0, 1.0)).collect())
|
||||
}
|
||||
|
||||
@@ -118,19 +118,9 @@ impl IccProfile {
|
||||
eval3(&*self.to_xf(intent, bpc)?, &src)
|
||||
}
|
||||
|
||||
/// Device values of the medium's white: no ink, or full RGB / grey.
|
||||
fn device_white(&self) -> Vec<f32> {
|
||||
vec![if self.kind == ProfileKind::Cmyk { 0.0 } else { 1.0 }; self.channels()]
|
||||
}
|
||||
|
||||
/// sRGB → device values. White is the medium's white (no ink) in every intent but absolute
|
||||
/// colorimetric, exactly: craft-cms's exact evaluation leaves a few tenths of a percent of
|
||||
/// ink there.
|
||||
/// sRGB → device values.
|
||||
pub fn from_srgb(&self, rgb: [f32; 3], intent: Intent, bpc: bool) -> Option<Vec<f32>> {
|
||||
let x = self.inverse_xf(intent, bpc)?;
|
||||
if intent != Intent::AbsoluteColorimetric && rgb.iter().all(|v| *v >= 1.0) {
|
||||
return Some(self.device_white());
|
||||
}
|
||||
eval(&x, &rgb, self.channels())
|
||||
}
|
||||
|
||||
@@ -148,9 +138,6 @@ impl IccProfile {
|
||||
/// Lab (D50) → device values with `intent`.
|
||||
pub fn from_lab(&self, lab: Lab, intent: Intent, bpc: bool) -> Option<Vec<f32>> {
|
||||
let x = slot(&self.from_lab, intent, bpc).get_or_init(|| build(Builtin::LabD50.profile(), &self.profile, intent, bpc)).clone()?;
|
||||
if intent != Intent::AbsoluteColorimetric && lab.l >= 100.0 && lab.a.abs() < 1e-3 && lab.b.abs() < 1e-3 {
|
||||
return Some(self.device_white());
|
||||
}
|
||||
eval(&x, &[lab.l / 100.0, (lab.a + 128.0) / 255.0, (lab.b + 128.0) / 255.0], self.channels())
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::sync::OnceLock;
|
||||
use craft_cms::profile::{Lut, LutKind};
|
||||
use craft_cms::{Builtin, Clut, Curve, Intent as CmsIntent, Profile, pipeline::Stage};
|
||||
|
||||
use super::Intent;
|
||||
use super::lab::Lab;
|
||||
use super::{CmsError, Intent};
|
||||
|
||||
const A2B_GRID: usize = 9;
|
||||
const B2A_GRID: usize = 17;
|
||||
@@ -28,46 +28,51 @@ fn dec_lab(v: &[f32]) -> Lab {
|
||||
}
|
||||
|
||||
fn lut16(clut: Clut) -> Lut {
|
||||
let (i, o) = (clut.inputs, clut.outputs);
|
||||
let (i, o) = (clut.inputs(), clut.outputs());
|
||||
let ident = |n: usize| Stage::Curves(vec![Curve::Table(vec![0.0, 1.0]); n]);
|
||||
Lut { kind: LutKind::Lut16, inputs: i, outputs: o, stages: vec![ident(i), Stage::Clut(clut), ident(o)] }
|
||||
}
|
||||
|
||||
/// The Generic CMYK model as an ICC output profile (built once).
|
||||
pub fn generic_cmyk_profile() -> &'static Profile {
|
||||
static P: OnceLock<Profile> = OnceLock::new();
|
||||
P.get_or_init(|| {
|
||||
let g = super::generic();
|
||||
// CMYK → Lab (media-relative).
|
||||
let a2b = Clut::sample(vec![A2B_GRID; 4], 3, |inp, out| {
|
||||
let at = |i: usize| inp.get(i).copied().unwrap_or(0.0);
|
||||
out.copy_from_slice(&enc_lab(g.to_lab_rel([at(0), at(1), at(2), at(3)])));
|
||||
});
|
||||
// Lab → CMYK through the model's separation.
|
||||
let b2a = Clut::sample(vec![B2A_GRID; 3], 4, |inp, out| {
|
||||
let cmyk = g.from_lab(dec_lab(inp), Intent::RelativeColorimetric, true);
|
||||
out.copy_from_slice(&cmyk.map(|v| v.clamp(0.0, 1.0)));
|
||||
});
|
||||
let (a2b, b2a) = (lut16(a2b), lut16(b2a));
|
||||
// craft-cms profiles can't be constructed field by field outside the crate, so the
|
||||
// tables replace those of a built-in CMYK output profile and the result is re-encoded.
|
||||
let mut p = Builtin::CoatedCmyk.profile().clone();
|
||||
p.rendering_intent = CmsIntent::RelativeColorimetric;
|
||||
p.description = super::GENERIC_CMYK.into();
|
||||
p.copyright = "No copyright; made by DesignCraft from its parametric press model. Use freely.".into();
|
||||
p.white_point = [0.9642, 1.0, 0.8249];
|
||||
p.black_point = None;
|
||||
p.chad = None;
|
||||
p.a2b = [Some(a2b.clone()), Some(a2b.clone()), Some(a2b)];
|
||||
p.b2a = [Some(b2a.clone()), Some(b2a.clone()), Some(b2a)];
|
||||
p.with_encoded_bytes()
|
||||
pub fn generic_cmyk_profile() -> Result<&'static Profile, CmsError> {
|
||||
static P: OnceLock<Result<Profile, CmsError>> = OnceLock::new();
|
||||
P.get_or_init(build_generic_cmyk).as_ref().map_err(Clone::clone)
|
||||
}
|
||||
|
||||
fn build_generic_cmyk() -> Result<Profile, CmsError> {
|
||||
let bad = |e: craft_cms::CmsError| CmsError::BadProfile(format!("{}: {e}", super::GENERIC_CMYK));
|
||||
let g = super::generic();
|
||||
// CMYK → Lab (media-relative).
|
||||
let a2b = Clut::sample(vec![A2B_GRID; 4], 3, |inp, out| {
|
||||
let at = |i: usize| inp.get(i).copied().unwrap_or(0.0);
|
||||
out.copy_from_slice(&enc_lab(g.to_lab_rel([at(0), at(1), at(2), at(3)])));
|
||||
})
|
||||
.map_err(bad)?;
|
||||
// Lab → CMYK through the model's separation.
|
||||
let b2a = Clut::sample(vec![B2A_GRID; 3], 4, |inp, out| {
|
||||
let cmyk = g.from_lab(dec_lab(inp), Intent::RelativeColorimetric, true);
|
||||
out.copy_from_slice(&cmyk.map(|v| v.clamp(0.0, 1.0)));
|
||||
})
|
||||
.map_err(bad)?;
|
||||
let (a2b, b2a) = (lut16(a2b), lut16(b2a));
|
||||
// craft-cms profiles can't be constructed field by field outside the crate, so the
|
||||
// tables replace those of a built-in CMYK output profile and the result is re-encoded.
|
||||
let mut p = Builtin::CoatedCmyk.profile().clone();
|
||||
p.rendering_intent = CmsIntent::RelativeColorimetric;
|
||||
p.description = super::GENERIC_CMYK.into();
|
||||
p.copyright = "No copyright; made by DesignCraft from its parametric press model. Use freely.".into();
|
||||
p.white_point = [0.9642, 1.0, 0.8249];
|
||||
p.black_point = None;
|
||||
p.chad = None;
|
||||
p.a2b = [Some(a2b.clone()), Some(a2b.clone()), Some(a2b)];
|
||||
p.b2a = [Some(b2a.clone()), Some(b2a.clone()), Some(b2a)];
|
||||
Ok(p.with_encoded_bytes())
|
||||
}
|
||||
|
||||
/// [`generic_cmyk_profile`]'s ICC bytes.
|
||||
pub fn generic_cmyk_icc() -> &'static [u8] {
|
||||
static B: OnceLock<std::sync::Arc<Vec<u8>>> = OnceLock::new();
|
||||
B.get_or_init(|| generic_cmyk_profile().to_bytes()).as_slice()
|
||||
pub fn generic_cmyk_icc() -> Result<&'static [u8], CmsError> {
|
||||
static B: OnceLock<Result<std::sync::Arc<Vec<u8>>, CmsError>> = OnceLock::new();
|
||||
B.get_or_init(|| generic_cmyk_profile().map(Profile::to_bytes)).as_ref().map(|b| b.as_slice()).map_err(Clone::clone)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -76,7 +81,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn generic_cmyk_profile_round_trips() {
|
||||
let bytes = generic_cmyk_icc();
|
||||
let bytes = generic_cmyk_icc().unwrap();
|
||||
assert!(bytes.len() > 1000);
|
||||
assert_eq!(&bytes[36..40], b"acsp");
|
||||
assert_eq!(&bytes[16..20], b"CMYK");
|
||||
|
||||
@@ -327,8 +327,8 @@ impl Cms {
|
||||
pub fn cmyk_icc_profile(&self) -> Option<Arc<craft_cms::Profile>> {
|
||||
match &self.cmyk {
|
||||
CmykSpace::Generic(_) => {
|
||||
static P: OnceLock<Arc<craft_cms::Profile>> = OnceLock::new();
|
||||
Some(P.get_or_init(|| Arc::new(icc_out::generic_cmyk_profile().clone())).clone())
|
||||
static P: OnceLock<Option<Arc<craft_cms::Profile>>> = OnceLock::new();
|
||||
P.get_or_init(|| icc_out::generic_cmyk_profile().ok().map(|p| Arc::new(p.clone()))).clone()
|
||||
}
|
||||
CmykSpace::Device => None,
|
||||
CmykSpace::Icc(p) => Some(p.profile().clone()),
|
||||
@@ -337,16 +337,16 @@ impl Cms {
|
||||
|
||||
/// The PDF/X output intent for the working CMYK space. Device CMYK has no profile and is
|
||||
/// described by the Generic CMYK one.
|
||||
pub fn output_intent(&self) -> OutputIntent {
|
||||
match &self.cmyk {
|
||||
pub fn output_intent(&self) -> Result<OutputIntent, CmsError> {
|
||||
Ok(match &self.cmyk {
|
||||
CmykSpace::Icc(p) if p.name == COATED_FOGRA39 => {
|
||||
OutputIntent { icc: Cow::Borrowed(craft_cms::builtin::COATED_FOGRA39_ICC), identifier: "FOGRA39", condition: COATED_FOGRA39.into() }
|
||||
}
|
||||
CmykSpace::Icc(p) => OutputIntent { icc: Cow::Owned(p.profile().to_bytes().to_vec()), identifier: "Custom", condition: p.name.clone() },
|
||||
CmykSpace::Generic(_) | CmykSpace::Device => {
|
||||
OutputIntent { icc: Cow::Borrowed(icc_out::generic_cmyk_icc()), identifier: "Custom", condition: GENERIC_CMYK.into() }
|
||||
OutputIntent { icc: Cow::Borrowed(icc_out::generic_cmyk_icc()?), identifier: "Custom", condition: GENERIC_CMYK.into() }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Working RGB → display sRGB.
|
||||
|
||||
@@ -77,13 +77,13 @@ fn saved_cmyk_names_still_load() {
|
||||
|
||||
#[test]
|
||||
fn output_intent_follows_the_working_cmyk_space() {
|
||||
let fogra = cms().output_intent();
|
||||
let fogra = cms().output_intent().unwrap();
|
||||
assert_eq!((fogra.identifier, fogra.condition.as_str()), ("FOGRA39", COATED_FOGRA39));
|
||||
assert_eq!(&*fogra.icc, craft_cms::builtin::COATED_FOGRA39_ICC);
|
||||
for name in [GENERIC_CMYK, DEVICE_CMYK] {
|
||||
let g = Cms::new(&ColorSettings { cmyk: name.into(), ..Default::default() }).unwrap().output_intent();
|
||||
let g = Cms::new(&ColorSettings { cmyk: name.into(), ..Default::default() }).unwrap().output_intent().unwrap();
|
||||
assert_eq!((g.identifier, g.condition.as_str()), ("Custom", GENERIC_CMYK), "{name}");
|
||||
assert_eq!(&*g.icc, icc_out::generic_cmyk_icc());
|
||||
assert_eq!(&*g.icc, icc_out::generic_cmyk_icc().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ fn output_intent_follows_the_working_cmyk_space() {
|
||||
fn paper_white_and_rich_black() {
|
||||
let c = cms();
|
||||
assert_eq!(c.srgb_to_cmyk([1.0, 1.0, 1.0], Intent::RelativeColorimetric), [0.0; 4]);
|
||||
for intent in [Intent::Perceptual, Intent::RelativeColorimetric, Intent::Saturation] {
|
||||
assert_eq!(c.lab_to_cmyk(lab::Lab::new(100.0, 0.0, 0.0), intent), [0.0; 4], "{intent:?}");
|
||||
}
|
||||
let k = c.srgb_to_cmyk([0.0, 0.0, 0.0], Intent::RelativeColorimetric);
|
||||
assert!(k[3] > 0.8, "black generation puts most of the black in K: {k:?}");
|
||||
assert!(k.iter().sum::<f32>() <= 3.3 + 1e-3, "Coated FOGRA39 total ink limit (330 %): {k:?}");
|
||||
|
||||
@@ -122,13 +122,15 @@ fn to_srgb(src: &Profile, options: TransformOptions) -> Option<Arc<Transform>> {
|
||||
}
|
||||
|
||||
/// Convert interleaved pixels (`src_stride` / `dst_stride` values per pixel, colour channels
|
||||
/// first; the rest of `dst` is left alone). `false` when the strides don't fit the transform.
|
||||
/// first; the rest of `dst` is left alone). `false` when the buffers don't fit the transform.
|
||||
fn convert(x: &Transform, src: &[u8], src_stride: usize, dst: &mut [u8], dst_stride: usize) -> bool {
|
||||
if x.inputs() == 0 || x.inputs() > src_stride || x.outputs() == 0 || x.outputs() > dst_stride {
|
||||
return false;
|
||||
match x.convert_u8(src, src_stride, dst, dst_stride, false) {
|
||||
Ok(()) => true,
|
||||
Err(e) => {
|
||||
log::warn!("image ICC conversion: {e}; profile ignored");
|
||||
false
|
||||
}
|
||||
}
|
||||
x.convert_u8(src, src_stride, dst, dst_stride, false);
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether an RGB transform leaves every value within 1 of where it was (an sRGB profile): the
|
||||
|
||||
@@ -184,7 +184,7 @@ fn cmyk_uses_its_own_profile_before_the_working_space() {
|
||||
assert_eq!(img.get_pixel(0, 0).0, [0, 255, 255, 255]);
|
||||
assert_eq!(img.get_pixel(1, 0).0, [0, 0, 0, 128]);
|
||||
// An embedded profile wins over it.
|
||||
let icc = cms::icc_out::generic_cmyk_icc();
|
||||
let icc = cms::icc_out::generic_cmyk_icc().unwrap();
|
||||
let img = to_display_rgba(Pixels::Cmyk(raster()), Some(icc), &device).unwrap();
|
||||
assert_eq!(img.get_pixel(0, 0).0, shown([255, 0, 0, 0], &DisplayColour::of(&generic(true))));
|
||||
assert_eq!(img.get_pixel(1, 0).0[3], 128, "alpha kept");
|
||||
@@ -227,7 +227,7 @@ fn gray_with_an_embedded_profile_is_converted() {
|
||||
fn unusable_profiles_leave_the_pixels_alone() {
|
||||
let px = [64, 192, 64, 255];
|
||||
let adobe = adobe_rgb_icc();
|
||||
let cmyk = cms::icc_out::generic_cmyk_icc().to_vec();
|
||||
let cmyk = cms::icc_out::generic_cmyk_icc().unwrap().to_vec();
|
||||
let mut huge = adobe.clone();
|
||||
huge.resize(MAX_ICC_BYTES + 1, 0);
|
||||
let mut lying = adobe.clone();
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn make_pdfx4(pdf: &[u8], title: &str) -> Option<Vec<u8>> {
|
||||
out.extend_from_slice(b"\nendobj\n");
|
||||
};
|
||||
// The output profile and the output intent.
|
||||
let intent = designcraft_color::cms::active().output_intent();
|
||||
let intent = designcraft_color::cms::active().output_intent().ok()?;
|
||||
let (icc_id, oi_id) = (size, size + 1);
|
||||
let mut s = format!("<</N 4/Length {}>>\nstream\n", intent.icc.len()).into_bytes();
|
||||
s.extend_from_slice(&intent.icc);
|
||||
|
||||
Reference in New Issue
Block a user