Skip to main content

pdfboss_write/
image.rs

1//! Image import for document creation. JPEG passes through untouched as
2//! `/DCTDecode` (dimensions sniffed from its SOF marker); PNG is decoded
3//! to a raster with its alpha split into an `/SMask`; raw rasters cover
4//! generated content. No encode-side JPX/JBIG2/CCITT — by design.
5
6use pdfboss_core::{Dict, Name, ObjRef, Object};
7
8use crate::error::{Error, Result};
9use crate::writer::Writer;
10
11/// A decoded or passthrough image ready to embed as an image XObject.
12#[derive(Debug, Clone, PartialEq)]
13pub struct ImageData {
14    width: u32,
15    height: u32,
16    kind: ImageKind,
17}
18
19/// How the pixels are held.
20#[derive(Debug, Clone, PartialEq)]
21enum ImageKind {
22    /// Original JPEG bytes, embedded as-is with `/Filter /DCTDecode`.
23    Jpeg { data: Vec<u8>, gray: bool },
24    /// Uncompressed raster, Flate-compressed on embedding.
25    Raster {
26        data: Vec<u8>,
27        color: RasterColor,
28        smask: Option<Vec<u8>>,
29    },
30}
31
32/// Raster sample layout.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34enum RasterColor {
35    /// 8-bit DeviceGray.
36    Gray8,
37    /// 8-bit DeviceRGB, samples interleaved.
38    Rgb8,
39    /// 1-bit DeviceGray, rows packed MSB-first and byte-padded.
40    Mono1,
41}
42
43impl ImageData {
44    /// Imports a PNG. Truecolor and grayscale (8- and 16-bit, the latter
45    /// reduced to 8), palette (expanded), and alpha (split to `/SMask`)
46    /// are all supported.
47    pub fn png(bytes: &[u8]) -> Result<ImageData> {
48        let mut decoder = png::Decoder::new(std::io::Cursor::new(bytes));
49        decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
50        let mut reader = decoder
51            .read_info()
52            .map_err(|e| Error::Image(format!("png header: {e}")))?;
53        let size = reader
54            .output_buffer_size()
55            .ok_or_else(|| Error::Image("png output buffer size overflows".into()))?;
56        let mut buf = vec![0u8; size];
57        let info = reader
58            .next_frame(&mut buf)
59            .map_err(|e| Error::Image(format!("png pixel data: {e}")))?;
60        buf.truncate(info.buffer_size());
61        if info.bit_depth != png::BitDepth::Eight {
62            return Err(Error::Image(format!(
63                "png bit depth {:?} survived expansion",
64                info.bit_depth
65            )));
66        }
67        let kind = match info.color_type {
68            png::ColorType::Grayscale => ImageKind::Raster {
69                data: buf,
70                color: RasterColor::Gray8,
71                smask: None,
72            },
73            png::ColorType::GrayscaleAlpha => {
74                let (gray, alpha) = split_alpha(&buf, 1);
75                ImageKind::Raster {
76                    data: gray,
77                    color: RasterColor::Gray8,
78                    smask: Some(alpha),
79                }
80            }
81            png::ColorType::Rgb => ImageKind::Raster {
82                data: buf,
83                color: RasterColor::Rgb8,
84                smask: None,
85            },
86            png::ColorType::Rgba => {
87                let (rgb, alpha) = split_alpha(&buf, 3);
88                ImageKind::Raster {
89                    data: rgb,
90                    color: RasterColor::Rgb8,
91                    smask: Some(alpha),
92                }
93            }
94            other => {
95                return Err(Error::Image(format!(
96                    "png color type {other:?} survived expansion"
97                )));
98            }
99        };
100        Ok(ImageData {
101            width: info.width,
102            height: info.height,
103            kind,
104        })
105    }
106
107    /// Imports a baseline or progressive JPEG by passthrough. Dimensions
108    /// and component count are read from the SOF marker; grayscale and
109    /// three-component (YCbCr/RGB) images are supported, anything else is
110    /// an error.
111    pub fn jpeg(bytes: &[u8]) -> Result<ImageData> {
112        if bytes.len() < 2 || bytes[0] != 0xFF || bytes[1] != 0xD8 {
113            return Err(Error::Image("jpeg missing SOI marker".into()));
114        }
115        let mut pos = 2usize;
116        loop {
117            if pos >= bytes.len() {
118                return Err(Error::Image("jpeg truncated before a SOF marker".into()));
119            }
120            if bytes[pos] != 0xFF {
121                return Err(Error::Image(format!(
122                    "jpeg expected a marker at byte {pos}, found 0x{:02X}",
123                    bytes[pos]
124                )));
125            }
126            while pos < bytes.len() && bytes[pos] == 0xFF {
127                pos += 1;
128            }
129            if pos >= bytes.len() {
130                return Err(Error::Image("jpeg truncated inside a marker".into()));
131            }
132            let marker = bytes[pos];
133            pos += 1;
134            match marker {
135                0xC0..=0xC2 => return sniff_sof(bytes, pos),
136                0xC3 | 0xC5..=0xC7 | 0xC9..=0xCB | 0xCD..=0xCF => {
137                    return Err(Error::Image(format!(
138                        "jpeg SOF{} (marker 0xFF{marker:02X}) is not supported for passthrough",
139                        marker as usize - 0xC0
140                    )));
141                }
142                0xD9 => return Err(Error::Image("jpeg ended (EOI) before a SOF marker".into())),
143                0xDA => {
144                    return Err(Error::Image("jpeg scan started before a SOF marker".into()));
145                }
146                0x00 => return Err(Error::Image("jpeg stray 0xFF00 outside a scan".into())),
147                0x01 | 0xD0..=0xD7 => {}
148                other => pos = skip_segment(bytes, pos, other)?,
149            }
150        }
151    }
152
153    /// Imports a PNG or JPEG, dispatched by content rather than by file
154    /// extension: a PNG signature selects [`ImageData::png`], a JPEG SOI
155    /// marker selects [`ImageData::jpeg`], anything else is an error.
156    pub fn decode(bytes: &[u8]) -> Result<ImageData> {
157        if bytes.starts_with(&[0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1A, b'\n']) {
158            return ImageData::png(bytes);
159        }
160        if bytes.starts_with(&[0xFF, 0xD8]) {
161            return ImageData::jpeg(bytes);
162        }
163        Err(Error::Image("not a png or jpeg (by content)".into()))
164    }
165
166    /// Wraps an 8-bit grayscale raster; `data` is `width * height` bytes.
167    pub fn gray8(width: u32, height: u32, data: Vec<u8>) -> Result<ImageData> {
168        let expected = checked_dims("gray8", width, height)?;
169        check_len("gray8", expected, data.len())?;
170        Ok(ImageData {
171            width,
172            height,
173            kind: ImageKind::Raster {
174                data,
175                color: RasterColor::Gray8,
176                smask: None,
177            },
178        })
179    }
180
181    /// Wraps an 8-bit RGB raster; `data` is `width * height * 3` bytes.
182    pub fn rgb8(width: u32, height: u32, data: Vec<u8>) -> Result<ImageData> {
183        let expected = checked_dims("rgb8", width, height)? * 3;
184        check_len("rgb8", expected, data.len())?;
185        Ok(ImageData {
186            width,
187            height,
188            kind: ImageKind::Raster {
189                data,
190                color: RasterColor::Rgb8,
191                smask: None,
192            },
193        })
194    }
195
196    /// Wraps a 1-bit raster; rows are packed MSB-first, each row padded to
197    /// a whole byte. A set bit is black (sample 0).
198    pub fn mono(width: u32, height: u32, data: Vec<u8>) -> Result<ImageData> {
199        checked_dims("mono", width, height)?;
200        let expected = (width as usize).div_ceil(8) * height as usize;
201        check_len("mono", expected, data.len())?;
202        Ok(ImageData {
203            width,
204            height,
205            kind: ImageKind::Raster {
206                data,
207                color: RasterColor::Mono1,
208                smask: None,
209            },
210        })
211    }
212
213    /// Pixel width.
214    pub fn width(&self) -> u32 {
215        self.width
216    }
217
218    /// Pixel height.
219    pub fn height(&self) -> u32 {
220        self.height
221    }
222
223    /// Emits this image (and its soft mask, if any) into `w` as image
224    /// XObject(s), returning the image object's reference. The soft mask
225    /// is put first, so its object number precedes the image's.
226    pub(crate) fn build_xobject(&self, w: &mut Writer) -> ObjRef {
227        match &self.kind {
228            ImageKind::Jpeg { data, gray } => {
229                let mut dict = self.base_dict();
230                dict.insert(name("Filter"), Object::Name(name("DCTDecode")));
231                dict.insert(name("BitsPerComponent"), Object::Int(8));
232                dict.insert(name("ColorSpace"), Object::Name(gray_or_rgb(*gray)));
233                w.put_stream_raw(dict, data.clone())
234            }
235            ImageKind::Raster { data, color, smask } => {
236                let mask_ref = smask.as_ref().map(|alpha| {
237                    let mut mask = self.base_dict();
238                    mask.insert(name("BitsPerComponent"), Object::Int(8));
239                    mask.insert(name("ColorSpace"), Object::Name(name("DeviceGray")));
240                    w.put_stream(mask, alpha.clone())
241                });
242                let mut dict = self.base_dict();
243                match color {
244                    RasterColor::Gray8 => {
245                        dict.insert(name("BitsPerComponent"), Object::Int(8));
246                        dict.insert(name("ColorSpace"), Object::Name(name("DeviceGray")));
247                    }
248                    RasterColor::Rgb8 => {
249                        dict.insert(name("BitsPerComponent"), Object::Int(8));
250                        dict.insert(name("ColorSpace"), Object::Name(name("DeviceRGB")));
251                    }
252                    RasterColor::Mono1 => {
253                        dict.insert(name("BitsPerComponent"), Object::Int(1));
254                        dict.insert(name("ColorSpace"), Object::Name(name("DeviceGray")));
255                        dict.insert(
256                            name("Decode"),
257                            Object::Array(vec![Object::Int(1), Object::Int(0)]),
258                        );
259                    }
260                }
261                if let Some(mask_ref) = mask_ref {
262                    dict.insert(name("SMask"), Object::Ref(mask_ref));
263                }
264                w.put_stream(dict, data.clone())
265            }
266        }
267    }
268
269    /// The dictionary entries every image XObject shares.
270    fn base_dict(&self) -> Dict {
271        let mut dict = Dict::new();
272        dict.insert(name("Type"), Object::Name(name("XObject")));
273        dict.insert(name("Subtype"), Object::Name(name("Image")));
274        dict.insert(name("Width"), Object::Int(i64::from(self.width)));
275        dict.insert(name("Height"), Object::Int(i64::from(self.height)));
276        dict
277    }
278}
279
280/// A `Name` from a string literal.
281fn name(text: &str) -> Name {
282    Name(text.to_string())
283}
284
285/// The device color space name for a one- or three-component image.
286fn gray_or_rgb(gray: bool) -> Name {
287    if gray {
288        name("DeviceGray")
289    } else {
290        name("DeviceRGB")
291    }
292}
293
294fn split_alpha(samples: &[u8], color_channels: usize) -> (Vec<u8>, Vec<u8>) {
295    let pixels = samples.len() / (color_channels + 1);
296    let mut color: Vec<u8> = Vec::with_capacity(pixels * color_channels);
297    let mut alpha: Vec<u8> = Vec::with_capacity(pixels);
298    for px in samples.chunks_exact(color_channels + 1) {
299        color.extend_from_slice(&px[..color_channels]);
300        alpha.push(px[color_channels]);
301    }
302    (color, alpha)
303}
304
305fn sniff_sof(bytes: &[u8], pos: usize) -> Result<ImageData> {
306    if pos + 8 > bytes.len() {
307        return Err(Error::Image("jpeg truncated inside its SOF marker".into()));
308    }
309    let precision = bytes[pos + 2];
310    if precision != 8 {
311        return Err(Error::Image(format!(
312            "jpeg sample precision is {precision}, only 8 is supported"
313        )));
314    }
315    let height = u32::from(u16::from_be_bytes([bytes[pos + 3], bytes[pos + 4]]));
316    let width = u32::from(u16::from_be_bytes([bytes[pos + 5], bytes[pos + 6]]));
317    if width == 0 || height == 0 {
318        return Err(Error::Image(format!(
319            "jpeg declares degenerate dimensions {width}x{height}"
320        )));
321    }
322    let gray = match bytes[pos + 7] {
323        1 => true,
324        3 => false,
325        n => {
326            return Err(Error::Image(format!(
327                "jpeg has {n} components, only 1 or 3 are supported"
328            )));
329        }
330    };
331    Ok(ImageData {
332        width,
333        height,
334        kind: ImageKind::Jpeg {
335            data: bytes.to_vec(),
336            gray,
337        },
338    })
339}
340
341fn skip_segment(bytes: &[u8], pos: usize, marker: u8) -> Result<usize> {
342    if pos + 2 > bytes.len() {
343        return Err(Error::Image(format!(
344            "jpeg truncated in the length of marker 0xFF{marker:02X}"
345        )));
346    }
347    let len = usize::from(u16::from_be_bytes([bytes[pos], bytes[pos + 1]]));
348    if len < 2 {
349        return Err(Error::Image(format!(
350            "jpeg marker 0xFF{marker:02X} has segment length {len}, minimum is 2"
351        )));
352    }
353    if pos + len > bytes.len() {
354        return Err(Error::Image(format!(
355            "jpeg truncated inside the segment of marker 0xFF{marker:02X}"
356        )));
357    }
358    Ok(pos + len)
359}
360
361fn checked_dims(label: &str, width: u32, height: u32) -> Result<usize> {
362    if width == 0 || height == 0 {
363        return Err(Error::Image(format!(
364            "{label} raster: dimensions {width}x{height} must be nonzero"
365        )));
366    }
367    Ok(width as usize * height as usize)
368}
369
370fn check_len(label: &str, expected: usize, got: usize) -> Result<()> {
371    if got != expected {
372        return Err(Error::Image(format!(
373            "{label} raster: expected {expected} bytes, got {got}"
374        )));
375    }
376    Ok(())
377}
378
379#[cfg(test)]
380mod tests {
381    use super::{ImageData, ImageKind, RasterColor};
382    use crate::error::Error;
383    use std::io::Write;
384
385    fn encode_png(
386        width: u32,
387        height: u32,
388        color: png::ColorType,
389        depth: png::BitDepth,
390        palette: Option<&[u8]>,
391        data: &[u8],
392    ) -> Vec<u8> {
393        let mut out: Vec<u8> = Vec::new();
394        let mut enc = png::Encoder::new(&mut out, width, height);
395        enc.set_color(color);
396        enc.set_depth(depth);
397        if let Some(p) = palette {
398            enc.set_palette(p.to_vec());
399        }
400        let mut writer = enc.write_header().unwrap();
401        writer.write_image_data(data).unwrap();
402        writer.finish().unwrap();
403        out
404    }
405
406    fn png_chunk(name: &[u8; 4], payload: &[u8]) -> Vec<u8> {
407        let mut chunk: Vec<u8> = Vec::new();
408        chunk.extend_from_slice(&(payload.len() as u32).to_be_bytes());
409        chunk.extend_from_slice(name);
410        chunk.extend_from_slice(payload);
411        let mut crc = flate2::Crc::new();
412        crc.update(name);
413        crc.update(payload);
414        chunk.extend_from_slice(&crc.sum().to_be_bytes());
415        chunk
416    }
417
418    fn interlaced_gray_2x2(pixels: [u8; 4]) -> Vec<u8> {
419        let [p00, p10, p01, p11] = pixels;
420        let mut ihdr: Vec<u8> = Vec::new();
421        ihdr.extend_from_slice(&2u32.to_be_bytes());
422        ihdr.extend_from_slice(&2u32.to_be_bytes());
423        ihdr.extend_from_slice(&[8, 0, 0, 0, 1]);
424        let raw: [u8; 7] = [0, p00, 0, p10, 0, p01, p11];
425        let mut zlib = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
426        zlib.write_all(&raw).unwrap();
427        let idat = zlib.finish().unwrap();
428        let mut file: Vec<u8> = vec![137, 80, 78, 71, 13, 10, 26, 10];
429        file.extend_from_slice(&png_chunk(b"IHDR", &ihdr));
430        file.extend_from_slice(&png_chunk(b"IDAT", &idat));
431        file.extend_from_slice(&png_chunk(b"IEND", &[]));
432        file
433    }
434
435    fn raster(img: &ImageData) -> (&[u8], RasterColor, Option<&[u8]>) {
436        match &img.kind {
437            ImageKind::Raster { data, color, smask } => (data, *color, smask.as_deref()),
438            ImageKind::Jpeg { .. } => panic!("expected raster, got jpeg"),
439        }
440    }
441
442    fn image_message(result: crate::error::Result<ImageData>) -> String {
443        match result {
444            Err(Error::Image(msg)) => msg,
445            other => panic!("expected Error::Image, got {other:?}"),
446        }
447    }
448
449    #[test]
450    fn png_rgb() {
451        let data: [u8; 12] = [255, 0, 0, 0, 255, 0, 0, 0, 255, 9, 8, 7];
452        let bytes = encode_png(2, 2, png::ColorType::Rgb, png::BitDepth::Eight, None, &data);
453        let img = ImageData::png(&bytes).unwrap();
454        assert_eq!((img.width(), img.height()), (2, 2));
455        let (pixels, color, smask) = raster(&img);
456        assert_eq!(color, RasterColor::Rgb8);
457        assert_eq!(pixels, data);
458        assert!(smask.is_none());
459    }
460
461    #[test]
462    fn png_rgba_splits_smask() {
463        let data: [u8; 16] = [1, 2, 3, 128, 4, 5, 6, 255, 7, 8, 9, 0, 10, 11, 12, 64];
464        let bytes = encode_png(
465            2,
466            2,
467            png::ColorType::Rgba,
468            png::BitDepth::Eight,
469            None,
470            &data,
471        );
472        let img = ImageData::png(&bytes).unwrap();
473        assert_eq!((img.width(), img.height()), (2, 2));
474        let (pixels, color, smask) = raster(&img);
475        assert_eq!(color, RasterColor::Rgb8);
476        assert_eq!(pixels, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
477        assert_eq!(smask, Some([128, 255, 0, 64].as_slice()));
478    }
479
480    #[test]
481    fn png_gray() {
482        let data: [u8; 6] = [0, 60, 120, 180, 220, 255];
483        let bytes = encode_png(
484            3,
485            2,
486            png::ColorType::Grayscale,
487            png::BitDepth::Eight,
488            None,
489            &data,
490        );
491        let img = ImageData::png(&bytes).unwrap();
492        assert_eq!((img.width(), img.height()), (3, 2));
493        let (pixels, color, smask) = raster(&img);
494        assert_eq!(color, RasterColor::Gray8);
495        assert_eq!(pixels, data);
496        assert!(smask.is_none());
497    }
498
499    #[test]
500    fn png_gray_alpha_splits_smask() {
501        let data: [u8; 4] = [50, 200, 100, 30];
502        let bytes = encode_png(
503            2,
504            1,
505            png::ColorType::GrayscaleAlpha,
506            png::BitDepth::Eight,
507            None,
508            &data,
509        );
510        let img = ImageData::png(&bytes).unwrap();
511        assert_eq!((img.width(), img.height()), (2, 1));
512        let (pixels, color, smask) = raster(&img);
513        assert_eq!(color, RasterColor::Gray8);
514        assert_eq!(pixels, [50, 100]);
515        assert_eq!(smask, Some([200, 30].as_slice()));
516    }
517
518    #[test]
519    fn png_palette_expands_to_rgb() {
520        let palette: [u8; 6] = [255, 0, 0, 0, 255, 0];
521        let bytes = encode_png(
522            2,
523            1,
524            png::ColorType::Indexed,
525            png::BitDepth::Eight,
526            Some(&palette),
527            &[0, 1],
528        );
529        let img = ImageData::png(&bytes).unwrap();
530        assert_eq!((img.width(), img.height()), (2, 1));
531        let (pixels, color, smask) = raster(&img);
532        assert_eq!(color, RasterColor::Rgb8);
533        assert_eq!(pixels, [255, 0, 0, 0, 255, 0]);
534        assert!(smask.is_none());
535    }
536
537    #[test]
538    fn png_sixteen_bit_reduces_to_eight() {
539        let data: [u8; 12] = [
540            0xAB, 0xCD, 0x12, 0x34, 0xFF, 0xFF, 0x00, 0x01, 0x80, 0x00, 0x7F, 0xFE,
541        ];
542        let bytes = encode_png(
543            2,
544            1,
545            png::ColorType::Rgb,
546            png::BitDepth::Sixteen,
547            None,
548            &data,
549        );
550        let img = ImageData::png(&bytes).unwrap();
551        assert_eq!((img.width(), img.height()), (2, 1));
552        let (pixels, color, smask) = raster(&img);
553        assert_eq!(color, RasterColor::Rgb8);
554        assert_eq!(pixels, [0xAB, 0x12, 0xFF, 0x00, 0x80, 0x7F]);
555        assert!(smask.is_none());
556    }
557
558    #[test]
559    fn png_interlaced_deinterlaces() {
560        let bytes = interlaced_gray_2x2([10, 20, 30, 40]);
561        let img = ImageData::png(&bytes).unwrap();
562        assert_eq!((img.width(), img.height()), (2, 2));
563        let (pixels, color, smask) = raster(&img);
564        assert_eq!(color, RasterColor::Gray8);
565        assert_eq!(pixels, [10, 20, 30, 40]);
566        assert!(smask.is_none());
567    }
568
569    #[test]
570    fn png_garbage_is_image_error() {
571        let msg = image_message(ImageData::png(&[1, 2, 3, 4, 5, 6, 7, 8]));
572        assert!(!msg.is_empty());
573    }
574
575    fn jpeg_sof(marker: u8, precision: u8, width: u16, height: u16, components: u8) -> Vec<u8> {
576        let mut seg: Vec<u8> = vec![0xFF, marker];
577        seg.extend_from_slice(&(8 + 3 * components as u16).to_be_bytes());
578        seg.push(precision);
579        seg.extend_from_slice(&height.to_be_bytes());
580        seg.extend_from_slice(&width.to_be_bytes());
581        seg.push(components);
582        for id in 0..components {
583            seg.extend_from_slice(&[id + 1, 0x11, 0]);
584        }
585        seg
586    }
587
588    fn jpeg_app1() -> Vec<u8> {
589        let payload = b"pdfboss-write";
590        let mut seg: Vec<u8> = vec![0xFF, 0xE1];
591        seg.extend_from_slice(&((payload.len() + 2) as u16).to_be_bytes());
592        seg.extend_from_slice(payload);
593        seg
594    }
595
596    fn jpeg_bytes(segments: &[Vec<u8>]) -> Vec<u8> {
597        let mut out: Vec<u8> = vec![0xFF, 0xD8];
598        for seg in segments {
599            out.extend_from_slice(seg);
600        }
601        out.extend_from_slice(&[0xFF, 0xD9]);
602        out
603    }
604
605    #[test]
606    fn jpeg_color_baseline() {
607        let bytes = jpeg_bytes(&[jpeg_app1(), jpeg_sof(0xC0, 8, 5, 7, 3)]);
608        let img = ImageData::jpeg(&bytes).unwrap();
609        assert_eq!((img.width(), img.height()), (5, 7));
610        match &img.kind {
611            ImageKind::Jpeg { data, gray } => {
612                assert_eq!(data, &bytes);
613                assert!(!gray);
614            }
615            ImageKind::Raster { .. } => panic!("expected jpeg passthrough"),
616        }
617    }
618
619    #[test]
620    fn jpeg_gray_with_fill_bytes() {
621        let mut sof = jpeg_sof(0xC1, 8, 9, 4, 1);
622        sof.insert(0, 0xFF);
623        let bytes = jpeg_bytes(&[jpeg_app1(), sof]);
624        let img = ImageData::jpeg(&bytes).unwrap();
625        assert_eq!((img.width(), img.height()), (9, 4));
626        match &img.kind {
627            ImageKind::Jpeg { data, gray } => {
628                assert_eq!(data, &bytes);
629                assert!(gray);
630            }
631            ImageKind::Raster { .. } => panic!("expected jpeg passthrough"),
632        }
633    }
634
635    #[test]
636    fn jpeg_progressive_sof2() {
637        let bytes = jpeg_bytes(&[jpeg_sof(0xC2, 8, 640, 480, 3)]);
638        let img = ImageData::jpeg(&bytes).unwrap();
639        assert_eq!((img.width(), img.height()), (640, 480));
640    }
641
642    #[test]
643    fn jpeg_four_components_rejected_naming_count() {
644        let bytes = jpeg_bytes(&[jpeg_sof(0xC0, 8, 4, 4, 4)]);
645        let msg = image_message(ImageData::jpeg(&bytes));
646        assert!(msg.contains('4'), "message should name the count: {msg}");
647    }
648
649    #[test]
650    fn jpeg_lossless_sof3_rejected() {
651        let bytes = jpeg_bytes(&[jpeg_sof(0xC3, 8, 4, 4, 1)]);
652        image_message(ImageData::jpeg(&bytes));
653    }
654
655    #[test]
656    fn jpeg_truncated_rejected() {
657        let full = jpeg_bytes(&[jpeg_app1(), jpeg_sof(0xC0, 8, 5, 7, 3)]);
658        image_message(ImageData::jpeg(&full[..6]));
659    }
660
661    #[test]
662    fn jpeg_missing_soi_rejected() {
663        image_message(ImageData::jpeg(&[0x00, 0x11, 0x22]));
664    }
665
666    #[test]
667    fn jpeg_non_eight_bit_precision_rejected() {
668        let bytes = jpeg_bytes(&[jpeg_sof(0xC1, 12, 4, 4, 1)]);
669        let msg = image_message(ImageData::jpeg(&bytes));
670        assert!(
671            msg.contains("12"),
672            "message should name the precision: {msg}"
673        );
674    }
675
676    #[test]
677    fn gray8_accepts_exact_length() {
678        let img = ImageData::gray8(2, 3, vec![1, 2, 3, 4, 5, 6]).unwrap();
679        assert_eq!((img.width(), img.height()), (2, 3));
680        let (pixels, color, smask) = raster(&img);
681        assert_eq!(color, RasterColor::Gray8);
682        assert_eq!(pixels, [1, 2, 3, 4, 5, 6]);
683        assert!(smask.is_none());
684    }
685
686    #[test]
687    fn gray8_wrong_length_names_expected_and_got() {
688        let msg = image_message(ImageData::gray8(2, 3, vec![0; 5]));
689        assert!(msg.contains('6') && msg.contains('5'), "{msg}");
690    }
691
692    #[test]
693    fn rgb8_accepts_exact_length() {
694        let img = ImageData::rgb8(2, 1, vec![9, 8, 7, 6, 5, 4]).unwrap();
695        assert_eq!((img.width(), img.height()), (2, 1));
696        let (pixels, color, smask) = raster(&img);
697        assert_eq!(color, RasterColor::Rgb8);
698        assert_eq!(pixels, [9, 8, 7, 6, 5, 4]);
699        assert!(smask.is_none());
700    }
701
702    #[test]
703    fn rgb8_wrong_length_names_expected_and_got() {
704        let msg = image_message(ImageData::rgb8(2, 1, vec![0; 7]));
705        assert!(msg.contains('6') && msg.contains('7'), "{msg}");
706    }
707
708    #[test]
709    fn mono_accepts_row_padded_length() {
710        let img = ImageData::mono(10, 3, vec![0; 6]).unwrap();
711        assert_eq!((img.width(), img.height()), (10, 3));
712        let (pixels, color, smask) = raster(&img);
713        assert_eq!(color, RasterColor::Mono1);
714        assert_eq!(pixels, [0; 6]);
715        assert!(smask.is_none());
716    }
717
718    #[test]
719    fn mono_wrong_length_names_expected_and_got() {
720        let msg = image_message(ImageData::mono(10, 3, vec![0; 4]));
721        assert!(msg.contains('6') && msg.contains('4'), "{msg}");
722    }
723
724    #[test]
725    fn zero_dimensions_rejected() {
726        image_message(ImageData::gray8(0, 3, vec![]));
727        image_message(ImageData::rgb8(3, 0, vec![]));
728        image_message(ImageData::mono(0, 0, vec![]));
729    }
730
731    use pdfboss_core::{Dict, Document, Name, ObjRef, Object, Stream};
732
733    use crate::writer::{WriteOptions, Writer, XrefStyle};
734
735    fn name(text: &str) -> Name {
736        Name(text.into())
737    }
738
739    fn document_with_xobject(img: &ImageData, compress: bool) -> (Document, ObjRef) {
740        let mut w = Writer::new(WriteOptions {
741            xref: XrefStyle::Table,
742            compress,
743            object_streams: false,
744            version: (1, 7),
745        });
746        let image_ref = img.build_xobject(&mut w);
747        let content = w.put_stream(Dict::new(), b"q Q\n".to_vec());
748        let pages = w.reserve();
749        let mut page = Dict::new();
750        page.insert(name("Type"), Object::Name(name("Page")));
751        page.insert(name("Parent"), Object::Ref(pages));
752        page.insert(
753            name("MediaBox"),
754            Object::Array(vec![
755                Object::Int(0),
756                Object::Int(0),
757                Object::Int(100),
758                Object::Int(100),
759            ]),
760        );
761        page.insert(name("Contents"), Object::Ref(content));
762        let page_ref = w.put(Object::Dict(page));
763        let mut tree = Dict::new();
764        tree.insert(name("Type"), Object::Name(name("Pages")));
765        tree.insert(name("Kids"), Object::Array(vec![Object::Ref(page_ref)]));
766        tree.insert(name("Count"), Object::Int(1));
767        w.fill(pages, Object::Dict(tree)).unwrap();
768        let mut catalog = Dict::new();
769        catalog.insert(name("Type"), Object::Name(name("Catalog")));
770        catalog.insert(name("Pages"), Object::Ref(pages));
771        let root = w.put(Object::Dict(catalog));
772        let doc = Document::load(w.finish(root).unwrap()).unwrap();
773        (doc, image_ref)
774    }
775
776    fn xobject_stream(doc: &Document, r: ObjRef) -> Stream {
777        doc.resolve(&Object::Ref(r))
778            .unwrap()
779            .as_stream()
780            .unwrap()
781            .clone()
782    }
783
784    #[test]
785    fn xobject_jpeg_gray_passes_through_raw() {
786        let bytes = jpeg_bytes(&[jpeg_sof(0xC0, 8, 9, 4, 1)]);
787        let img = ImageData::jpeg(&bytes).unwrap();
788        let (doc, image_ref) = document_with_xobject(&img, true);
789        let stream = xobject_stream(&doc, image_ref);
790        assert_eq!(stream.dict.get_name("Type"), Some(&name("XObject")));
791        assert_eq!(stream.dict.get_name("Subtype"), Some(&name("Image")));
792        assert_eq!(stream.dict.get_int("Width"), Some(9));
793        assert_eq!(stream.dict.get_int("Height"), Some(4));
794        assert_eq!(stream.dict.get_name("Filter"), Some(&name("DCTDecode")));
795        assert_eq!(stream.dict.get_int("BitsPerComponent"), Some(8));
796        assert_eq!(
797            stream.dict.get_name("ColorSpace"),
798            Some(&name("DeviceGray"))
799        );
800        assert_eq!(stream.data, bytes);
801    }
802
803    #[test]
804    fn xobject_jpeg_color_uses_device_rgb() {
805        let bytes = jpeg_bytes(&[jpeg_sof(0xC0, 8, 5, 7, 3)]);
806        let img = ImageData::jpeg(&bytes).unwrap();
807        let (doc, image_ref) = document_with_xobject(&img, true);
808        let stream = xobject_stream(&doc, image_ref);
809        assert_eq!(stream.dict.get_int("Width"), Some(5));
810        assert_eq!(stream.dict.get_int("Height"), Some(7));
811        assert_eq!(stream.dict.get_name("Filter"), Some(&name("DCTDecode")));
812        assert_eq!(stream.dict.get_name("ColorSpace"), Some(&name("DeviceRGB")));
813        assert_eq!(stream.data, bytes);
814    }
815
816    #[test]
817    fn xobject_rgb_raster_flate_round_trips() {
818        let pixels = vec![255, 0, 0, 0, 255, 0, 0, 0, 255, 9, 8, 7];
819        let img = ImageData::rgb8(2, 2, pixels.clone()).unwrap();
820        let (doc, image_ref) = document_with_xobject(&img, true);
821        let stream = xobject_stream(&doc, image_ref);
822        assert_eq!(stream.dict.get_name("Type"), Some(&name("XObject")));
823        assert_eq!(stream.dict.get_name("Subtype"), Some(&name("Image")));
824        assert_eq!(stream.dict.get_int("Width"), Some(2));
825        assert_eq!(stream.dict.get_int("Height"), Some(2));
826        assert_eq!(stream.dict.get_name("Filter"), Some(&name("FlateDecode")));
827        assert_eq!(stream.dict.get_int("BitsPerComponent"), Some(8));
828        assert_eq!(stream.dict.get_name("ColorSpace"), Some(&name("DeviceRGB")));
829        assert!(stream.dict.get("SMask").is_none());
830        assert!(stream.dict.get("Decode").is_none());
831        assert_eq!(doc.stream_data(&stream).unwrap(), pixels);
832    }
833
834    #[test]
835    fn xobject_smask_is_emitted_first_as_gray8() {
836        let data: [u8; 16] = [1, 2, 3, 128, 4, 5, 6, 255, 7, 8, 9, 0, 10, 11, 12, 64];
837        let bytes = encode_png(
838            2,
839            2,
840            png::ColorType::Rgba,
841            png::BitDepth::Eight,
842            None,
843            &data,
844        );
845        let img = ImageData::png(&bytes).unwrap();
846        let (doc, image_ref) = document_with_xobject(&img, false);
847        assert_eq!(
848            image_ref.num, 2,
849            "the soft mask must claim the number first"
850        );
851        let stream = xobject_stream(&doc, image_ref);
852        let mask_ref = stream.dict.get_ref("SMask").expect("SMask reference");
853        assert_eq!(mask_ref.num, image_ref.num - 1);
854        let mask = xobject_stream(&doc, mask_ref);
855        assert_eq!(mask.dict.get_name("Type"), Some(&name("XObject")));
856        assert_eq!(mask.dict.get_name("Subtype"), Some(&name("Image")));
857        assert_eq!(mask.dict.get_int("Width"), Some(2));
858        assert_eq!(mask.dict.get_int("Height"), Some(2));
859        assert_eq!(mask.dict.get_int("BitsPerComponent"), Some(8));
860        assert_eq!(mask.dict.get_name("ColorSpace"), Some(&name("DeviceGray")));
861        assert_eq!(doc.stream_data(&mask).unwrap(), [128, 255, 0, 64]);
862        assert_eq!(
863            doc.stream_data(&stream).unwrap(),
864            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
865        );
866    }
867
868    #[test]
869    fn xobject_mono1_adds_inverted_decode() {
870        let rows = vec![0b1010_1010u8; 6];
871        let img = ImageData::mono(10, 3, rows.clone()).unwrap();
872        let (doc, image_ref) = document_with_xobject(&img, false);
873        let stream = xobject_stream(&doc, image_ref);
874        assert_eq!(stream.dict.get_int("Width"), Some(10));
875        assert_eq!(stream.dict.get_int("Height"), Some(3));
876        assert_eq!(stream.dict.get_int("BitsPerComponent"), Some(1));
877        assert_eq!(
878            stream.dict.get_name("ColorSpace"),
879            Some(&name("DeviceGray"))
880        );
881        assert_eq!(
882            stream.dict.get_array("Decode"),
883            Some([Object::Int(1), Object::Int(0)].as_slice())
884        );
885        assert_eq!(doc.stream_data(&stream).unwrap(), rows);
886    }
887
888    #[test]
889    fn jpeg_rejects_degenerate_dimensions() {
890        for (width, height) in [(0u16, 8u16), (8, 0), (0, 0)] {
891            let bytes = jpeg_bytes(&[jpeg_sof(0xC0, 8, width, height, 3)]);
892            let msg = image_message(ImageData::jpeg(&bytes));
893            assert!(msg.contains("degenerate"), "{width}x{height}: {msg}");
894        }
895    }
896}