Skip to main content

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}