Skip to main content

pdfrum_edit/image/
raw.rs

1//! Raw interleaved samples as an image `XObject`, with the alpha channel
2//! split off into an `/SMask`.
3
4use pdfrum_object::{Array, ByteSpan, Dict, Object, Stream};
5
6use super::EmbeddedImage;
7use super::jpeg::{device_space, image_dict};
8use crate::doc::EditDoc;
9use crate::error::Error;
10use crate::names;
11
12/// How the bytes handed to [`crate::EditDoc::embed_image`] are laid out.
13///
14/// The oracle takes a bitmap object and reads the format back off it; here
15/// the caller states it, because what arrives is loose bytes rather than a
16/// bitmap that knows its own shape.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum PixelFormat {
19    /// One eight-bit grey sample per pixel, `/DeviceGray`.
20    Gray8,
21    /// Three eight-bit samples per pixel, `/DeviceRGB`.
22    Rgb8,
23    /// Four eight-bit samples per pixel, `/DeviceCMYK`.
24    Cmyk8,
25    /// Four eight-bit samples per pixel; the fourth becomes a `/DeviceGray`
26    /// `/SMask` and the first three the image's own `/DeviceRGB` samples.
27    Rgba8,
28    /// One bit per pixel, rows padded to a byte, written as an `/ImageMask`.
29    ///
30    /// A set bit paints, which is the sense `/Decode [1 0]` gives a stencil
31    /// mask; see [`crate::EditDoc::embed_image`].
32    Mask1,
33}
34
35impl PixelFormat {
36    /// Bytes one row of `width` pixels occupies.
37    fn row_bytes(self, width: u32) -> Option<usize> {
38        let width = usize::try_from(width).ok()?;
39        match self {
40            Self::Mask1 => width.checked_add(7).map(|w| w / 8),
41            Self::Gray8 => Some(width),
42            Self::Rgb8 => width.checked_mul(3),
43            Self::Cmyk8 | Self::Rgba8 => width.checked_mul(4),
44        }
45    }
46
47    /// The `/ColorSpace` components the *image* stream carries, which is one
48    /// fewer than [`Self::Rgba8`] is handed.
49    fn stored_components(self) -> u8 {
50        match self {
51            Self::Mask1 | Self::Gray8 => 1,
52            Self::Rgb8 | Self::Rgba8 => 3,
53            Self::Cmyk8 => 4,
54        }
55    }
56}
57
58pub(super) fn embed(
59    doc: &mut EditDoc<'_>,
60    pixels: &[u8],
61    width: u32,
62    height: u32,
63    format: PixelFormat,
64) -> Result<EmbeddedImage, Error> {
65    // `CPDF_Image::SetImage` returns without touching the stream when either
66    // dimension is below one (`cpdf_image.cpp:186-189`); with no stream to
67    // hand back, an error is the same answer a caller can act on.
68    if width == 0 || height == 0 {
69        return Err(Error::EmptyImage);
70    }
71    let row = format.row_bytes(width).ok_or(Error::EmptyImage)?;
72    let expected = usize::try_from(height)
73        .ok()
74        .and_then(|h| row.checked_mul(h))
75        .ok_or(Error::EmptyImage)?;
76    if pixels.len() != expected {
77        return Err(Error::ImageDataLength {
78            expected,
79            found: pixels.len(),
80        });
81    }
82
83    let mut dict = image_dict(width, height);
84    let data = match format {
85        // `:196-220`: a one-bit bitmap whose palette has a transparent entry
86        // becomes `/ImageMask true`, and the `reset` colour being the
87        // transparent one is what inverts `/Decode`. A caller handing us a
88        // mask has said which sense they mean by choosing `Mask1`: a set bit
89        // paints, so the sample value 1 must map to 0 — the "paint" end of an
90        // `/ImageMask`'s range (ISO 32000-1 §8.9.6.2) — which is `[1 0]`.
91        PixelFormat::Mask1 => {
92            dict.push(names::IMAGE_MASK.clone(), Object::Bool(true));
93            dict.push(
94                names::DECODE.clone(),
95                Object::Array(Array::of([Object::Int(1), Object::Int(0)])),
96            );
97            dict.push(names::BITS_PER_COMPONENT.clone(), Object::Int(1));
98            pixels.to_vec()
99        }
100        PixelFormat::Rgba8 => {
101            let (colour, alpha) = split_alpha(pixels);
102            let smask = doc.add(Object::Stream(Box::new(Stream::new(
103                smask_dict(width, height),
104                ByteSpan::from(alpha),
105            ))));
106            push_space(&mut dict, format);
107            dict.push(names::SMASK.clone(), Object::Ref(smask));
108            colour
109        }
110        PixelFormat::Gray8 | PixelFormat::Rgb8 | PixelFormat::Cmyk8 => {
111            push_space(&mut dict, format);
112            pixels.to_vec()
113        }
114    };
115
116    // No `/Filter`: the stream writer flate-encodes any stream that declares
117    // none, which is the same place the subsetter leaves its font programs.
118    Ok(EmbeddedImage {
119        image: doc.add(Object::Stream(Box::new(Stream::new(
120            dict,
121            ByteSpan::from(data),
122        )))),
123        width,
124        height,
125    })
126}
127
128fn push_space(dict: &mut Dict, format: PixelFormat) {
129    if let Some(space) = device_space(format.stored_components()) {
130        dict.push(names::COLOR_SPACE.clone(), Object::Name(space));
131    }
132    dict.push(names::BITS_PER_COMPONENT.clone(), Object::Int(8));
133}
134
135/// The alpha image's dictionary: eight-bit `/DeviceGray` at the colour
136/// image's size.
137fn smask_dict(width: u32, height: u32) -> Dict {
138    let mut dict = image_dict(width, height);
139    dict.push(
140        names::COLOR_SPACE.clone(),
141        Object::Name(names::DEVICE_GRAY.clone()),
142    );
143    dict.push(names::BITS_PER_COMPONENT.clone(), Object::Int(8));
144    dict
145}
146
147/// Interleaved RGBA into an RGB image and its alpha plane.
148fn split_alpha(pixels: &[u8]) -> (Vec<u8>, Vec<u8>) {
149    let count = pixels.len() / 4;
150    let mut colour = Vec::with_capacity(count * 3);
151    let mut alpha = Vec::with_capacity(count);
152    for [red, green, blue, opacity] in pixels.as_chunks::<4>().0 {
153        colour.extend_from_slice(&[*red, *green, *blue]);
154        alpha.push(*opacity);
155    }
156    (colour, alpha)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::{PixelFormat, split_alpha};
162
163    #[test]
164    fn a_row_is_padded_to_a_byte_only_for_a_mask() {
165        assert_eq!(PixelFormat::Mask1.row_bytes(9), Some(2));
166        assert_eq!(PixelFormat::Gray8.row_bytes(9), Some(9));
167        assert_eq!(PixelFormat::Rgb8.row_bytes(9), Some(27));
168        assert_eq!(PixelFormat::Rgba8.row_bytes(9), Some(36));
169        assert_eq!(PixelFormat::Cmyk8.row_bytes(9), Some(36));
170    }
171
172    // The stored image is RGB even though the caller handed four channels.
173    #[test]
174    fn rgba_stores_three_components() {
175        assert_eq!(PixelFormat::Rgba8.stored_components(), 3);
176    }
177
178    #[test]
179    fn the_alpha_channel_leaves_the_colour_channels_in_order() {
180        let (colour, alpha) = split_alpha(&[1, 2, 3, 4, 5, 6, 7, 8]);
181        assert_eq!(colour, vec![1, 2, 3, 5, 6, 7]);
182        assert_eq!(alpha, vec![4, 8]);
183    }
184}