1use pdfboss_core::{Dict, Name, ObjRef, Object};
7
8use crate::error::{Error, Result};
9use crate::writer::Writer;
10
11#[derive(Debug, Clone, PartialEq)]
13pub struct ImageData {
14 width: u32,
15 height: u32,
16 kind: ImageKind,
17}
18
19#[derive(Debug, Clone, PartialEq)]
21enum ImageKind {
22 Jpeg { data: Vec<u8>, gray: bool },
24 Raster {
26 data: Vec<u8>,
27 color: RasterColor,
28 smask: Option<Vec<u8>>,
29 },
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34enum RasterColor {
35 Gray8,
37 Rgb8,
39 Mono1,
41}
42
43impl ImageData {
44 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 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 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 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 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 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 pub fn width(&self) -> u32 {
215 self.width
216 }
217
218 pub fn height(&self) -> u32 {
220 self.height
221 }
222
223 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 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
280fn name(text: &str) -> Name {
282 Name(text.to_string())
283}
284
285fn 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}