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 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 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 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 pub fn width(&self) -> u32 {
202 self.width
203 }
204
205 pub fn height(&self) -> u32 {
207 self.height
208 }
209
210 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 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
267fn name(text: &str) -> Name {
269 Name(text.to_string())
270}
271
272fn 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}