mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 10:37:11 -06:00
Soft Proofing's Profile menu takes a printer / paper ICC profile (Other…;
`view.softProof {profile, intent, paperInk}`), RGB or CMYK, matrix or LUT.
The CMS (moxcms) builds a 33^3 proof table over the working space: forward with
the chosen intent (Perceptual / Relative), back relative colorimetric; the
destination gamut warning comes from the colorimetric round trip. Simulate
Paper & Ink renders absolute colorimetric: the profile's media white replaces
screen white and the ink black is unscaled. The pipeline gets the table by id
(`Proof::profile`, `register_proof_profile`), so Proof stays Copy and render
keys include it. Proof copies are named after the profile.
Sources: ICC.1:2022 (rendering intents, media white point, absolute
colorimetric); own design for the proof table.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
63 lines
3.0 KiB
Rust
63 lines
3.0 KiB
Rust
//! Soft proofing against an output-device ICC profile (a printer and paper): loads the profile,
|
|
//! builds its proof table through the CMS and registers it with the pipeline, which a
|
|
//! [`Proof`](lightcraft_pipeline::Proof) then names by id.
|
|
|
|
use lightcraft_codecs::display::MAX_PROFILE_BYTES;
|
|
pub use lightcraft_codecs::proof::ProofIntent;
|
|
|
|
/// A device profile ready to proof against.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct ProofProfile {
|
|
/// Pass as [`Proof::profile`](lightcraft_pipeline::Proof::profile).
|
|
pub id: u64,
|
|
/// The profile's description, else the file name.
|
|
pub name: String,
|
|
}
|
|
|
|
/// Load the profile at `path` for proofing with `intent`, simulating paper and ink or not.
|
|
pub fn load(path: &str, intent: ProofIntent, paper_ink: bool) -> Result<ProofProfile, String> {
|
|
let path = path.trim();
|
|
let meta = std::fs::metadata(path).map_err(|e| format!("{path}: {e}"))?;
|
|
if meta.len() > MAX_PROFILE_BYTES as u64 {
|
|
return Err(format!("{path}: too large for an ICC profile"));
|
|
}
|
|
let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?;
|
|
let fallback = std::path::Path::new(path).file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
|
|
from_bytes(&bytes, intent, paper_ink, &fallback).map_err(|e| format!("{path}: {e}"))
|
|
}
|
|
|
|
/// [`load`] from the profile's bytes; `fallback_name` names a profile without a description.
|
|
pub fn from_bytes(bytes: &[u8], intent: ProofIntent, paper_ink: bool, fallback_name: &str) -> Result<ProofProfile, String> {
|
|
let table = lightcraft_codecs::proof::proof_table(bytes, intent, paper_ink)?;
|
|
let lut = lightcraft_pipeline::ProofLut::new(table.n, table.data).ok_or("the profile's tables give no usable colours")?;
|
|
let id = {
|
|
use std::hash::{Hash, Hasher};
|
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
|
(bytes, intent, paper_ink).hash(&mut h);
|
|
h.finish()
|
|
};
|
|
let id = lightcraft_pipeline::register_proof_profile(id, lut);
|
|
Ok(ProofProfile { id, name: table.description.unwrap_or_else(|| fallback_name.to_string()) })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use lightcraft_codecs::space::NamedSpace;
|
|
|
|
#[test]
|
|
fn profiles_register_by_content_and_options() {
|
|
let p3 = lightcraft_codecs::icc::write_named(NamedSpace::DisplayP3);
|
|
let a = from_bytes(&p3, ProofIntent::Perceptual, false, "x").unwrap();
|
|
let b = from_bytes(&p3, ProofIntent::Perceptual, true, "x").unwrap();
|
|
assert_ne!(a.id, b.id);
|
|
assert_eq!(a, from_bytes(&p3, ProofIntent::Perceptual, false, "x").unwrap());
|
|
assert!(lightcraft_pipeline::proof_profile(a.id).is_some());
|
|
assert!(!a.name.is_empty());
|
|
assert!(from_bytes(b"nope", ProofIntent::Relative, false, "x").is_err());
|
|
assert!(load("/nonexistent/printer.icc", ProofIntent::Relative, false).is_err());
|
|
assert_eq!(ProofIntent::parse("Relative"), Some(ProofIntent::Relative));
|
|
assert_eq!(ProofIntent::parse("saturation"), None);
|
|
}
|
|
}
|