Skip to main content

pdf_engine/
color.rs

1//! Color helpers for render output conversion.
2
3// Copyright (c) 2026 Innovation Trigger B.V.
4//
5// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
6// the PDFluent Commercial Licence. See the LICENSE file in this repository --
7// that file travels with the copy you received, which a URL does not.
8
9/// Preserve the original DeviceCMYK components as 8-bit output.
10pub fn preserve_device_cmyk(c: f32, m: f32, y: f32, k: f32) -> [u8; 4] {
11    [
12        (c.clamp(0.0, 1.0) * 255.0) as u8,
13        (m.clamp(0.0, 1.0) * 255.0) as u8,
14        (y.clamp(0.0, 1.0) * 255.0) as u8,
15        (k.clamp(0.0, 1.0) * 255.0) as u8,
16    ]
17}
18
19pub(crate) fn rgba_to_cmyk_pixel(rgba: &[u8]) -> [u8; 4] {
20    let alpha = rgba[3] as f32 / 255.0;
21    let bg = 1.0 - alpha;
22    let r = (rgba[0] as f32 / 255.0) * alpha + bg;
23    let g = (rgba[1] as f32 / 255.0) * alpha + bg;
24    let b = (rgba[2] as f32 / 255.0) * alpha + bg;
25    let k = 1.0 - r.max(g).max(b);
26    let (c, m, y) = if k >= 1.0 - f32::EPSILON {
27        (0.0_f32, 0.0_f32, 0.0_f32)
28    } else {
29        let inv = 1.0 - k;
30        ((inv - r) / inv, (inv - g) / inv, (inv - b) / inv)
31    };
32
33    [
34        (c.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
35        (m.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
36        (y.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
37        (k.clamp(0.0, 1.0) * 255.0 + 0.5) as u8,
38    ]
39}
40
41pub(crate) fn rgba_to_cmyk_buffer(rgba: &[u8]) -> Vec<u8> {
42    let mut out = Vec::with_capacity(rgba.len());
43    for px in rgba.chunks_exact(4) {
44        out.extend_from_slice(&rgba_to_cmyk_pixel(px));
45    }
46    out
47}
48
49pub(crate) fn blend_cmyk(bottom: [u8; 4], top: [u8; 4], alpha: u8) -> [u8; 4] {
50    let alpha = alpha as f32 / 255.0;
51    let inv_alpha = 1.0 - alpha;
52    let mut out = [0; 4];
53    for (idx, component) in out.iter_mut().enumerate() {
54        let blended = bottom[idx] as f32 * inv_alpha + top[idx] as f32 * alpha;
55        *component = blended.clamp(0.0, 255.0).round() as u8;
56    }
57    out
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn preserve_device_cmyk_truncates_to_u8() {
66        assert_eq!(
67            preserve_device_cmyk(0.25, 0.5, 0.75, 1.0),
68            [63, 127, 191, 255]
69        );
70    }
71
72    #[test]
73    fn blend_cmyk_half_alpha() {
74        assert_eq!(
75            blend_cmyk([0, 0, 0, 0], [255, 128, 64, 32], 128),
76            [128, 64, 32, 16]
77        );
78    }
79}