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