krilla: CMYK, gray and spot colors keep their exact inks (patch 17) (#417)

* krilla: CMYK, gray and spot colors keep their exact inks (patch 17)

Gray, CMYK, Separation and DeviceN colors stored each component as a
u8, so every ink was written as a multiple of 1/255: C75 became
0.7490196 and C4 M4 Y4 K4 became 0.039215688.

- cmyk, luma, separation and device_n Color::new_f32 take components
  from 0 to 1 and write them as given. The u8 constructors are
  unchanged.
- Gradient stops rewritten into one DeviceN, Separation or CMYK space
  keep their components.
- Mesh gradients write corner colors at 16 bits a component.

* Tests: the 75% K mesh corner comment gives the asserted 0.749996
This commit is contained in:
.CooCoo
2026-10-31 23:30:23 -04:00
committed by GitHub
parent 9d17829194
commit fb08be5fa1
6 changed files with 667 additions and 58 deletions
+94 -8
View File
@@ -933,8 +933,8 @@ fn mesh(patches: Vec<MeshPatch>) -> backend::paint::Paint {
}
/// Each Type 6 shading in the file: color space family and colorant names, components a
/// corner (from `/Decode`), and the corner color bytes of each patch.
type MeshSummary = ((String, Vec<String>), usize, Vec<Vec<u8>>);
/// corner (from `/Decode`), and the 16-bit corner color components of each patch.
type MeshSummary = ((String, Vec<String>), usize, Vec<Vec<u16>>);
fn mesh_shadings(bytes: &[u8]) -> Vec<MeshSummary> {
let pdf = hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap();
@@ -946,21 +946,27 @@ fn mesh_shadings(bytes: &[u8]) -> Vec<MeshSummary> {
continue;
}
assert_eq!(dict.get::<i32>(b"BitsPerCoordinate".as_slice()), Some(32));
assert_eq!(dict.get::<i32>(b"BitsPerComponent".as_slice()), Some(8));
assert_eq!(dict.get::<i32>(b"BitsPerComponent".as_slice()), Some(16));
assert_eq!(dict.get::<i32>(b"BitsPerFlag".as_slice()), Some(8));
let cs = dict.get::<Object<'_>>(b"ColorSpace".as_slice()).unwrap();
let decode = dict.get::<Array<'_>>(b"Decode".as_slice()).unwrap().iter::<f32>().count();
let components = (decode - 4) / 2;
let data = stream.decoded().unwrap();
let patch_len = 1 + 12 * 8 + 4 * components;
let patch_len = 1 + 12 * 8 + 4 * 2 * components;
assert_eq!(data.len() % patch_len, 0, "whole patches of {components} components a corner");
let corners = data.chunks(patch_len).map(|patch| patch[1 + 12 * 8..].to_vec()).collect();
let samples = |bytes: &[u8]| -> Vec<u16> { bytes.chunks(2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect() };
let corners = data.chunks(patch_len).map(|patch| samples(&patch[1 + 12 * 8..])).collect();
out.push((colorant_names(&cs), components, corners));
}
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
/// 8-bit components as the 16-bit components that carry the same value (`v * 257`).
fn wide(components: Vec<u16>) -> Vec<u16> {
components.into_iter().map(|v| v * 257).collect()
}
fn gold_spot(tint: u8) -> backend::color::Color {
use backend::color::separation::{self, SeparationColorant, SeparationSpace};
separation::Color::new(tint, SeparationSpace::new(SeparationColorant::Custom("Gold".to_owned()), gold().into())).into()
@@ -981,7 +987,7 @@ fn mesh_mixing_process_and_spot_corners_is_a_device_n_shading() {
]
};
let names = |v: &[&str]| v.iter().map(|s| (*s).to_owned()).collect::<Vec<_>>();
let expected = vec![vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0], vec![0, 0, 255, 0, 0, 128, 255, 0, 0, 255, 0, 0]];
let expected = vec![wide(vec![255, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0]), wide(vec![0, 0, 255, 0, 0, 128, 255, 0, 0, 255, 0, 0])];
let opaque = page_with(|surface| fill_rect(surface, 0.0, mesh(patches(1.0))));
assert_eq!(mesh_shadings(&opaque), vec![(("DeviceN".to_owned(), names(&["Cyan", "Black", "Gold"])), 3, expected.clone())]);
@@ -989,7 +995,7 @@ fn mesh_mixing_process_and_spot_corners_is_a_device_n_shading() {
let translucent = page_with(|surface| fill_rect(surface, 0.0, mesh(patches(0.5))));
let found = mesh_shadings(&translucent);
assert_eq!(found.len(), 2, "{found:?}");
assert_eq!(found[0], (("DeviceGray".to_owned(), vec![]), 1, vec![vec![255; 4], vec![255, 255, 255, 128]]));
assert_eq!(found[0], (("DeviceGray".to_owned(), vec![]), 1, vec![wide(vec![255; 4]), vec![65535, 65535, 65535, 32768]]));
assert_eq!(found[1], (("DeviceN".to_owned(), names(&["Cyan", "Black", "Gold"])), 3, expected));
assert_eq!(soft_masks(&translucent).len(), 1);
}
@@ -1022,10 +1028,90 @@ fn rgb_mesh_is_a_device_rgb_shading() {
let rgb = |r, g, b| -> backend::color::Color { backend::color::rgb::Color::new(r, g, b).into() };
let patch = square(10.0, 10.0, 40.0, [rgb(255, 0, 0), rgb(0, 255, 0), rgb(0, 0, 255), rgb(255, 255, 255)], [1.0; 4]);
let bytes = page_with(|surface| fill_rect(surface, 0.0, mesh(vec![patch])));
let corners = vec![vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]];
let corners = vec![wide(vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255])];
assert_eq!(mesh_shadings(&bytes), vec![(("DeviceRGB".to_owned(), vec![]), 3, corners)]);
}
// Color precision (the `new_f32` constructors).
/// The first page's content stream as text.
fn content(bytes: &[u8]) -> String {
let pdf = hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap();
let page = pdf.pages().iter().next().unwrap();
String::from_utf8_lossy(page.page_stream().unwrap()).into_owned()
}
/// CMYK inks, gray, spot tints and DeviceN tints made with `new_f32` are written as given, so
/// C75 is 0.75 and not 191/255; the 8-bit constructors still write `value / 255`.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
fn f32_colors_are_written_without_rounding() {
use backend::color::separation::{self, SeparationColorant, SeparationSpace};
let gold_space = SeparationSpace::new(SeparationColorant::Custom("Gold".to_owned()), gold().into());
let space = DeviceNSpace::new([Colorant::Process(Process::Cyan), spot("Gold", gold())]).unwrap();
let colors: Vec<backend::color::Color> = vec![
cmyk::Color::new_f32(0.75, 0.0, 1.0, 0.0).into(),
cmyk::Color::new_f32(0.04, 0.04, 0.04, 0.04).into(),
luma::Color::new_f32(0.96).into(),
separation::Color::new_f32(0.75, gold_space).into(),
device_n::Color::new_f32(vec![0.45, 0.04], space).unwrap().into(),
cmyk::Color::new(191, 0, 255, 0).into(),
];
let text = content(&device_n_sample(cmyk_settings(), colors).unwrap());
for op in ["0.75 0 1 0 k", "0.04 0.04 0.04 0.04 k", "0.96 g", "0.75 scn", "0.45 0.04 scn", "0.7490196 0 1 0 k"] {
assert!(text.contains(op), "{op:?} in {text:?}");
}
}
/// `new_f32` clamps to 0 to 1 and reads NaN and -0 as 0, so a color equals (and hashes like)
/// the 8-bit color with the same components.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
fn f32_colors_clamp_and_compare_by_value() {
use std::collections::HashSet;
let odd = cmyk::Color::new_f32(1.5, -0.2, f32::NAN, -0.0);
assert_eq!(odd, cmyk::Color::new(255, 0, 0, 0));
assert_eq!(HashSet::from([odd, cmyk::Color::new(255, 0, 0, 0)]).len(), 1);
assert_eq!(luma::Color::new_f32(2.0), luma::Color::white());
let text = content(&page_with(|surface| fill_rect(surface, 0.0, odd.into())));
assert!(text.contains("1 0 0 0 k"), "{text:?}");
}
/// The colors of a shading's function (`/C0` and `/C1` of the first shading's exponential
/// function).
fn shading_ends(bytes: &[u8]) -> (Vec<f32>, Vec<f32>) {
let pdf = hayro_syntax::Pdf::new(Arc::new(bytes.to_vec())).unwrap();
let page = pdf.pages().iter().next().unwrap();
let patterns = &page.resources().patterns;
let key = patterns.keys().next().unwrap();
let pattern = patterns.get::<Dict<'_>>(key.as_ref()).unwrap();
let shading = pattern.get::<Dict<'_>>(b"Shading".as_slice()).unwrap();
let function = shading.get::<Dict<'_>>(b"Function".as_slice()).unwrap();
let floats = |key: &[u8]| -> Vec<f32> { function.get::<Array<'_>>(key).unwrap().iter::<f32>().collect() };
(floats(b"C0"), floats(b"C1"))
}
/// Gradient stops keep their inks when a spot and process colors are rewritten into one
/// DeviceN space, and mesh corners are written at 16 bits, so K 75% reads 0.749996 rather than
/// 191/255.
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[cfg_attr(not(target_arch = "wasm32"), test)]
fn f32_colors_keep_their_inks_in_shadings() {
use backend::color::separation::{self, SeparationColorant, SeparationSpace};
let gold_spot: backend::color::Color =
separation::Color::new_f32(0.75, SeparationSpace::new(SeparationColorant::Custom("Gold".to_owned()), gold().into())).into();
let cyan: backend::color::Color = cmyk::Color::new_f32(0.04, 0.0, 0.0, 0.0).into();
let bytes = page_with(|surface| fill_rect(surface, 0.0, linear([gold_spot, cyan])));
assert_eq!(shadings(&bytes)[0].0, ("DeviceN".to_owned(), vec!["Cyan".to_owned(), "Gold".to_owned()]));
assert_eq!(shading_ends(&bytes), (vec![0.0, 0.75], vec![0.04, 0.0]));
let k75 = || -> backend::color::Color { cmyk::Color::new_f32(0.0, 0.0, 0.0, 0.75).into() };
let patch = square(10.0, 10.0, 40.0, [k75(), k75(), k75(), k75()], [1.0; 4]);
let bytes = page_with(|surface| fill_rect(surface, 0.0, mesh(vec![patch])));
let corner = [0, 0, 0, 49151];
assert_eq!(mesh_shadings(&bytes), vec![(("DeviceCMYK".to_owned(), vec![]), 4, vec![corner.repeat(4)])]);
}
const FOGRA39: &[u8] = include_bytes!("../../cms/profiles/craft-coated-fogra39.icc");
fn text(dict: &Dict<'_>, key: &[u8]) -> Option<String> {
+20
View File
@@ -26,6 +26,7 @@ itself omits those texts. Its examples and four CC0 ICC profiles are retained wi
| `patches/14-devicen-pdf-version.patch` | DeviceN attributes by PDF version | `graphics/device_n.rs` |
| `patches/15-pdfx-conformance.patch` | PDF/X conformance key | `serialize.rs` |
| `patches/16-modification-date.patch` | Modification date | `interchange/metadata.rs` |
| `patches/17-color-precision.patch` | Color precision | `graphics/{color,shading_function}.rs` |
Each patch applies with `patch -p1` from this directory, in numeric order, to the pristine
crate. `SOURCE.json` records, for every retained file, the upstream hash (`upstream_sha256`)
@@ -410,3 +411,22 @@ Tests: `crates/pdf/tests/writing.rs` (`writes_pages_metadata_and_selected_versio
`the_creation_date_is_the_modification_date_when_none_is_given`) and
`crates/prepress/tests/flatten_document.rs`
(`a_document_flattens_page_by_page_and_keeps_boxes_metadata_and_shared_fonts`).
## 17: color precision
Upstream stores gray, CMYK, Separation and DeviceN components as `u8`, so every ink is written
as a multiple of 1/255: C75 becomes 0.7490196 and C4 M4 Y4 K4 becomes 0.039215688.
- `cmyk::Color::new_f32`, `luma::Color::new_f32`, `separation::Color::new_f32` and
`device_n::Color::new_f32` take components from 0 to 1 and write them as given. Values
outside 0 to 1 are clamped and NaN is 0.
- The `u8` constructors keep their signatures and write `value / 255`, as upstream does.
- The colors store each component as an `f32` whose bits `Eq` and `Hash` compare; -0 is
stored as 0, so equal components compare equal. `RegularColor::as_rgb` rounds gray to the
nearest 8-bit value.
- Gradient stops that are rewritten into one DeviceCMYK, Separation or DeviceN space keep
their components, and a DeviceN color's CMYK alternate (annotations, tags) is no longer
rounded.
- Mesh gradients write their corner colors and opacities at 16 bits a component instead of 8.
Tests: `crates/pdf/tests/print_color.rs` (`f32_colors_*`, `mesh_*`, `rgb_mesh_is_a_device_rgb_shading`).
+2 -2
View File
@@ -152,7 +152,7 @@
{
"path": "src/graphics/color.rs",
"upstream_sha256": "e5843d33ef184ab5c458025d1b1dc53df6ee6aaf085595ea14604d080b98f8a4",
"sha256": "553c2c1ce7a7c26dc11a227d70db0e30933e0b0e69d2d6c550e1384c0a3037ec"
"sha256": "5b0de85e0f3eac2905f975f676f1ef5d0eebb99a9a35670fefb419ed36fed6cf"
},
{
"path": "src/graphics/device_n.rs",
@@ -207,7 +207,7 @@
{
"path": "src/graphics/shading_function.rs",
"upstream_sha256": "0e0fc8bb1a32272457ef560fa80933972d0f5ef5fb7046424c6de48093cfd978",
"sha256": "2831ec0d78a755db9311a16d01afd5c9f714e4d43bdf69b42c0cb291387ca743"
"sha256": "6da45a96a98d32d4ef878fa03ac1fddbccccea2e2d6333d3a0e90ca009061f81"
},
{
"path": "src/graphics/shading_pattern.rs",
+411
View File
@@ -0,0 +1,411 @@
diff --git a/src/graphics/color.rs b/src/graphics/color.rs
index 6fb0618..06a3098 100644
--- a/src/graphics/color.rs
+++ b/src/graphics/color.rs
@@ -69,7 +69,7 @@
//! [`SerializeSettings`]: crate::SerializeSettings
use std::fmt::Debug;
-use std::hash::Hash;
+use std::hash::{Hash, Hasher};
use crate::configure::ValidationError;
use crate::graphics::icc::ICCBasedColorSpace;
@@ -82,6 +82,48 @@ pub(crate) const DEVICE_GRAY: &str = "DeviceGray";
/// The PDF name for the device CMYK color space.
pub(crate) const DEVICE_CMYK: &str = "DeviceCMYK";
+/// A color component from 0 to 1: a gray level, a process ink or a spot tint.
+///
+/// `new` clamps the value to 0 to 1 and turns NaN and -0 into 0, so equal components have equal
+/// bits; `Eq` and `Hash` compare the bits.
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct Tint(f32);
+
+impl Tint {
+ pub(crate) fn new(value: f32) -> Self {
+ // `max` returns the other operand for NaN; adding 0 turns -0 into 0.
+ Tint(value.max(0.0).min(1.0) + 0.0)
+ }
+
+ /// The component `value / 255`, as the 8-bit constructors give it.
+ pub(crate) fn from_u8(value: u8) -> Self {
+ Tint(f32::from(value) / 255.0)
+ }
+
+ pub(crate) fn get(self) -> f32 {
+ self.0
+ }
+
+ /// The nearest 8-bit value.
+ pub(crate) fn to_u8(self) -> u8 {
+ (self.0 * 255.0).round() as u8
+ }
+}
+
+impl PartialEq for Tint {
+ fn eq(&self, other: &Self) -> bool {
+ self.0.to_bits() == other.0.to_bits()
+ }
+}
+
+impl Eq for Tint {}
+
+impl Hash for Tint {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.0.to_bits().hash(state);
+ }
+}
+
/// A wrapper for storing colors from different color spaces.
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub enum Color {
@@ -160,7 +202,10 @@ impl RegularColor {
pub(crate) fn as_rgb(self) -> Option<rgb::Color> {
Some(match self {
Self::Rgb(r) => r,
- Self::Luma(l) => rgb::Color::new(l.0, l.0, l.0),
+ Self::Luma(l) => {
+ let v = l.0.to_u8();
+ rgb::Color::new(v, v, v)
+ }
Self::Cmyk(_) => return None,
})
}
@@ -183,16 +228,24 @@ impl SpecialColor {
/// Gray-scale colors.
pub mod luma {
- use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace};
+ use crate::color::{
+ CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace, Tint,
+ };
/// A luma color.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
- pub struct Color(pub(crate) u8);
+ pub struct Color(pub(crate) Tint);
impl Color {
/// Create a new luma color.
pub fn new(lightness: u8) -> Color {
- Color(lightness)
+ Color(Tint::from_u8(lightness))
+ }
+
+ /// Create a luma color from a lightness from 0 (black) to 1 (white), written to the
+ /// PDF without rounding. Values outside 0 to 1 are clamped, and NaN is 0.
+ pub fn new_f32(lightness: f32) -> Color {
+ Color(Tint::new(lightness))
}
/// Create a black luma color.
@@ -206,7 +259,7 @@ pub mod luma {
}
pub(crate) fn to_pdf_color(self) -> f32 {
- self.0 as f32 / 255.0
+ self.0.get()
}
}
@@ -251,27 +304,43 @@ impl From<SpecialColor> for Color {
/// CMYK colors.
pub mod cmyk {
- use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace};
+ use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace, Tint};
use crate::graphics::icc::ICCBasedColorSpace;
use crate::SerializeSettings;
/// A CMYK color.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
- pub struct Color(pub(crate) u8, pub(crate) u8, pub(crate) u8, pub(crate) u8);
+ pub struct Color(
+ pub(crate) Tint,
+ pub(crate) Tint,
+ pub(crate) Tint,
+ pub(crate) Tint,
+ );
impl Color {
/// Create a new CMYK color.
pub fn new(cyan: u8, magenta: u8, yellow: u8, black: u8) -> Color {
- Color(cyan, magenta, yellow, black)
+ Color(
+ Tint::from_u8(cyan),
+ Tint::from_u8(magenta),
+ Tint::from_u8(yellow),
+ Tint::from_u8(black),
+ )
+ }
+
+ /// Create a CMYK color from inks from 0 to 1, written to the PDF without rounding.
+ /// Values outside 0 to 1 are clamped, and NaN is 0.
+ pub fn new_f32(cyan: f32, magenta: f32, yellow: f32, black: f32) -> Color {
+ Color(
+ Tint::new(cyan),
+ Tint::new(magenta),
+ Tint::new(yellow),
+ Tint::new(black),
+ )
}
pub(crate) fn to_pdf_color(self) -> [f32; 4] {
- [
- self.0 as f32 / 255.0,
- self.1 as f32 / 255.0,
- self.2 as f32 / 255.0,
- self.3 as f32 / 255.0,
- ]
+ [self.0.get(), self.1.get(), self.2.get(), self.3.get()]
}
}
@@ -390,23 +459,35 @@ pub mod rgb {
/// Separation (spot) colors.
pub mod separation {
- use crate::color::RegularColor;
+ use crate::color::{RegularColor, Tint};
/// A spot color.
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub struct Color {
- pub(crate) tint: u8,
+ pub(crate) tint: Tint,
pub(crate) space: SeparationSpace,
}
impl Color {
/// Create a new spot color.
pub fn new(tint: u8, space: SeparationSpace) -> Self {
- Self { tint, space }
+ Self {
+ tint: Tint::from_u8(tint),
+ space,
+ }
+ }
+
+ /// Create a spot color with a tint from 0 to 1, written to the PDF without rounding.
+ /// A tint outside 0 to 1 is clamped, and NaN is 0.
+ pub fn new_f32(tint: f32, space: SeparationSpace) -> Self {
+ Self {
+ tint: Tint::new(tint),
+ space,
+ }
}
pub(crate) fn to_pdf_color(&self) -> f32 {
- self.tint as f32 / 255.0
+ self.tint.get()
}
pub(crate) fn color_space(&self) -> SeparationSpace {
@@ -498,7 +579,7 @@ pub mod separation {
pub mod device_n {
use std::sync::Arc;
- use crate::color::cmyk;
+ use crate::color::{cmyk, Tint};
/// A process colorant.
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
@@ -656,7 +737,7 @@ pub mod device_n {
/// A DeviceN color: one tint per colorant of its space.
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub struct Color {
- pub(crate) tints: Vec<u8>,
+ pub(crate) tints: Vec<Tint>,
pub(crate) space: DeviceNSpace,
}
@@ -666,20 +747,30 @@ pub mod device_n {
///
/// Returns `None` unless there is one tint per colorant.
pub fn new(tints: Vec<u8>, space: DeviceNSpace) -> Option<Self> {
+ Self::new_f32(
+ tints.into_iter().map(|t| Tint::from_u8(t).get()).collect(),
+ space,
+ )
+ }
+
+ /// Create a DeviceN color with tints from 0 (no ink) to 1 (full ink), in the order
+ /// of the space's colorants, written to the PDF without rounding. Tints outside 0
+ /// to 1 are clamped, and NaN is 0.
+ ///
+ /// Returns `None` unless there is one tint per colorant.
+ pub fn new_f32(tints: Vec<f32>, space: DeviceNSpace) -> Option<Self> {
+ let tints: Vec<Tint> = tints.into_iter().map(Tint::new).collect();
(tints.len() == space.colorants.len()).then_some(Self { tints, space })
}
pub(crate) fn to_pdf_color(&self) -> Vec<f32> {
- self.tints.iter().map(|t| *t as f32 / 255.0).collect()
+ self.tints.iter().map(|t| t.get()).collect()
}
/// The color in the alternate CMYK space.
pub(crate) fn alternate(&self) -> cmyk::Color {
- let [c, m, y, k] = self
- .space
- .alternate(&self.to_pdf_color())
- .map(|v| (v * 255.0).round().clamp(0.0, 255.0) as u8);
- cmyk::Color::new(c, m, y, k)
+ let [c, m, y, k] = self.space.alternate(&self.to_pdf_color());
+ cmyk::Color::new_f32(c, m, y, k)
}
}
diff --git a/src/graphics/shading_function.rs b/src/graphics/shading_function.rs
index 74c56f3..8aae81a 100644
--- a/src/graphics/shading_function.rs
+++ b/src/graphics/shading_function.rs
@@ -277,12 +277,12 @@ impl GradientProperties {
}
}
-/// The inks of one gradient color: process tints and spot tints (0–255).
+/// The inks of one gradient color: process tints and spot tints (0–1).
struct Inks {
- process: [u8; 4],
+ process: [f32; 4],
/// Process colorants a DeviceN color names, even at zero tint.
named_process: [bool; 4],
- spots: Vec<(String, u8)>,
+ spots: Vec<(String, f32)>,
}
/// A spot named by a gradient color: the Separation space a stop used, and the CMYK
@@ -353,13 +353,13 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
let mut all_inks = Vec::with_capacity(colors.len());
for color in colors {
let mut inks = Inks {
- process: [0; 4],
+ process: [0.0; 4],
named_process: [false; 4],
spots: Vec::new(),
};
match color {
- Color::Regular(RegularColor::Cmyk(c)) => inks.process = [c.0, c.1, c.2, c.3],
- Color::Regular(RegularColor::Luma(l)) => inks.process[3] = 255 - l.0,
+ Color::Regular(RegularColor::Cmyk(c)) => inks.process = c.to_pdf_color(),
+ Color::Regular(RegularColor::Luma(l)) => inks.process[3] = 1.0 - l.to_pdf_color(),
Color::Regular(RegularColor::Rgb(_)) => return None,
Color::Special(SpecialColor::Separation(s)) => {
let SeparationColorant::Custom(name) = &s.space.colorant else {
@@ -370,19 +370,19 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
_ => None,
};
add_spot(&mut spot_inks, name, Some(&s.space), alternate);
- inks.spots.push((name.clone(), s.tint));
+ inks.spots.push((name.clone(), s.tint.get()));
}
Color::Special(SpecialColor::DeviceN(c)) => {
for (colorant, tint) in c.space.colorants().iter().zip(&c.tints) {
match colorant {
Colorant::Process(p) => {
let i = Process::ALL.iter().position(|q| q == p).unwrap_or(0);
- inks.process[i] = *tint;
+ inks.process[i] = tint.get();
inks.named_process[i] = true;
}
Colorant::Spot { name, alternate } => {
add_spot(&mut spot_inks, name, None, Some(*alternate));
- inks.spots.push((name.clone(), *tint));
+ inks.spots.push((name.clone(), tint.get()));
}
}
}
@@ -395,14 +395,14 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
.filter(|&i| {
all_inks
.iter()
- .any(|inks| inks.process[i] > 0 || inks.named_process[i])
+ .any(|inks| inks.process[i] > 0.0 || inks.named_process[i])
})
.collect();
let spot_tint = |inks: &Inks, name: &str| {
inks.spots
.iter()
.find(|(n, _)| n == name)
- .map_or(0, |(_, t)| *t)
+ .map_or(0.0, |(_, t)| *t)
};
match (process.as_slice(), spot_inks.as_slice()) {
@@ -411,7 +411,7 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
.iter()
.map(|inks| {
let [c, m, y, k] = inks.process;
- cmyk::Color::new(c, m, y, k).into()
+ cmyk::Color::new_f32(c, m, y, k).into()
})
.collect(),
),
@@ -428,7 +428,8 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
all_inks
.iter()
.map(|inks| {
- separation::Color::new(spot_tint(inks, &spot.name), space.clone()).into()
+ separation::Color::new_f32(spot_tint(inks, &spot.name), space.clone())
+ .into()
})
.collect(),
)
@@ -448,9 +449,9 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
all_inks
.iter()
.map(|inks| {
- let mut tints: Vec<u8> = process.iter().map(|&i| inks.process[i]).collect();
+ let mut tints: Vec<f32> = process.iter().map(|&i| inks.process[i]).collect();
tints.extend(spot_inks.iter().map(|s| spot_tint(inks, &s.name)));
- device_n::Color::new(tints, space.clone()).map(Color::from)
+ device_n::Color::new_f32(tints, space.clone()).map(Color::from)
})
.collect()
}
@@ -547,7 +548,7 @@ fn shading_color_space(sc: &mut SerializeContext, color: Color, use_opacities: b
/// A mesh gradient as a Type 6 (Coons patch mesh) shading: per patch an 8-bit edge flag (0, no
/// shared edge), the twelve points as 32-bit coordinates over the mesh's bounds (`Decode`), and
-/// the four corner colors (with `use_opacities`, the opacities as gray) at 8 bits a component,
+/// the four corner colors (with `use_opacities`, the opacities as gray) at 16 bits a component,
/// Flate-compressed. 32-bit coordinates keep the patches where they are to well under a printer
/// dot, as Illustrator writes its meshes.
fn serialize_mesh_shading(
@@ -584,8 +585,8 @@ fn serialize_mesh_shading(
* f64::from(u32::MAX))
.round() as u32
};
- let byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
- let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * components));
+ let component = |v: f32| ((v.clamp(0.0, 1.0) * 65535.0).round() as u16).to_be_bytes();
+ let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * 2 * components));
for patch in &mesh.patches {
data.push(0u8);
for (x, y) in &patch.points {
@@ -594,12 +595,12 @@ fn serialize_mesh_shading(
}
for (color, opacity) in patch.colors.iter().zip(patch.opacities.iter()) {
if use_opacities {
- data.push(byte(opacity.get()));
+ data.extend(component(opacity.get()));
} else {
let values = color.to_pdf_color();
// Mixed spaces fail the document (`mixed_mesh_colors`); still never misread bytes.
for i in 0..components {
- data.push(byte(values.get(i).copied().unwrap_or(0.0)));
+ data.extend(component(values.get(i).copied().unwrap_or(0.0)));
}
}
}
@@ -611,7 +612,7 @@ fn serialize_mesh_shading(
shading.shading_type(StreamShadingType::CoonsPatch);
set_colorspace(cs, shading.deref_mut().deref_mut());
shading.bits_per_coordinate(32);
- shading.bits_per_component(8);
+ shading.bits_per_component(16);
shading.bits_per_flag(8);
let mut decode = vec![x0, x1, y0, y1];
for _ in 0..components {
+118 -27
View File
@@ -69,7 +69,7 @@
//! [`SerializeSettings`]: crate::SerializeSettings
use std::fmt::Debug;
use std::hash::Hash;
use std::hash::{Hash, Hasher};
use crate::configure::ValidationError;
use crate::graphics::icc::ICCBasedColorSpace;
@@ -82,6 +82,48 @@ pub(crate) const DEVICE_GRAY: &str = "DeviceGray";
/// The PDF name for the device CMYK color space.
pub(crate) const DEVICE_CMYK: &str = "DeviceCMYK";
/// A color component from 0 to 1: a gray level, a process ink or a spot tint.
///
/// `new` clamps the value to 0 to 1 and turns NaN and -0 into 0, so equal components have equal
/// bits; `Eq` and `Hash` compare the bits.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Tint(f32);
impl Tint {
pub(crate) fn new(value: f32) -> Self {
// `max` returns the other operand for NaN; adding 0 turns -0 into 0.
Tint(value.max(0.0).min(1.0) + 0.0)
}
/// The component `value / 255`, as the 8-bit constructors give it.
pub(crate) fn from_u8(value: u8) -> Self {
Tint(f32::from(value) / 255.0)
}
pub(crate) fn get(self) -> f32 {
self.0
}
/// The nearest 8-bit value.
pub(crate) fn to_u8(self) -> u8 {
(self.0 * 255.0).round() as u8
}
}
impl PartialEq for Tint {
fn eq(&self, other: &Self) -> bool {
self.0.to_bits() == other.0.to_bits()
}
}
impl Eq for Tint {}
impl Hash for Tint {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
/// A wrapper for storing colors from different color spaces.
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub enum Color {
@@ -160,7 +202,10 @@ impl RegularColor {
pub(crate) fn as_rgb(self) -> Option<rgb::Color> {
Some(match self {
Self::Rgb(r) => r,
Self::Luma(l) => rgb::Color::new(l.0, l.0, l.0),
Self::Luma(l) => {
let v = l.0.to_u8();
rgb::Color::new(v, v, v)
}
Self::Cmyk(_) => return None,
})
}
@@ -183,16 +228,24 @@ impl SpecialColor {
/// Gray-scale colors.
pub mod luma {
use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace};
use crate::color::{
CieBasedColorSpace, DeviceColorSpace, RegularColor, RegularColorSpace, Tint,
};
/// A luma color.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
pub struct Color(pub(crate) u8);
pub struct Color(pub(crate) Tint);
impl Color {
/// Create a new luma color.
pub fn new(lightness: u8) -> Color {
Color(lightness)
Color(Tint::from_u8(lightness))
}
/// Create a luma color from a lightness from 0 (black) to 1 (white), written to the
/// PDF without rounding. Values outside 0 to 1 are clamped, and NaN is 0.
pub fn new_f32(lightness: f32) -> Color {
Color(Tint::new(lightness))
}
/// Create a black luma color.
@@ -206,7 +259,7 @@ pub mod luma {
}
pub(crate) fn to_pdf_color(self) -> f32 {
self.0 as f32 / 255.0
self.0.get()
}
}
@@ -251,27 +304,43 @@ impl From<SpecialColor> for Color {
/// CMYK colors.
pub mod cmyk {
use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace};
use crate::color::{CieBasedColorSpace, DeviceColorSpace, RegularColorSpace, Tint};
use crate::graphics::icc::ICCBasedColorSpace;
use crate::SerializeSettings;
/// A CMYK color.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy)]
pub struct Color(pub(crate) u8, pub(crate) u8, pub(crate) u8, pub(crate) u8);
pub struct Color(
pub(crate) Tint,
pub(crate) Tint,
pub(crate) Tint,
pub(crate) Tint,
);
impl Color {
/// Create a new CMYK color.
pub fn new(cyan: u8, magenta: u8, yellow: u8, black: u8) -> Color {
Color(cyan, magenta, yellow, black)
Color(
Tint::from_u8(cyan),
Tint::from_u8(magenta),
Tint::from_u8(yellow),
Tint::from_u8(black),
)
}
/// Create a CMYK color from inks from 0 to 1, written to the PDF without rounding.
/// Values outside 0 to 1 are clamped, and NaN is 0.
pub fn new_f32(cyan: f32, magenta: f32, yellow: f32, black: f32) -> Color {
Color(
Tint::new(cyan),
Tint::new(magenta),
Tint::new(yellow),
Tint::new(black),
)
}
pub(crate) fn to_pdf_color(self) -> [f32; 4] {
[
self.0 as f32 / 255.0,
self.1 as f32 / 255.0,
self.2 as f32 / 255.0,
self.3 as f32 / 255.0,
]
[self.0.get(), self.1.get(), self.2.get(), self.3.get()]
}
}
@@ -390,23 +459,35 @@ pub mod rgb {
/// Separation (spot) colors.
pub mod separation {
use crate::color::RegularColor;
use crate::color::{RegularColor, Tint};
/// A spot color.
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub struct Color {
pub(crate) tint: u8,
pub(crate) tint: Tint,
pub(crate) space: SeparationSpace,
}
impl Color {
/// Create a new spot color.
pub fn new(tint: u8, space: SeparationSpace) -> Self {
Self { tint, space }
Self {
tint: Tint::from_u8(tint),
space,
}
}
/// Create a spot color with a tint from 0 to 1, written to the PDF without rounding.
/// A tint outside 0 to 1 is clamped, and NaN is 0.
pub fn new_f32(tint: f32, space: SeparationSpace) -> Self {
Self {
tint: Tint::new(tint),
space,
}
}
pub(crate) fn to_pdf_color(&self) -> f32 {
self.tint as f32 / 255.0
self.tint.get()
}
pub(crate) fn color_space(&self) -> SeparationSpace {
@@ -498,7 +579,7 @@ pub mod separation {
pub mod device_n {
use std::sync::Arc;
use crate::color::cmyk;
use crate::color::{cmyk, Tint};
/// A process colorant.
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
@@ -656,7 +737,7 @@ pub mod device_n {
/// A DeviceN color: one tint per colorant of its space.
#[derive(Debug, Hash, Eq, PartialEq, Clone)]
pub struct Color {
pub(crate) tints: Vec<u8>,
pub(crate) tints: Vec<Tint>,
pub(crate) space: DeviceNSpace,
}
@@ -666,20 +747,30 @@ pub mod device_n {
///
/// Returns `None` unless there is one tint per colorant.
pub fn new(tints: Vec<u8>, space: DeviceNSpace) -> Option<Self> {
Self::new_f32(
tints.into_iter().map(|t| Tint::from_u8(t).get()).collect(),
space,
)
}
/// Create a DeviceN color with tints from 0 (no ink) to 1 (full ink), in the order
/// of the space's colorants, written to the PDF without rounding. Tints outside 0
/// to 1 are clamped, and NaN is 0.
///
/// Returns `None` unless there is one tint per colorant.
pub fn new_f32(tints: Vec<f32>, space: DeviceNSpace) -> Option<Self> {
let tints: Vec<Tint> = tints.into_iter().map(Tint::new).collect();
(tints.len() == space.colorants.len()).then_some(Self { tints, space })
}
pub(crate) fn to_pdf_color(&self) -> Vec<f32> {
self.tints.iter().map(|t| *t as f32 / 255.0).collect()
self.tints.iter().map(|t| t.get()).collect()
}
/// The color in the alternate CMYK space.
pub(crate) fn alternate(&self) -> cmyk::Color {
let [c, m, y, k] = self
.space
.alternate(&self.to_pdf_color())
.map(|v| (v * 255.0).round().clamp(0.0, 255.0) as u8);
cmyk::Color::new(c, m, y, k)
let [c, m, y, k] = self.space.alternate(&self.to_pdf_color());
cmyk::Color::new_f32(c, m, y, k)
}
}
+22 -21
View File
@@ -277,12 +277,12 @@ impl GradientProperties {
}
}
/// The inks of one gradient color: process tints and spot tints (0–255).
/// The inks of one gradient color: process tints and spot tints (0–1).
struct Inks {
process: [u8; 4],
process: [f32; 4],
/// Process colorants a DeviceN color names, even at zero tint.
named_process: [bool; 4],
spots: Vec<(String, u8)>,
spots: Vec<(String, f32)>,
}
/// A spot named by a gradient color: the Separation space a stop used, and the CMYK
@@ -353,13 +353,13 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
let mut all_inks = Vec::with_capacity(colors.len());
for color in colors {
let mut inks = Inks {
process: [0; 4],
process: [0.0; 4],
named_process: [false; 4],
spots: Vec::new(),
};
match color {
Color::Regular(RegularColor::Cmyk(c)) => inks.process = [c.0, c.1, c.2, c.3],
Color::Regular(RegularColor::Luma(l)) => inks.process[3] = 255 - l.0,
Color::Regular(RegularColor::Cmyk(c)) => inks.process = c.to_pdf_color(),
Color::Regular(RegularColor::Luma(l)) => inks.process[3] = 1.0 - l.to_pdf_color(),
Color::Regular(RegularColor::Rgb(_)) => return None,
Color::Special(SpecialColor::Separation(s)) => {
let SeparationColorant::Custom(name) = &s.space.colorant else {
@@ -370,19 +370,19 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
_ => None,
};
add_spot(&mut spot_inks, name, Some(&s.space), alternate);
inks.spots.push((name.clone(), s.tint));
inks.spots.push((name.clone(), s.tint.get()));
}
Color::Special(SpecialColor::DeviceN(c)) => {
for (colorant, tint) in c.space.colorants().iter().zip(&c.tints) {
match colorant {
Colorant::Process(p) => {
let i = Process::ALL.iter().position(|q| q == p).unwrap_or(0);
inks.process[i] = *tint;
inks.process[i] = tint.get();
inks.named_process[i] = true;
}
Colorant::Spot { name, alternate } => {
add_spot(&mut spot_inks, name, None, Some(*alternate));
inks.spots.push((name.clone(), *tint));
inks.spots.push((name.clone(), tint.get()));
}
}
}
@@ -395,14 +395,14 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
.filter(|&i| {
all_inks
.iter()
.any(|inks| inks.process[i] > 0 || inks.named_process[i])
.any(|inks| inks.process[i] > 0.0 || inks.named_process[i])
})
.collect();
let spot_tint = |inks: &Inks, name: &str| {
inks.spots
.iter()
.find(|(n, _)| n == name)
.map_or(0, |(_, t)| *t)
.map_or(0.0, |(_, t)| *t)
};
match (process.as_slice(), spot_inks.as_slice()) {
@@ -411,7 +411,7 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
.iter()
.map(|inks| {
let [c, m, y, k] = inks.process;
cmyk::Color::new(c, m, y, k).into()
cmyk::Color::new_f32(c, m, y, k).into()
})
.collect(),
),
@@ -428,7 +428,8 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
all_inks
.iter()
.map(|inks| {
separation::Color::new(spot_tint(inks, &spot.name), space.clone()).into()
separation::Color::new_f32(spot_tint(inks, &spot.name), space.clone())
.into()
})
.collect(),
)
@@ -448,9 +449,9 @@ pub(crate) fn common_color_space(colors: &[Color]) -> Option<Vec<Color>> {
all_inks
.iter()
.map(|inks| {
let mut tints: Vec<u8> = process.iter().map(|&i| inks.process[i]).collect();
let mut tints: Vec<f32> = process.iter().map(|&i| inks.process[i]).collect();
tints.extend(spot_inks.iter().map(|s| spot_tint(inks, &s.name)));
device_n::Color::new(tints, space.clone()).map(Color::from)
device_n::Color::new_f32(tints, space.clone()).map(Color::from)
})
.collect()
}
@@ -547,7 +548,7 @@ fn shading_color_space(sc: &mut SerializeContext, color: Color, use_opacities: b
/// A mesh gradient as a Type 6 (Coons patch mesh) shading: per patch an 8-bit edge flag (0, no
/// shared edge), the twelve points as 32-bit coordinates over the mesh's bounds (`Decode`), and
/// the four corner colors (with `use_opacities`, the opacities as gray) at 8 bits a component,
/// the four corner colors (with `use_opacities`, the opacities as gray) at 16 bits a component,
/// Flate-compressed. 32-bit coordinates keep the patches where they are to well under a printer
/// dot, as Illustrator writes its meshes.
fn serialize_mesh_shading(
@@ -584,8 +585,8 @@ fn serialize_mesh_shading(
* f64::from(u32::MAX))
.round() as u32
};
let byte = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * components));
let component = |v: f32| ((v.clamp(0.0, 1.0) * 65535.0).round() as u16).to_be_bytes();
let mut data = Vec::with_capacity(mesh.patches.len() * (1 + 12 * 8 + 4 * 2 * components));
for patch in &mesh.patches {
data.push(0u8);
for (x, y) in &patch.points {
@@ -594,12 +595,12 @@ fn serialize_mesh_shading(
}
for (color, opacity) in patch.colors.iter().zip(patch.opacities.iter()) {
if use_opacities {
data.push(byte(opacity.get()));
data.extend(component(opacity.get()));
} else {
let values = color.to_pdf_color();
// Mixed spaces fail the document (`mixed_mesh_colors`); still never misread bytes.
for i in 0..components {
data.push(byte(values.get(i).copied().unwrap_or(0.0)));
data.extend(component(values.get(i).copied().unwrap_or(0.0)));
}
}
}
@@ -611,7 +612,7 @@ fn serialize_mesh_shading(
shading.shading_type(StreamShadingType::CoonsPatch);
set_colorspace(cs, shading.deref_mut().deref_mut());
shading.bits_per_coordinate(32);
shading.bits_per_component(8);
shading.bits_per_component(16);
shading.bits_per_flag(8);
let mut decode = vec![x0, x1, y0, y1];
for _ in 0..components {