rasterrocket_color/transfer.rs
1//! Transfer function lookup tables for PDF rendering.
2//!
3//! A *transfer function* in PDF maps a device-colour value in \[0, 1\] to an
4//! adjusted output value, allowing calibration curves and halftone screen
5//! corrections. Inside this crate the function is pre-sampled to a 256-entry
6//! `u8→u8` lookup table (`TransferLut`): entry `i` holds the output byte for
7//! input byte `i`.
8//!
9//! ## Identity and non-identity LUTs
10//!
11//! The identity LUT (`TransferLut::IDENTITY`) maps every value to itself and is
12//! the correct default when no PDF transfer function is in effect. Non-identity
13//! LUTs are built by sampling the PDF transfer function at the 256 points
14//! `i/255.0` for `i` in `0..=255` and converting the floating-point result back
15//! to a `u8`.
16//!
17//! ## PDF mapping
18//!
19//! PDF transfer functions work per-channel. The graphics state therefore keeps
20//! one `TransferLut` per channel (R, G, B, gray, and the four CMYK channels).
21//! The CMYK LUTs are derived from the RGB/gray LUT via
22//! [`TransferLut::invert_complement`], matching the logic in
23//! `SplashState::setTransfer`.
24//!
25//! ## Safety / infallibility
26//!
27//! Every public method is infallible. `apply` is a plain array index whose
28//! index is always a `u8`, so it is always in `0..=255`, which is exactly the
29//! length of the internal array. There are no panicking paths in normal use.
30//!
31//! ## Public inner field
32//!
33//! `TransferLut(pub [u8; 256])` exposes the raw array deliberately: PDF allows
34//! arbitrary 256-entry transfer tables, and code that samples a PDF function
35//! needs direct write access. See [`From<[u8; 256]>`](TransferLut#impl-From<[u8;+256]>-for-TransferLut)
36//! for ergonomic construction.
37
38/// A per-channel lookup table: `output[i] = lut[input[i]]`.
39///
40/// Always exactly 256 entries — one for every possible byte value. The type is
41/// a newtype so that callers cannot accidentally pass a raw `[u8; 256]` in the
42/// wrong argument position.
43///
44/// The inner field is `pub` intentionally: PDF transfer functions are arbitrary
45/// 256-entry tables, and the code that samples them needs direct write access.
46/// Prefer [`From<[u8; 256]>`] for construction from an owned array.
47#[derive(Clone, PartialEq, Eq)]
48pub struct TransferLut(pub [u8; 256]);
49
50// Compile-time assertion: the inner array must be exactly 256 bytes.
51// This guards against any future refactor that might change the constant.
52const _ASSERT_LUT_LEN: () = assert!(
53 std::mem::size_of::<TransferLut>() == 256,
54 "TransferLut must contain exactly 256 bytes"
55);
56
57impl TransferLut {
58 /// The identity mapping: every value `i` maps to `i`.
59 ///
60 /// This is the correct default when no PDF transfer function is active.
61 /// All 256 entries are initialised at compile time via a `const` loop using
62 /// a `u8` counter so that the index-to-byte cast is lossless by type.
63 pub const IDENTITY: Self = {
64 let mut t = [0u8; 256];
65 // A u8 loop variable guarantees the index value fits in a byte without
66 // any arithmetic: `i as usize` is always in 0..=255.
67 let mut i = 0u8;
68 loop {
69 t[i as usize] = i;
70 if i == 255 {
71 break;
72 }
73 i += 1;
74 }
75 Self(t)
76 };
77
78 /// The per-byte inverting mapping: every value `i` maps to `255 - i`.
79 ///
80 /// Unlike [`invert_complement`](Self::invert_complement) (which composes an
81 /// existing LUT with the complement), this is the standalone "negate" LUT —
82 /// suitable for test fixtures that need a non-identity transfer where the
83 /// output is easy to assert against. Same `u8` loop-counter rationale as
84 /// [`IDENTITY`](Self::IDENTITY): index-to-byte cast is lossless by type.
85 pub const INVERTED: Self = {
86 let mut t = [0u8; 256];
87 let mut i = 0u8;
88 loop {
89 t[i as usize] = 255 - i;
90 if i == 255 {
91 break;
92 }
93 i += 1;
94 }
95 Self(t)
96 };
97
98 /// Apply the LUT to a single byte.
99 ///
100 /// # Safety / infallibility
101 ///
102 /// `v` is a `u8`, so `v as usize` is always in `0..=255`. The inner array
103 /// is always 256 entries (enforced by the compile-time assertion
104 /// `_ASSERT_LUT_LEN`), so the index is always in bounds. This method
105 /// cannot panic.
106 #[inline]
107 #[must_use]
108 pub const fn apply(&self, v: u8) -> u8 {
109 self.0[v as usize]
110 }
111
112 /// Produce a new LUT where `output[i] = 255 - self[255 - i]`.
113 ///
114 /// Used by `GraphicsState::set_transfer` to derive the CMYK LUTs from the
115 /// RGB/gray LUTs, matching `SplashState::setTransfer` in SplashState.cc.
116 ///
117 /// # Index safety
118 ///
119 /// The closure index `i` comes from `0..256` (the length of the output
120 /// array), so `255 - i` is always in `0..=255` — always a valid index into
121 /// the 256-entry `self.0` array.
122 #[must_use]
123 pub fn invert_complement(&self) -> Self {
124 Self(std::array::from_fn(|i| 255 - self.0[255 - i]))
125 }
126
127 /// Return a reference to the raw 256-entry array.
128 ///
129 /// Useful for `memcpy`-style copies into an external state block.
130 #[must_use]
131 pub const fn as_array(&self) -> &[u8; 256] {
132 &self.0
133 }
134
135 /// Compose two LUTs: apply `self` first, then `other`.
136 ///
137 /// The returned LUT is equivalent to `other.apply(self.apply(v))` for every
138 /// input byte `v`. This is the natural piping / chaining operation for
139 /// transfer functions.
140 ///
141 /// # Examples
142 ///
143 /// ```
144 /// use color::TransferLut;
145 ///
146 /// // Build an inversion LUT: i → 255 - i.
147 /// let mut inv_table = [0u8; 256];
148 /// for i in 0usize..=255 {
149 /// inv_table[i] = (255 - i) as u8;
150 /// }
151 /// let inv = TransferLut::from(inv_table);
152 ///
153 /// // Composing inversion with itself gives the identity.
154 /// let composed = inv.compose(&inv);
155 /// assert_eq!(composed, TransferLut::IDENTITY);
156 ///
157 /// // Composing with identity leaves any LUT unchanged.
158 /// let id = TransferLut::IDENTITY;
159 /// assert_eq!(inv.compose(&id), inv);
160 /// assert_eq!(id.compose(&inv), inv);
161 /// ```
162 #[must_use]
163 pub fn compose(&self, other: &Self) -> Self {
164 Self(std::array::from_fn(|i| other.apply(self.0[i])))
165 }
166
167 /// Compose a slice of LUTs left-to-right, starting from the identity.
168 ///
169 /// `compose_many(&[a, b, c])` is equivalent to
170 /// `IDENTITY.compose(a).compose(b).compose(c)`. An empty slice returns
171 /// [`IDENTITY`](Self::IDENTITY).
172 #[must_use]
173 pub fn compose_many(luts: &[&Self]) -> Self {
174 luts.iter()
175 .fold(Self::IDENTITY, |acc, lut| acc.compose(lut))
176 }
177}
178
179impl Default for TransferLut {
180 fn default() -> Self {
181 Self::IDENTITY
182 }
183}
184
185impl std::fmt::Debug for TransferLut {
186 /// Compact representation showing the first two and last entry only,
187 /// avoiding a 256-element dump that would overwhelm debug output.
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 write!(
190 f,
191 "TransferLut([{}, {}, ..., {}])",
192 self.0[0], self.0[1], self.0[255]
193 )
194 }
195}
196
197impl From<[u8; 256]> for TransferLut {
198 /// Construct a `TransferLut` from a raw 256-entry array.
199 fn from(arr: [u8; 256]) -> Self {
200 Self(arr)
201 }
202}
203
204impl From<TransferLut> for [u8; 256] {
205 /// Unwrap a `TransferLut` back into a raw array (consuming).
206 fn from(lut: TransferLut) -> Self {
207 lut.0
208 }
209}
210
211impl AsRef<[u8]> for TransferLut {
212 /// View the LUT as a byte slice (length 256).
213 fn as_ref(&self) -> &[u8] {
214 &self.0
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 // ── IDENTITY ────────────────────────────────────────────────────────────
223
224 #[test]
225 fn identity_all_256_entries() {
226 let lut = TransferLut::IDENTITY;
227 for i in 0u8..=255 {
228 assert_eq!(lut.apply(i), i, "IDENTITY should map {i} to {i}");
229 }
230 }
231
232 // ── invert_complement ───────────────────────────────────────────────────
233
234 #[test]
235 fn invert_complement_of_identity_is_identity() {
236 let lut = TransferLut::IDENTITY;
237 let inv = lut.invert_complement();
238 // 255 - (255 - i) = i for all i
239 for i in 0u8..=255 {
240 assert_eq!(
241 inv.apply(i),
242 i,
243 "invert_complement(identity)[{i}] should be {i}"
244 );
245 }
246 }
247
248 #[test]
249 fn invert_complement_nontrivial() {
250 // Build a LUT that maps i → 255 - i (inversion).
251 let lut = TransferLut(std::array::from_fn(|i| {
252 u8::try_from(255 - i).expect("i < 256")
253 }));
254 let inv = lut.invert_complement();
255 // inv[i] = 255 - lut[255 - i] = 255 - (255 - (255 - i)) = 255 - i
256 for i in 0u8..=255 {
257 assert_eq!(inv.apply(i), 255 - i);
258 }
259 }
260
261 // ── compose ─────────────────────────────────────────────────────────────
262
263 #[test]
264 fn compose_identity_is_neutral() {
265 let id = TransferLut::IDENTITY;
266 let lut = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
267 u8::try_from((i * 2) % 256).expect("(i*2)%256 < 256")
268 }));
269 assert_eq!(lut.compose(&id), lut, "lut ∘ id should equal lut");
270 assert_eq!(id.compose(&lut), lut, "id ∘ lut should equal lut");
271 }
272
273 #[test]
274 fn compose_inversion_twice_is_identity() {
275 let inv = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
276 u8::try_from(255 - i).expect("i < 256")
277 }));
278 let roundtrip = inv.compose(&inv);
279 assert_eq!(roundtrip, TransferLut::IDENTITY);
280 }
281
282 #[test]
283 fn compose_associativity() {
284 let double = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
285 u8::try_from((i * 2) % 256).expect("(i*2)%256 < 256")
286 }));
287 let inv = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
288 u8::try_from(255 - i).expect("i < 256")
289 }));
290 let half = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
291 u8::try_from(i / 2).expect("i/2 < 256")
292 }));
293 // (double ∘ inv) ∘ half == double ∘ (inv ∘ half)
294 let left = double.compose(&inv).compose(&half);
295 let right = double.compose(&inv.compose(&half));
296 assert_eq!(left, right);
297 }
298
299 // ── compose_many ────────────────────────────────────────────────────────
300
301 #[test]
302 fn compose_many_empty_is_identity() {
303 let result = TransferLut::compose_many(&[]);
304 assert_eq!(result, TransferLut::IDENTITY);
305 }
306
307 #[test]
308 fn compose_many_single() {
309 let inv = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
310 u8::try_from(255 - i).expect("i < 256")
311 }));
312 let result = TransferLut::compose_many(&[&inv]);
313 assert_eq!(result, inv);
314 }
315
316 #[test]
317 fn compose_many_matches_sequential_compose() {
318 let a = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
319 u8::try_from((i * 3) % 256).expect("(i*3)%256 < 256")
320 }));
321 let b = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
322 u8::try_from(255 - i).expect("i < 256")
323 }));
324 let c = TransferLut::from(std::array::from_fn::<u8, 256, _>(|i| {
325 u8::try_from(i / 2).expect("i/2 < 256")
326 }));
327 let expected = a.compose(&b).compose(&c);
328 let got = TransferLut::compose_many(&[&a, &b, &c]);
329 assert_eq!(got, expected);
330 }
331
332 // ── From / AsRef conversions ─────────────────────────────────────────────
333
334 #[test]
335 fn from_array_roundtrip() {
336 let arr =
337 std::array::from_fn::<u8, 256, _>(|i| u8::try_from(i ^ 0xAA).expect("i^0xAA < 256"));
338 let lut = TransferLut::from(arr);
339 let arr2: [u8; 256] = lut.into();
340 assert_eq!(arr, arr2);
341 }
342
343 #[test]
344 fn as_ref_length_and_content() {
345 let lut = TransferLut::IDENTITY;
346 let slice: &[u8] = lut.as_ref();
347 assert_eq!(slice.len(), 256);
348 for (i, &b) in slice.iter().enumerate() {
349 assert_eq!(b, u8::try_from(i).expect("IDENTITY has 256 entries"));
350 }
351 }
352
353 // ── Debug ────────────────────────────────────────────────────────────────
354
355 #[test]
356 fn debug_format_does_not_dump_256_entries() {
357 let s = format!("{:?}", TransferLut::IDENTITY);
358 // Should be short — just a compact summary, not 256 comma-separated values.
359 assert!(s.len() < 80, "Debug output unexpectedly long: {s}");
360 assert!(s.contains("TransferLut"));
361 }
362}