1mod bitimage;
25mod cache;
26mod dct;
27mod decode_array;
28mod dict;
29#[cfg(feature = "jbig2")]
30mod jbig2;
31#[cfg(feature = "jpeg2000")]
32mod jpx;
33mod mask;
34mod packed;
35mod rows;
36mod scanline;
37
38pub use bitimage::BitImage;
39pub use cache::{ImageCache, MAX_BYTES, RequestedSize};
40pub(crate) use dct::decode_dct;
41pub(crate) use decode_array::DecodeMap;
42pub(crate) use dict::ImageDict;
43#[cfg(feature = "jbig2")]
44pub use jbig2::decode_jbig2;
45#[cfg(feature = "jpeg2000")]
46pub(crate) use jpx::SpaceOverride;
47#[cfg(feature = "jpeg2000")]
48pub use jpx::{JpxImage, decode_jpx};
49pub use mask::ImageMask;
50pub(crate) use mask::{ColorKey, matte_color};
51pub use packed::{Depth, Packed, Unpacked};
52pub use rows::{Converted, Palette, Rgb8, Rgba8, Row, Rows, Source};
53
54use crate::color::{ColorSpace, Rgb};
55use crate::error::Error;
56use crate::function::FunctionCache;
57use crate::names;
58use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
59#[cfg(feature = "ccitt")]
60use pdfrum_filters::{CcittParams, decode_ccitt};
61use pdfrum_filters::{Filter, decode_chain};
62use pdfrum_object::{Dict, Object, Resolve, Stream};
63
64pub const MAX_IMAGE_PIXELS: u64 = 1 << 30;
75
76#[must_use]
81pub fn image_area_is_workable(width: u32, height: u32) -> bool {
82 u64::from(width).saturating_mul(u64::from(height)) <= MAX_IMAGE_PIXELS
83}
84
85#[derive(Debug, Clone, PartialEq)]
91#[non_exhaustive]
92pub enum Pixels {
93 Stencil(BitImage),
96 Gray8(Box<[u8]>),
98 Rgb8(Box<[u8]>),
100 Cmyk8(Box<[u8]>),
102 Indexed {
104 indices: Box<[u8]>,
106 palette: Box<[Rgb]>,
108 },
109}
110
111impl Pixels {
112 #[must_use]
114 pub fn components(&self) -> usize {
115 match self {
116 Self::Stencil(_) | Self::Gray8(_) | Self::Indexed { .. } => 1,
117 Self::Rgb8(_) => 3,
118 Self::Cmyk8(_) => 4,
119 }
120 }
121
122 #[must_use]
124 pub fn byte_size(&self) -> usize {
125 match self {
126 Self::Stencil(b) => b.bits.len(),
127 Self::Gray8(d) | Self::Rgb8(d) | Self::Cmyk8(d) => d.len(),
128 Self::Indexed { indices, palette } => {
129 indices.len() + palette.len() * std::mem::size_of::<Rgb>()
130 }
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq)]
149#[non_exhaustive]
150pub enum Samples {
151 Packed(Packed),
153 Whole(Pixels),
155}
156
157impl Samples {
158 #[must_use]
160 pub fn components(&self) -> usize {
161 match self {
162 Self::Packed(p) => p.components(),
163 Self::Whole(p) => p.components(),
164 }
165 }
166
167 #[must_use]
173 pub fn byte_size(&self) -> usize {
174 match self {
175 Self::Packed(p) => p.byte_size(),
176 Self::Whole(p) => p.byte_size(),
177 }
178 }
179
180 #[must_use]
185 pub const fn is_stencil(&self) -> bool {
186 matches!(self, Self::Whole(Pixels::Stencil(_)))
187 }
188
189 #[must_use]
194 pub fn palette(&self) -> Option<&[Rgb]> {
195 match self {
196 Self::Whole(Pixels::Indexed { palette, .. }) => Some(palette),
197 _ => None,
198 }
199 }
200
201 #[must_use]
207 pub fn to_pixels(&self) -> Pixels {
208 match self {
209 Self::Whole(p) => p.clone(),
210 Self::Packed(p) => {
211 let data = Unpacked::new(p).collect_all();
212 match p.components() {
213 1 => Pixels::Gray8(data),
214 4 => Pixels::Cmyk8(data),
215 _ => Pixels::Rgb8(data),
216 }
217 }
218 }
219 }
220}
221
222#[derive(Debug, Clone, PartialEq)]
224pub struct ImageData {
225 pub width: u32,
228 pub height: u32,
230 pub samples: Samples,
232 pub mask: Option<ImageMask>,
234 pub matte: Option<Rgb>,
237 pub interpolate: bool,
239}
240
241impl ImageData {
242 #[must_use]
244 pub fn byte_size(&self) -> usize {
245 self.samples.byte_size()
246 + match &self.mask {
247 Some(ImageMask::Alpha { alpha, .. }) => alpha.len(),
248 _ => 0,
249 }
250 }
251}
252
253#[expect(
268 clippy::too_many_arguments,
269 reason = "the image ladder genuinely needs the stream, both resource \
270 dictionaries, the requested size, the resolver, the function \
271 cache, limits and diagnostics"
272)]
273#[expect(
274 clippy::too_many_lines,
275 reason = "the load ladder reads as one sequence; splitting it would hide \
276 the order the rungs run in"
277)]
278pub fn decode_image<R: Resolve>(
279 stream: &Stream,
280 form_resources: Option<&Dict>,
281 page_resources: Option<&Dict>,
282 size: RequestedSize,
283 r: &R,
284 functions: &mut FunctionCache,
285 limits: &Limits,
286 diags: &mut Diagnostics,
287) -> Result<ImageData, Error> {
288 let info = ImageDict::load(&stream.dict, r, diags)?;
289
290 if info.image_mask {
292 return decode_stencil(stream, &info, r, limits, diags);
293 }
294
295 let space = resolve_space(
296 &stream.dict,
297 form_resources,
298 page_resources,
299 r,
300 functions,
301 limits,
302 diags,
303 );
304 let components = info
305 .components
306 .max(u32::try_from(space.as_ref().map_or(0, ColorSpace::n_components)).unwrap_or(0));
307 let info = ImageDict { components, ..info };
308
309 let decoded = decode_chain(stream, info.total_bytes().unwrap_or(0), r, limits, diags);
310
311 #[cfg(not(feature = "jpeg2000"))]
314 let _ = size;
315 let (width, height, samples, jpx_alpha) = match info.last_filter {
316 #[cfg(feature = "jpeg2000")]
317 Some(Filter::Jpx) => {
318 let smask_in_data = stream.dict.int(names::SMASK_IN_DATA, r).unwrap_or(0);
319 let image = decode_jpx(&decoded.data, space.as_ref(), smask_in_data, size, limits)
323 .inspect_err(|_| {
324 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
325 })?;
326 if image.space_override != SpaceOverride::Keep {
327 diags.record(Severity::Recovered, DiagKind::JpxColorSpaceOverride, None);
328 }
329 let pixels = match (&space, image.components) {
330 (Some(cs @ ColorSpace::Indexed(indexed)), 1) => {
348 let indices: Box<[u8]> = if info.bpc >= 8 {
354 image.data.iter().copied().collect()
355 } else {
356 let scale = 8u32.saturating_sub(info.bpc);
357 image
358 .data
359 .iter()
360 .map(|&v| u8::try_from(u32::from(v) >> scale).unwrap_or(0))
361 .collect()
362 };
363 let palette = (0..=indexed.max_index)
364 .map(|i| cs.to_rgb(&[f32::from(i)]))
365 .collect();
366 Pixels::Indexed { indices, palette }
367 }
368 (_, 1) => Pixels::Gray8(image.data.into()),
369 (_, 4) => Pixels::Cmyk8(image.data.into()),
370 _ => Pixels::Rgb8(image.data.into()),
371 };
372 (
373 image.width,
374 image.height,
375 Samples::Whole(pixels),
376 image.alpha,
377 )
378 }
379 #[cfg(not(feature = "jpeg2000"))]
380 Some(Filter::Jpx) => {
381 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
382 return Err(Error::ImageUndecodable {
383 what: concat!("this build has no ", "Jpx", " decoder (feature `jpeg2000`)"),
384 });
385 }
386 #[cfg(feature = "jbig2")]
387 Some(Filter::Jbig2) => {
388 let globals = info
389 .params
390 .stream(names::JBIG2_GLOBALS, r)
391 .map(|s| decode_chain(&s, 0, r, limits, diags).data);
393 let bits = decode_jbig2(
394 globals.as_deref(),
395 &decoded.data,
396 info.width,
397 info.height,
398 limits,
399 )
400 .inspect_err(|_| {
401 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
402 })?;
403 if info.image_mask {
415 (
416 info.width,
417 info.height,
418 Samples::Whole(Pixels::Stencil(bits)),
419 None,
420 )
421 } else {
422 let mut samples = bits.bits;
423 for byte in &mut samples {
424 *byte = !*byte;
425 }
426 let samples = unpack(&info, space.as_ref(), &samples, diags)?;
427 (info.width, info.height, samples, None)
428 }
429 }
430 #[cfg(not(feature = "jbig2"))]
431 Some(Filter::Jbig2) => {
432 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
433 return Err(Error::ImageUndecodable {
434 what: concat!("this build has no ", "Jbig2", " decoder (feature `jbig2`)"),
435 });
436 }
437 Some(Filter::Dct) => {
438 let image = decode_dct(&decoded.data, (info.width, info.height)).inspect_err(|_| {
439 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
440 })?;
441 if image.width != info.width || image.height != info.height {
443 diags.record(
444 Severity::Recovered,
445 DiagKind::ImageDimensionsFromCodec,
446 None,
447 );
448 }
449 if !dct::component_mismatch_allowed(space.as_ref(), image.components) {
450 return Err(Error::ImageUndecodable {
451 what: "JPEG component count disagrees with the colour space",
452 });
453 }
454 let mut data = image.data;
455 apply_codec_decode(&mut data, space.as_ref(), image.components, &info);
456 let pixels = match image.components {
457 1 => Pixels::Gray8(data.into()),
458 4 => Pixels::Cmyk8(data.into()),
459 _ => Pixels::Rgb8(data.into()),
460 };
461 (image.width, image.height, Samples::Whole(pixels), None)
462 }
463 Some(Filter::CcittFax) => {
467 let samples = ccitt_samples(&info, &decoded.data, r, limits, diags)?;
468 let samples = unpack(&info, space.as_ref(), &samples, diags)?;
469 (info.width, info.height, samples, None)
470 }
471 _ => {
472 if decoded.image.is_some() && info.last_filter.is_none() {
473 return Err(Error::ImageUndecodable {
474 what: "an unrecognised filter left no decoder",
475 });
476 }
477 let samples = unpack(&info, space.as_ref(), &decoded.data, diags)?;
478 (info.width, info.height, samples, None)
479 }
480 };
481
482 let mask = load_mask(
485 &stream.dict,
486 &info,
487 space.as_ref(),
488 jpx_alpha,
489 r,
490 functions,
491 limits,
492 diags,
493 );
494
495 let mask = match mask {
501 Some(ImageMask::ColorKey(key)) => {
502 resolve_color_key(&key, &info, &decoded.data, width, height)
503 }
504 other => other,
505 };
506
507 let matte = matte_color(
508 stream.dict.array(names::MATTE, r).as_ref(),
509 space.as_ref(),
510 usize::try_from(info.components).unwrap_or(0),
511 );
512
513 Ok(ImageData {
514 width,
515 height,
516 samples,
517 mask,
518 matte,
519 interpolate: stream.dict.bool(names::INTERPOLATE).unwrap_or(false),
520 })
521}
522
523fn resolve_color_key(
538 key: &ColorKey,
539 info: &ImageDict,
540 data: &[u8],
541 width: u32,
542 height: u32,
543) -> Option<ImageMask> {
544 let components = usize::try_from(info.components).unwrap_or(0);
545 if components == 0 || info.bpc == 0 || key.ranges.is_empty() {
546 return None;
547 }
548 let pitch = info.pitch()?;
549 let pixels_per_row = usize::try_from(width).ok()?;
550 let rows = usize::try_from(height).ok()?;
551 let mut alpha = vec![255u8; pixels_per_row.checked_mul(rows)?];
552 let mut samples = vec![0u32; components];
553 let mut any = false;
554 for y in 0..rows {
555 let (line, availability) = scanline::scanline(data, u32::try_from(y).unwrap_or(0), pitch);
556 if availability == scanline::Availability::Absent {
559 continue;
560 }
561 for x in 0..pixels_per_row {
562 for (c, slot) in samples.iter_mut().enumerate() {
563 let bit_pos = (x * components + c) * info.bpc as usize;
564 *slot = scanline::get_bits(&line, bit_pos, info.bpc);
565 }
566 if key.is_transparent(&samples)
567 && let Some(a) = alpha.get_mut(y * pixels_per_row + x)
568 {
569 *a = 0;
570 any = true;
571 }
572 }
573 }
574 any.then(|| ImageMask::Alpha {
575 width,
576 height,
577 alpha: alpha.into(),
578 stencil: false,
579 })
580}
581
582fn reads_the_stream_directly(info: &ImageDict) -> bool {
594 !matches!(
595 info.last_filter,
596 Some(Filter::Jbig2 | Filter::Jpx | Filter::Dct | Filter::CcittFax)
597 )
598}
599
600fn decode_stencil<R: Resolve>(
603 stream: &Stream,
604 info: &ImageDict,
605 r: &R,
606 limits: &Limits,
607 diags: &mut Diagnostics,
608) -> Result<ImageData, Error> {
609 let total = info.total_bytes().ok_or(Error::ImageTooLarge)?;
610 let decoded = decode_chain(stream, total, r, limits, diags);
611 let row_bytes = info.pitch().ok_or(Error::ImageTooLarge)?;
612
613 if info.last_filter == Some(Filter::Jbig2) {
622 return stencil_from_jbig2(stream, info, &decoded.data, r, limits, diags);
623 }
624
625 let ccitt = if info.last_filter == Some(Filter::CcittFax) {
630 Some(ccitt_samples(info, &decoded.data, r, limits, diags)?)
631 } else {
632 None
633 };
634 let samples = ccitt.as_deref().unwrap_or(&decoded.data);
635
636 let mut bits = vec![0u8; total];
637 let mut padded = false;
638 let raw = reads_the_stream_directly(info);
639 for y in 0..info.height {
640 let (mut line, availability) = scanline::scanline(samples, y, row_bytes);
641 padded |= availability != scanline::Availability::Whole;
642 if info.default_decode && !(raw && availability == scanline::Availability::Absent) {
647 scanline::invert_line(&mut line);
648 }
649 let start = usize::try_from(y).unwrap_or(0).saturating_mul(row_bytes);
650 if let Some(dest) = bits.get_mut(start..start + row_bytes) {
651 dest.copy_from_slice(&line);
652 }
653 }
654 if padded {
655 diags.record(Severity::Recovered, DiagKind::ImageStreamTruncated, None);
656 }
657 Ok(ImageData {
658 width: info.width,
659 height: info.height,
660 samples: Samples::Whole(Pixels::Stencil(BitImage {
661 width: info.width,
662 height: info.height,
663 row_bytes,
664 bits,
665 })),
666 mask: None,
667 matte: None,
668 interpolate: stream.dict.bool(names::INTERPOLATE).unwrap_or(false),
669 })
670}
671
672#[cfg(feature = "ccitt")]
673fn ccitt_samples<R: Resolve>(
693 info: &ImageDict,
694 data: &[u8],
695 r: &R,
696 limits: &Limits,
697 diags: &mut Diagnostics,
698) -> Result<Vec<u8>, Error> {
699 let params = CcittParams::from_dict(&info.params, r);
700 let image =
701 decode_ccitt(data, params, info.width, info.height, limits, diags).map_err(|_| {
702 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
703 Error::ImageUndecodable {
704 what: "CCITT fax data would not decode",
705 }
706 })?;
707 let pitch = info.pitch().ok_or(Error::ImageTooLarge)?;
708 let total = info.total_bytes().ok_or(Error::ImageTooLarge)?;
709 if total > limits.max_decoded_stream_len {
713 return Err(Error::ImageTooLarge);
714 }
715 let mut out = vec![0xffu8; total];
719 for y in 0..info.height {
720 let src = usize::try_from(y)
721 .ok()
722 .and_then(|y| y.checked_mul(image.row_bytes));
723 let dest = usize::try_from(y).ok().and_then(|y| y.checked_mul(pitch));
724 let (Some(src), Some(dest)) = (src, dest) else {
725 continue;
726 };
727 let copy = pitch.min(image.row_bytes);
728 let (Some(from), Some(to)) = (
729 image.bits.get(src..src.saturating_add(copy)),
730 out.get_mut(dest..dest.saturating_add(copy)),
731 ) else {
732 continue;
733 };
734 to.copy_from_slice(from);
735 }
736 Ok(out)
737}
738#[cfg(not(feature = "ccitt"))]
739fn ccitt_samples<R: Resolve>(
740 info: &ImageDict,
741 data: &[u8],
742 r: &R,
743 limits: &Limits,
744 diags: &mut Diagnostics,
745) -> Result<Vec<u8>, Error> {
746 let _ = (info, data, r, limits);
747 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
748 Err(Error::ImageUndecodable {
749 what: "this build has no CCITT fax decoder (feature `ccitt`)",
750 })
751}
752
753#[cfg(feature = "jbig2")]
754fn stencil_from_jbig2<R: Resolve>(
769 stream: &Stream,
770 info: &ImageDict,
771 data: &[u8],
772 r: &R,
773 limits: &Limits,
774 diags: &mut Diagnostics,
775) -> Result<ImageData, Error> {
776 let globals = info
777 .params
778 .stream(names::JBIG2_GLOBALS, r)
779 .map(|s| decode_chain(&s, 0, r, limits, diags).data);
782 let mut image = decode_jbig2(globals.as_deref(), data, info.width, info.height, limits)
783 .inspect_err(|_| {
784 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
785 })?;
786 if !info.default_decode {
787 for byte in &mut image.bits {
788 *byte = !*byte;
789 }
790 }
791 Ok(ImageData {
792 width: info.width,
793 height: info.height,
794 samples: Samples::Whole(Pixels::Stencil(image)),
795 mask: None,
796 matte: None,
797 interpolate: stream.dict.bool(names::INTERPOLATE).unwrap_or(false),
798 })
799}
800#[cfg(not(feature = "jbig2"))]
801fn stencil_from_jbig2<R: Resolve>(
802 stream: &Stream,
803 info: &ImageDict,
804 data: &[u8],
805 r: &R,
806 limits: &Limits,
807 diags: &mut Diagnostics,
808) -> Result<ImageData, Error> {
809 let _ = (stream, info, data, r, limits);
810 diags.record(Severity::Suspicious, DiagKind::ImageDecodeFailed, None);
811 Err(Error::ImageUndecodable {
812 what: "this build has no JBIG2 decoder (feature `jbig2`)",
813 })
814}
815
816fn resolve_space<R: Resolve>(
818 dict: &Dict,
819 form_resources: Option<&Dict>,
820 page_resources: Option<&Dict>,
821 r: &R,
822 functions: &mut FunctionCache,
823 limits: &Limits,
824 diags: &mut Diagnostics,
825) -> Option<ColorSpace> {
826 let cs_obj = dict.raw(names::COLOR_SPACE)?;
827 form_resources
829 .and_then(|res| {
830 crate::color::load_colorspace(cs_obj, Some(res), r, functions, limits, diags)
831 })
832 .or_else(|| {
833 crate::color::load_colorspace(cs_obj, page_resources, r, functions, limits, diags)
834 })
835}
836
837struct SampleLayout {
841 pitch: usize,
843 pixels_per_row: usize,
845 rows: usize,
847 total_pixels: usize,
849 max_raw: u32,
851}
852
853fn scan_indices(
864 info: &ImageDict,
865 data: &[u8],
866 remap: Option<&DecodeMap>,
867 layout: &SampleLayout,
868 diags: &mut Diagnostics,
869) -> Box<[u8]> {
870 let mut indices = vec![0u8; layout.total_pixels];
871 let mut padded = false;
872 for y in 0..layout.rows {
873 let (line, availability) =
874 scanline::scanline(data, u32::try_from(y).unwrap_or(0), layout.pitch);
875 padded |= availability != scanline::Availability::Whole;
876 if availability == scanline::Availability::Absent {
877 continue;
878 }
879 for x in 0..layout.pixels_per_row {
880 let raw = scanline::get_bits(&line, x * info.bpc as usize, info.bpc);
881 let index = match remap {
882 #[expect(
883 clippy::cast_possible_truncation,
884 clippy::cast_sign_loss,
885 reason = "the clamp bounds the value to a palette index"
886 )]
887 Some(decode) => decode.apply(0, f64_to_f32(raw)).clamp(0.0, 255.0) as u8,
888 None => u8::try_from(raw.min(255)).unwrap_or(u8::MAX),
889 };
890 if let Some(slot) = indices.get_mut(y * layout.pixels_per_row + x) {
891 *slot = index;
892 }
893 }
894 }
895 if padded {
896 diags.record(Severity::Recovered, DiagKind::ImageStreamTruncated, None);
897 }
898 indices.into()
899}
900
901fn tint_palette(
909 info: &ImageDict,
910 space: &ColorSpace,
911 data: &[u8],
912 decode: &DecodeMap,
913 layout: &SampleLayout,
914 diags: &mut Diagnostics,
915) -> Pixels {
916 let indices = scan_indices(info, data, None, layout, diags);
920 let entries = usize::try_from(layout.max_raw).unwrap_or(255).min(255) + 1;
924 let palette = (0..entries)
925 .map(|i| {
926 #[expect(
927 clippy::cast_precision_loss,
928 reason = "an index of at most 255 is exact in f32"
929 )]
930 let value = decode.apply(0, i as f32);
931 space.to_rgb(&[value])
932 })
933 .collect();
934 Pixels::Indexed { indices, palette }
935}
936
937fn tint_per_pixel(
956 space: &ColorSpace,
957 samples: &[u8],
958 total_pixels: usize,
959) -> Result<Pixels, Error> {
960 let mut bgr = vec![0u8; total_pixels.checked_mul(3).ok_or(Error::ImageTooLarge)?];
961 space.translate_image_line(&mut bgr, samples, total_pixels, false);
962 for px in bgr.as_chunks_mut::<3>().0 {
963 px.swap(0, 2);
964 }
965 Ok(Pixels::Rgb8(bgr.into()))
966}
967
968fn unpack(
977 info: &ImageDict,
978 space: Option<&ColorSpace>,
979 data: &[u8],
980 diags: &mut Diagnostics,
981) -> Result<Samples, Error> {
982 let space = space.ok_or(Error::ImageNoColorSpace)?;
983 let components = usize::try_from(info.components).unwrap_or(0);
984 if components == 0 || info.bpc == 0 {
985 return Err(Error::ImageUndecodable {
986 what: "zero components or bit depth",
987 });
988 }
989 let pitch = info.pitch().ok_or(Error::ImageTooLarge)?;
990 let pixels_per_row = usize::try_from(info.width).unwrap_or(0);
991 let rows = usize::try_from(info.height).unwrap_or(0);
992 let total_pixels = pixels_per_row
993 .checked_mul(rows)
994 .ok_or(Error::ImageTooLarge)?;
995
996 let decode = DecodeMap::new(Some(space), components, info.bpc, info.decode.as_ref());
997 let max_raw = if info.bpc >= 32 {
998 u32::MAX
999 } else {
1000 (1u32 << info.bpc) - 1
1001 };
1002
1003 let layout = SampleLayout {
1004 pitch,
1005 pixels_per_row,
1006 rows,
1007 total_pixels,
1008 max_raw,
1009 };
1010
1011 if let ColorSpace::Indexed(indexed) = space {
1016 let indices = scan_indices(info, data, Some(&decode), &layout, diags);
1017 let palette = (0..=indexed.max_index)
1018 .map(|i| space.to_rgb(&[f32::from(i)]))
1019 .collect();
1020 return Ok(Samples::Whole(Pixels::Indexed { indices, palette }));
1021 }
1022
1023 if components == 1 && space.needs_image_conversion() {
1049 return Ok(Samples::Whole(tint_palette(
1050 info, space, data, &decode, &layout, diags,
1051 )));
1052 }
1053
1054 if space.needs_image_conversion() {
1058 let out = widen_whole(info, data, &decode, &layout, diags)?;
1059 return Ok(Samples::Whole(tint_per_pixel(space, &out, total_pixels)?));
1060 }
1061
1062 let depth = Depth::new(info.bpc).ok_or(Error::ImageUndecodable {
1066 what: "a bit depth that is not 1, 2, 4, 8 or 16",
1067 })?;
1068 let packed = Packed::with_map(
1069 data.into(),
1070 depth,
1071 components,
1072 pitch,
1073 info.width,
1074 info.height,
1075 &decode,
1076 );
1077 if packed.truncated() {
1080 diags.record(Severity::Recovered, DiagKind::ImageStreamTruncated, None);
1081 }
1082 Ok(Samples::Packed(packed))
1083}
1084
1085fn widen_whole(
1090 info: &ImageDict,
1091 data: &[u8],
1092 decode: &DecodeMap,
1093 layout: &SampleLayout,
1094 diags: &mut Diagnostics,
1095) -> Result<Vec<u8>, Error> {
1096 let components = usize::try_from(info.components).unwrap_or(0);
1097 let mut out = vec![
1098 0u8;
1099 layout
1100 .total_pixels
1101 .checked_mul(components)
1102 .ok_or(Error::ImageTooLarge)?
1103 ];
1104 let mut padded = false;
1105 for y in 0..layout.rows {
1106 let (line, availability) =
1107 scanline::scanline(data, u32::try_from(y).unwrap_or(0), layout.pitch);
1108 padded |= availability != scanline::Availability::Whole;
1109 if availability == scanline::Availability::Absent {
1113 continue;
1114 }
1115 for x in 0..layout.pixels_per_row {
1116 for c in 0..components {
1117 let bit_pos = (x * components + c) * info.bpc as usize;
1118 let raw = scanline::get_bits(&line, bit_pos, info.bpc);
1119 let value = decode.apply(c, f64_to_f32(raw));
1120 #[expect(
1121 clippy::cast_possible_truncation,
1122 clippy::cast_sign_loss,
1123 reason = "the clamp bounds the product to 0..=255"
1124 )]
1125 let byte = (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1126 if let Some(slot) = out.get_mut((y * layout.pixels_per_row + x) * components + c) {
1127 *slot = byte;
1128 }
1129 }
1130 }
1131 }
1132 if padded {
1133 diags.record(Severity::Recovered, DiagKind::ImageStreamTruncated, None);
1134 }
1135 Ok(out)
1136}
1137
1138fn apply_codec_decode(
1169 data: &mut [u8],
1170 space: Option<&ColorSpace>,
1171 components: u8,
1172 info: &ImageDict,
1173) {
1174 let components = usize::from(components);
1175 if components == 0 || info.default_decode {
1176 return;
1177 }
1178 let Some(space) = space else { return };
1179 if matches!(space, ColorSpace::Indexed(_)) {
1182 return;
1183 }
1184 let decode = DecodeMap::new(Some(space), components, 8, info.decode.as_ref());
1185 if decode.default {
1186 return;
1187 }
1188 let table = decode_table(&decode, components);
1189 for chunk in data.chunks_mut(components) {
1194 for (component, sample) in chunk.iter_mut().enumerate() {
1195 if let Some(row) = table.get(component)
1196 && let Some(mapped) = row.get(usize::from(*sample))
1197 {
1198 *sample = *mapped;
1199 }
1200 }
1201 }
1202}
1203
1204fn decode_table(decode: &DecodeMap, components: usize) -> Vec<[u8; 256]> {
1214 (0..components)
1215 .map(|component| {
1216 let mut row = [0u8; 256];
1217 for (raw, slot) in row.iter_mut().enumerate() {
1218 #[expect(
1219 clippy::cast_precision_loss,
1220 reason = "a table index below 256 is exact in f32"
1221 )]
1222 let value = decode.apply(component, raw as f32);
1223 #[expect(
1224 clippy::cast_possible_truncation,
1225 clippy::cast_sign_loss,
1226 reason = "the clamp bounds the product to 0..=255"
1227 )]
1228 let byte = (value.clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1229 *slot = byte;
1230 }
1231 row
1232 })
1233 .collect()
1234}
1235
1236#[expect(
1238 clippy::cast_precision_loss,
1239 reason = "raw samples cap at sixteen bits, exact in f32"
1240)]
1241fn f64_to_f32(raw: u32) -> f32 {
1242 raw as f32
1243}
1244
1245#[expect(
1248 clippy::too_many_arguments,
1249 reason = "loading a mask recursively needs the same context the base image did"
1250)]
1251fn load_mask<R: Resolve>(
1252 dict: &Dict,
1253 info: &ImageDict,
1254 space: Option<&ColorSpace>,
1255 jpx_alpha: Option<Vec<u8>>,
1256 r: &R,
1257 functions: &mut FunctionCache,
1258 limits: &Limits,
1259 diags: &mut Diagnostics,
1260) -> Option<ImageMask> {
1261 if let Some(alpha) = jpx_alpha {
1263 return Some(ImageMask::Alpha {
1264 width: info.width,
1265 height: info.height,
1266 alpha: alpha.into(),
1267 stencil: false,
1268 });
1269 }
1270 if let Some(smask) = dict.stream(names::SMASK, r) {
1272 return load_mask_image(&smask, false, r, functions, limits, diags);
1273 }
1274 match dict.get(names::MASK, r).as_deref() {
1275 Some(Object::Stream(mask_stream)) => {
1277 load_mask_image(mask_stream, true, r, functions, limits, diags)
1278 }
1279 Some(Object::Array(array)) => {
1281 let components = usize::try_from(info.components).unwrap_or(0);
1282 if !ColorKey::is_complete(array, components) {
1283 diags.record(Severity::Suspicious, DiagKind::ColorKeyArrayShort, None);
1284 }
1285 let max_raw = if info.bpc >= 32 {
1286 u32::MAX
1287 } else {
1288 (1u32 << info.bpc.max(1)) - 1
1289 };
1290 let _ = space;
1291 Some(ImageMask::ColorKey(ColorKey::from_array(
1292 array, components, max_raw,
1293 )))
1294 }
1295 _ => None,
1296 }
1297}
1298
1299fn load_mask_image<R: Resolve>(
1308 stream: &Stream,
1309 stencil: bool,
1310 r: &R,
1311 functions: &mut FunctionCache,
1312 limits: &Limits,
1313 diags: &mut Diagnostics,
1314) -> Option<ImageMask> {
1315 let decoded = decode_image(
1316 stream,
1317 None,
1318 None,
1319 RequestedSize::Full,
1321 r,
1322 functions,
1323 limits,
1324 diags,
1325 );
1326 let Ok(image) = decoded else {
1327 diags.record(Severity::Recovered, DiagKind::MaskDropped, None);
1328 return None;
1329 };
1330 let Some(alpha) = mask_plane(&image.samples, image.width, image.height) else {
1331 diags.record(Severity::Recovered, DiagKind::MaskDropped, None);
1332 return None;
1333 };
1334 Some(ImageMask::Alpha {
1335 width: image.width,
1336 height: image.height,
1337 alpha,
1338 stencil,
1339 })
1340}
1341
1342fn mask_plane(samples: &Samples, width: u32, height: u32) -> Option<Box<[u8]>> {
1363 if !image_area_is_workable(width, height) {
1369 return None;
1370 }
1371 let len = usize::try_from(width)
1372 .ok()?
1373 .checked_mul(usize::try_from(height).ok()?)?;
1374 let mut alpha = Vec::new();
1379 alpha.try_reserve_exact(len).ok()?;
1380 if let Samples::Whole(Pixels::Gray8(data)) = samples {
1381 alpha.extend(data.iter().take(len).copied());
1382 } else {
1383 let palette = match samples {
1384 Samples::Whole(Pixels::Indexed { palette, .. }) => Some(rows::Palette::new(palette)),
1385 _ => None,
1386 };
1387 let mut converted =
1388 rows::Converted::new(rows::Source::new(samples, width, height), palette);
1389 while let Some(row) = rows::Rows::next(&mut converted) {
1390 alpha.extend(row.pixels().iter().map(|px| px.0[0]));
1391 }
1392 }
1393 alpha.resize(len, 0);
1397 Some(alpha.into())
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402 #![allow(
1406 clippy::unreadable_literal,
1407 clippy::float_cmp,
1408 clippy::indexing_slicing,
1409 clippy::cast_precision_loss,
1410 clippy::cast_possible_truncation,
1411 clippy::cast_sign_loss,
1412 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
1413 )]
1414
1415 use super::{ImageData, Pixels, RequestedSize, Samples, decode_image};
1416 use crate::color::Rgb;
1417 use crate::function::FunctionCache;
1418 use crate::image::BitImage;
1419 use pdfrum_common::{DiagKind, Diagnostics, Limits};
1420 use pdfrum_object::{Array, ByteSpan, Dict, Name, NoResolve, Object, Stream};
1421
1422 fn stream(pairs: Vec<(Name, Object)>, data: &[u8]) -> Stream {
1423 Stream::new(Dict::from_pairs(pairs), ByteSpan::from(data.to_vec()))
1424 }
1425
1426 fn decode(s: &Stream) -> Result<ImageData, crate::Error> {
1427 let mut funcs = FunctionCache::new();
1428 let mut diags = Diagnostics::default();
1429 decode_image(
1430 s,
1431 None,
1432 None,
1433 RequestedSize::Full,
1434 &NoResolve,
1435 &mut funcs,
1436 &Limits::default(),
1437 &mut diags,
1438 )
1439 }
1440
1441 fn converted_row(samples: &Samples, width: u32, y: u32) -> Vec<[u8; 3]> {
1447 let palette = samples.palette().map(super::rows::Palette::new);
1448 let mut converted =
1452 super::rows::Converted::new(super::rows::Source::at_row(samples, width, y), palette);
1453 super::rows::Rows::next(&mut converted)
1454 .map(|row| {
1455 row.pixels()
1456 .iter()
1457 .map(|px| [px.0[0], px.0[1], px.0[2]])
1458 .collect()
1459 })
1460 .unwrap_or_default()
1461 }
1462
1463 fn sample_at(samples: &Samples, x: u32, y: u32, width: u32) -> [u8; 3] {
1469 converted_row(samples, width, y)
1470 .get(x as usize)
1471 .copied()
1472 .unwrap_or([0, 0, 0])
1473 }
1474
1475 #[test]
1476 fn a_colour_key_becomes_an_alpha_plane_on_the_raw_samples() {
1477 let mut mask = Array::default();
1481 mask.push(Object::Int(0));
1482 mask.push(Object::Int(0));
1483 let s = stream(
1484 vec![
1485 (Name::from("Width"), Object::Int(2)),
1486 (Name::from("Height"), Object::Int(2)),
1487 (Name::from("BitsPerComponent"), Object::Int(8)),
1488 (
1489 Name::from("ColorSpace"),
1490 Object::Name(Name::from("DeviceGray")),
1491 ),
1492 (Name::from("Mask"), Object::Array(mask)),
1493 ],
1494 &[0, 200, 0, 255],
1495 );
1496 let image = decode(&s).expect("should decode");
1497 let Some(crate::image::ImageMask::Alpha { alpha, .. }) = image.mask else {
1498 panic!("expected a resolved alpha plane, got {:?}", image.mask);
1499 };
1500 assert_eq!(&*alpha, &[0u8, 255, 0, 255]);
1501 }
1502
1503 #[test]
1504 fn a_colour_key_that_matches_nothing_leaves_the_image_opaque() {
1505 let mut mask = Array::default();
1508 mask.push(Object::Int(7));
1509 mask.push(Object::Int(9));
1510 let s = stream(
1511 vec![
1512 (Name::from("Width"), Object::Int(2)),
1513 (Name::from("Height"), Object::Int(1)),
1514 (Name::from("BitsPerComponent"), Object::Int(8)),
1515 (
1516 Name::from("ColorSpace"),
1517 Object::Name(Name::from("DeviceGray")),
1518 ),
1519 (Name::from("Mask"), Object::Array(mask)),
1520 ],
1521 &[0, 200],
1522 );
1523 assert!(decode(&s).expect("should decode").mask.is_none());
1524 }
1525
1526 #[test]
1527 fn a_row_past_the_end_of_the_stream_skips_the_decode_entirely() {
1528 let mut decode_array = Array::default();
1537 decode_array.push(Object::Real(1.0));
1538 let s = stream(
1539 vec![
1540 (Name::from("Width"), Object::Int(2)),
1541 (Name::from("Height"), Object::Int(2)),
1542 (Name::from("BitsPerComponent"), Object::Int(4)),
1543 (
1544 Name::from("ColorSpace"),
1545 Object::Name(Name::from("DeviceRGB")),
1546 ),
1547 (Name::from("Decode"), Object::Array(decode_array)),
1548 ],
1549 &[0xFF, 0xFF, 0xFF],
1551 );
1552 let image = decode(&s).expect("should decode");
1553 assert_eq!(
1554 image.samples.to_pixels(),
1555 Pixels::Rgb8(Box::from(&[0u8; 12][..])),
1556 "both rows are black; the absent one never reaches `/Decode`"
1557 );
1558 }
1559
1560 #[test]
1561 fn an_eight_bit_grayscale_image_round_trips() {
1562 let s = stream(
1563 vec![
1564 (Name::from("Width"), Object::Int(2)),
1565 (Name::from("Height"), Object::Int(2)),
1566 (Name::from("BitsPerComponent"), Object::Int(8)),
1567 (
1568 Name::from("ColorSpace"),
1569 Object::Name(Name::from("DeviceGray")),
1570 ),
1571 ],
1572 &[0, 85, 170, 255],
1573 );
1574 let image = decode(&s).expect("should decode");
1575 assert_eq!((image.width, image.height), (2, 2));
1576 assert_eq!(
1577 image.samples.to_pixels(),
1578 Pixels::Gray8(Box::from(&[0u8, 85, 170, 255][..]))
1579 );
1580 assert!(image.mask.is_none());
1581 }
1582
1583 const JBIG2_ALL_BLACK: [u8; 94] = [
1586 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x01, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x0d, 0xea,
1587 0x00, 0x00, 0x03, 0x53, 0x00, 0x00, 0x17, 0x11, 0x00, 0x00, 0x17, 0x11, 0x51, 0x00, 0x00,
1588 0x00, 0x00, 0x00, 0x01, 0x26, 0x00, 0x01, 0x00, 0x00, 0x00, 0x35, 0x00, 0x00, 0x0d, 0xea,
1589 0x00, 0x00, 0x03, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03,
1590 0xff, 0xfd, 0xff, 0x02, 0xfe, 0xfe, 0xfe, 0xff, 0x7f, 0x86, 0x53, 0x0f, 0xb6, 0xc9, 0x22,
1591 0xcf, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f, 0xff, 0x7f,
1592 0xff, 0x7f, 0xff, 0xac,
1593 ];
1594
1595 #[test]
1596 fn a_jbig2_image_with_a_colour_space_is_a_picture_and_not_a_stencil() {
1597 let s = stream(
1605 vec![
1606 (Name::from("Width"), Object::Int(400)),
1607 (Name::from("Height"), Object::Int(400)),
1608 (Name::from("BitsPerComponent"), Object::Int(1)),
1609 (
1610 Name::from("ColorSpace"),
1611 Object::Name(Name::from("DeviceGray")),
1612 ),
1613 (
1614 Name::from("Filter"),
1615 Object::Name(Name::from("JBIG2Decode")),
1616 ),
1617 ],
1618 &JBIG2_ALL_BLACK,
1619 );
1620 let image = decode(&s).expect("should decode");
1621 assert_eq!((image.width, image.height), (400, 400));
1622 let pixels = image.samples.to_pixels();
1623 let Pixels::Gray8(gray) = &pixels else {
1624 panic!("expected grey samples, got {pixels:?}");
1625 };
1626 assert_eq!(gray.len(), 400 * 400);
1627 assert!(
1628 gray.iter().all(|&v| v == 0),
1629 "every sample is black: JBIG2's set bit inverts to sample 0"
1630 );
1631 }
1632
1633 const JBIG2_RIGHT_HALF_BLACK: [u8; 69] = [
1638 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x01, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x08,
1639 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
1640 0x00, 0x00, 0x00, 0x01, 0x26, 0x00, 0x01, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x08,
1641 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x36,
1642 0xcd, 0xb3, 0x6c, 0xdb, 0x36, 0xcd, 0xb3, 0x6c, 0xdb,
1643 ];
1644
1645 fn jbig2_stencil(data: &[u8], decode: Option<[i64; 2]>) -> Stream {
1647 let mut pairs = vec![
1648 (Name::from("Width"), Object::Int(8)),
1649 (Name::from("Height"), Object::Int(8)),
1650 (Name::from("ImageMask"), Object::Bool(true)),
1651 (
1652 Name::from("Filter"),
1653 Object::Name(Name::from("JBIG2Decode")),
1654 ),
1655 ];
1656 if let Some([lo, hi]) = decode {
1657 pairs.push((
1658 Name::from("Decode"),
1659 Object::Array(Array::of([Object::Int(lo), Object::Int(hi)])),
1660 ));
1661 }
1662 stream(pairs, data)
1663 }
1664
1665 #[test]
1666 fn a_jbig2_stencil_takes_its_bits_from_the_codestream() {
1667 let image = decode(&jbig2_stencil(&JBIG2_RIGHT_HALF_BLACK, None)).expect("should decode");
1671 let Samples::Whole(Pixels::Stencil(BitImage {
1672 bits, row_bytes, ..
1673 })) = &image.samples
1674 else {
1675 panic!("expected a stencil, got {:?}", image.samples);
1676 };
1677 assert_eq!(*row_bytes, 1);
1678 assert_eq!(
1679 &bits[..],
1680 &[0b0000_1111u8; 8][..],
1681 "the codestream's black half is where the stencil inks"
1682 );
1683 }
1684
1685 #[test]
1686 fn a_jbig2_stencil_with_decode_one_zero_flips_every_bit() {
1687 let image =
1690 decode(&jbig2_stencil(&JBIG2_RIGHT_HALF_BLACK, Some([1, 0]))).expect("should decode");
1691 let Samples::Whole(Pixels::Stencil(BitImage { bits, .. })) = &image.samples else {
1692 panic!("expected a stencil, got {:?}", image.samples);
1693 };
1694 assert_eq!(&bits[..], &[0b1111_0000u8; 8][..]);
1695 }
1696
1697 #[test]
1698 fn a_jbig2_stencil_whose_codestream_will_not_decode_is_refused() {
1699 let mut funcs = FunctionCache::new();
1705 let mut diags = Diagnostics::default();
1706 let got = decode_image(
1707 &jbig2_stencil(b"0", None),
1708 None,
1709 None,
1710 RequestedSize::Full,
1711 &NoResolve,
1712 &mut funcs,
1713 &Limits::default(),
1714 &mut diags,
1715 );
1716 assert!(
1717 got.is_err(),
1718 "an undecodable codestream is fatal, got {got:?}"
1719 );
1720 assert!(
1721 diags.contains(&DiagKind::ImageDecodeFailed),
1722 "the refusal is recorded, not silent: {:?}",
1723 diags.entries()
1724 );
1725 }
1726
1727 #[test]
1728 fn a_stencil_mask_with_the_default_decode_is_inverted() {
1729 let s = stream(
1730 vec![
1731 (Name::from("Width"), Object::Int(8)),
1732 (Name::from("Height"), Object::Int(1)),
1733 (Name::from("ImageMask"), Object::Bool(true)),
1734 ],
1735 &[0b1010_1010],
1736 );
1737 let image = decode(&s).expect("should decode");
1738 let Samples::Whole(Pixels::Stencil(BitImage { bits, .. })) = &image.samples else {
1739 panic!("expected a stencil, got {:?}", image.samples);
1740 };
1741 assert_eq!(bits.first(), Some(&0b0101_0101));
1742 }
1743
1744 #[test]
1745 fn a_stencil_mask_with_decode_one_zero_is_copied_verbatim() {
1746 let s = stream(
1747 vec![
1748 (Name::from("Width"), Object::Int(8)),
1749 (Name::from("Height"), Object::Int(1)),
1750 (Name::from("ImageMask"), Object::Bool(true)),
1751 (
1752 Name::from("Decode"),
1753 Object::Array(Array::of([Object::Int(1), Object::Int(0)])),
1754 ),
1755 ],
1756 &[0b1010_1010],
1757 );
1758 let image = decode(&s).expect("should decode");
1759 let Samples::Whole(Pixels::Stencil(BitImage { bits, .. })) = &image.samples else {
1760 panic!("expected a stencil");
1761 };
1762 assert_eq!(bits.first(), Some(&0b1010_1010));
1763 }
1764
1765 #[test]
1766 fn a_truncated_stream_is_zero_padded_rather_than_rejected() {
1767 let s = stream(
1768 vec![
1769 (Name::from("Width"), Object::Int(2)),
1770 (Name::from("Height"), Object::Int(2)),
1771 (Name::from("BitsPerComponent"), Object::Int(8)),
1772 (
1773 Name::from("ColorSpace"),
1774 Object::Name(Name::from("DeviceGray")),
1775 ),
1776 ],
1777 &[10, 20],
1779 );
1780 let image = decode(&s).expect("should still decode");
1781 assert_eq!(
1782 image.samples.to_pixels(),
1783 Pixels::Gray8(Box::from(&[10u8, 20, 0, 0][..]))
1784 );
1785 }
1786
1787 #[test]
1788 fn an_indexed_image_keeps_its_indices_and_a_palette() {
1789 let s = stream(
1790 vec![
1791 (Name::from("Width"), Object::Int(4)),
1792 (Name::from("Height"), Object::Int(1)),
1793 (Name::from("BitsPerComponent"), Object::Int(2)),
1794 (
1795 Name::from("ColorSpace"),
1796 Object::Array(Array::of([
1797 Object::Name(Name::from("Indexed")),
1798 Object::Name(Name::from("DeviceGray")),
1799 Object::Int(3),
1800 Object::Str(pdfrum_object::PdfString::literal([0u8, 85, 170, 255])),
1801 ])),
1802 ),
1803 ],
1804 &[0b00_01_10_11],
1806 );
1807 let image = decode(&s).expect("should decode");
1808 let Samples::Whole(Pixels::Indexed { indices, palette }) = &image.samples else {
1809 panic!("expected indexed pixels, got {:?}", image.samples);
1810 };
1811 assert_eq!(&**indices, &[0, 1, 2, 3]);
1812 assert_eq!(palette.len(), 4);
1813 assert!(palette[0].r.abs() < 1e-6);
1814 assert!((palette[3].r - 1.0).abs() < 1e-6);
1815 }
1816
1817 #[test]
1818 fn a_bad_bit_depth_is_an_error_rather_than_a_repair() {
1819 let s = stream(
1820 vec![
1821 (Name::from("Width"), Object::Int(2)),
1822 (Name::from("Height"), Object::Int(2)),
1823 (Name::from("BitsPerComponent"), Object::Int(3)),
1824 (
1825 Name::from("ColorSpace"),
1826 Object::Name(Name::from("DeviceGray")),
1827 ),
1828 ],
1829 &[0; 16],
1830 );
1831 assert!(decode(&s).is_err());
1832 }
1833
1834 #[test]
1835 fn a_colour_key_mask_is_read_from_a_mask_array() {
1836 let s = stream(
1837 vec![
1838 (Name::from("Width"), Object::Int(2)),
1839 (Name::from("Height"), Object::Int(1)),
1840 (Name::from("BitsPerComponent"), Object::Int(8)),
1841 (
1842 Name::from("ColorSpace"),
1843 Object::Name(Name::from("DeviceGray")),
1844 ),
1845 (
1846 Name::from("Mask"),
1847 Object::Array(Array::of([Object::Int(0), Object::Int(10)])),
1848 ),
1849 ],
1850 &[5, 200],
1851 );
1852 let image = decode(&s).expect("should decode");
1858 let Some(super::ImageMask::Alpha { alpha, .. }) = &image.mask else {
1859 panic!("expected a resolved alpha plane, got {:?}", image.mask);
1860 };
1861 assert_eq!(&**alpha, &[0u8, 255]);
1862 let key =
1864 super::ColorKey::from_array(&Array::of([Object::Int(0), Object::Int(10)]), 1, 255);
1865 assert!(key.is_transparent(&[5]));
1866 assert!(!key.is_transparent(&[200]));
1867 }
1868
1869 #[test]
1870 fn pixel_lookup_is_bounds_checked() {
1871 let pixels = Samples::Whole(Pixels::Rgb8(Box::from(&[255u8, 0, 0, 0, 255, 0][..])));
1872 assert_eq!(sample_at(&pixels, 0, 0, 2), [255, 0, 0]);
1873 assert_eq!(sample_at(&pixels, 99, 99, 2), [0, 0, 0]);
1875 assert_eq!(pixels.components(), 3);
1876 }
1877
1878 fn codec_dict(space: &str, decode: Option<Vec<f32>>) -> super::ImageDict {
1880 let mut pairs = vec![
1881 (Name::from("Width"), Object::Int(2)),
1882 (Name::from("Height"), Object::Int(1)),
1883 (Name::from("BitsPerComponent"), Object::Int(8)),
1884 (Name::from("ColorSpace"), Object::Name(Name::from(space))),
1885 (Name::from("Filter"), Object::Name(Name::from("DCTDecode"))),
1886 ];
1887 if let Some(values) = decode {
1888 pairs.push((
1889 Name::from("Decode"),
1890 Object::Array(values.into_iter().map(Object::Real).collect()),
1891 ));
1892 }
1893 let mut diags = Diagnostics::default();
1894 super::ImageDict::load(&Dict::from_pairs(pairs), &NoResolve, &mut diags)
1895 .expect("the fixture dictionary should load")
1896 }
1897
1898 #[test]
1899 fn a_decode_array_reaches_a_codecs_output_too() {
1900 let info = codec_dict(
1904 "DeviceCMYK",
1905 Some(vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]),
1906 );
1907 assert!(!info.default_decode);
1908 let space = crate::color::ColorSpace::DeviceCmyk;
1909 let mut data = vec![255u8, 0, 0, 253, 0, 255, 255, 2];
1910 super::apply_codec_decode(&mut data, Some(&space), 4, &info);
1911 assert_eq!(data, vec![0u8, 255, 255, 2, 255, 0, 0, 253]);
1912 }
1913
1914 #[test]
1915 fn the_default_decode_leaves_a_codecs_output_untouched() {
1916 let info = codec_dict("DeviceCMYK", None);
1919 assert!(info.default_decode);
1920 let space = crate::color::ColorSpace::DeviceCmyk;
1921 let original = vec![255u8, 0, 0, 253, 1, 2, 3, 4];
1922 let mut data = original.clone();
1923 super::apply_codec_decode(&mut data, Some(&space), 4, &info);
1924 assert_eq!(data, original);
1925 let info = codec_dict(
1927 "DeviceCMYK",
1928 Some(vec![0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]),
1929 );
1930 let mut data = original.clone();
1931 super::apply_codec_decode(&mut data, Some(&space), 4, &info);
1932 assert_eq!(data, original);
1933 }
1934
1935 #[test]
1945 fn the_row_conversion_is_exactly_the_float_path() {
1946 for b in 0..=255u8 {
1951 let there = f32::from(b) / 255.0;
1952 let back = Rgb {
1953 r: there,
1954 g: there,
1955 b: there,
1956 }
1957 .to_bytes();
1958 assert_eq!(back, [b, b, b], "byte {b} does not survive the float trip");
1959 }
1960
1961 let gray = Samples::Whole(Pixels::Gray8((0..=255u8).collect()));
1963 for x in 0..256u32 {
1964 let v = u8::try_from(x).expect("x < 256");
1965 assert_eq!(sample_at(&gray, x, 0, 256), [v, v, v], "gray {x}");
1966 }
1967
1968 let rgb = Samples::Whole(Pixels::Rgb8(
1970 (0..=255u8).flat_map(|v| [v, 255 - v, v / 2]).collect(),
1971 ));
1972 for x in 0..256u32 {
1973 let v = u8::try_from(x).expect("x < 256");
1974 assert_eq!(sample_at(&rgb, x, 0, 256), [v, 255 - v, v / 2], "rgb {x}");
1975 }
1976
1977 let mut cmyk = Vec::new();
1981 let step = 17u16; for c in (0..=255u16).step_by(step as usize) {
1983 for m in (0..=255u16).step_by(step as usize) {
1984 for y in (0..=255u16).step_by(step as usize) {
1985 for k in (0..=255u16).step_by(step as usize) {
1986 cmyk.extend_from_slice(&[c as u8, m as u8, y as u8, k as u8]);
1987 }
1988 }
1989 }
1990 }
1991 let count = cmyk.len() / 4;
1992 let raw = cmyk.clone();
1993 let cmyk = Samples::Whole(Pixels::Cmyk8(cmyk.into()));
1994 let row = converted_row(&cmyk, count as u32, 0);
1998 for (x, got) in row.iter().enumerate() {
1999 let at = x * 4;
2000 let want = crate::color::ColorSpace::DeviceCmyk
2001 .to_rgb(&[
2002 f32::from(raw[at]) / 255.0,
2003 f32::from(raw[at + 1]) / 255.0,
2004 f32::from(raw[at + 2]) / 255.0,
2005 f32::from(raw[at + 3]) / 255.0,
2006 ])
2007 .to_bytes();
2008 assert_eq!(*got, want, "cmyk lattice point {x}");
2009 }
2010
2011 let palette: Box<[Rgb]> = (0..=255u8)
2014 .map(|v| Rgb {
2015 r: f32::from(v) / 255.0,
2016 g: f32::from(255 - v) / 255.0,
2017 b: 0.25,
2018 })
2019 .collect();
2020 let indexed = Samples::Whole(Pixels::Indexed {
2021 indices: (0..=255u8).collect(),
2022 palette: palette.clone(),
2023 });
2024 for x in 0..256u32 {
2025 let want = palette[x as usize].to_bytes();
2026 assert_eq!(sample_at(&indexed, x, 0, 256), want, "indexed {x}");
2027 }
2028
2029 let bits = BitImage {
2033 width: 2,
2034 height: 1,
2035 row_bytes: 1,
2036 bits: vec![0b1000_0000],
2037 };
2038 let stencil = Samples::Whole(Pixels::Stencil(bits));
2039 assert_eq!(sample_at(&stencil, 0, 0, 2), [0, 0, 0], "a set bit is ink");
2040 assert_eq!(
2041 sample_at(&stencil, 1, 0, 2),
2042 [255, 255, 255],
2043 "a clear bit is paper"
2044 );
2045
2046 for p in [&gray, &rgb, &cmyk, &indexed, &stencil] {
2049 assert_eq!(
2050 sample_at(p, 9999, 9999, 256),
2051 [0, 0, 0],
2052 "an out-of-range read is black"
2053 );
2054 }
2055 }
2056
2057 #[test]
2063 fn the_mask_planes_fast_arms_are_the_general_one() {
2064 let general = |pixels: &Samples, w: u32, h: u32| -> Vec<u8> {
2065 (0..h)
2066 .flat_map(|y| (0..w).map(move |x| (x, y)))
2067 .map(|(x, y)| sample_at(pixels, x, y, w)[0])
2068 .collect()
2069 };
2070
2071 for (w, h, len) in [(4_u32, 3_u32, 12_usize), (4, 3, 7), (4, 3, 20), (1, 1, 1)] {
2073 let data: Box<[u8]> = (0..len).map(|i| (i * 31 % 256) as u8).collect();
2074 let pixels = Samples::Whole(Pixels::Gray8(data));
2075 let got = super::mask_plane(&pixels, w, h).expect("dimensions multiply");
2076 let mut want = general(&pixels, w, h);
2077 want.resize((w * h) as usize, 0);
2078 assert_eq!(&got[..], &want[..], "gray {w}x{h}, {len} bytes");
2079 }
2080
2081 let palette: Box<[Rgb]> = (0..=255u8)
2083 .map(|v| Rgb {
2084 r: f32::from(v) / 255.0,
2085 g: 0.5,
2086 b: 0.25,
2087 })
2088 .collect();
2089 for (w, h, len) in [(8_u32, 4_u32, 32_usize), (8, 4, 10)] {
2090 let pixels = Samples::Whole(Pixels::Indexed {
2091 indices: (0..len).map(|i| (i * 7 % 256) as u8).collect(),
2092 palette: palette.clone(),
2093 });
2094 let got = super::mask_plane(&pixels, w, h).expect("dimensions multiply");
2095 let mut want = general(&pixels, w, h);
2096 want.resize((w * h) as usize, 0);
2097 assert_eq!(&got[..], &want[..], "indexed {w}x{h}, {len} indices");
2098 }
2099
2100 let rgb = Samples::Whole(Pixels::Rgb8((0..24u8).collect()));
2103 let got = super::mask_plane(&rgb, 4, 2).expect("dimensions multiply");
2104 assert_eq!(&got[..], &general(&rgb, 4, 2)[..]);
2105 }
2106
2107 #[test]
2110 fn a_gigapixel_image_is_not_a_workable_area() {
2111 assert!(
2112 super::image_area_is_workable(20_000, 28_000),
2113 "A0 at 600dpi"
2114 );
2115 assert!(!super::image_area_is_workable(65_536, 65_536), "4.3 Gpx");
2116 assert!(!super::image_area_is_workable(131_071, 131_071), "17 Gpx");
2117 assert!(super::image_area_is_workable(2, 2));
2118 }
2119
2120 #[test]
2130 fn a_mask_plane_too_large_to_allocate_is_dropped_rather_than_aborting() {
2131 let pixels = Samples::Whole(Pixels::Gray8(Box::new([0u8; 4])));
2132 assert_eq!(super::mask_plane(&pixels, 131_071, 131_071), None);
2133 assert_eq!(super::mask_plane(&pixels, 65_536, 65_536), None);
2137 assert!(super::mask_plane(&pixels, 2, 2).is_some());
2139 }
2140
2141 #[test]
2142 fn the_decode_table_is_exhaustively_the_per_sample_arithmetic() {
2143 let arrays: [Vec<f32>; 4] = [
2151 vec![1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0],
2152 vec![0.0, 0.5, 0.25, 1.0, 0.1, 0.9, 0.0, 1.0],
2153 vec![-1.0, 2.0, 2.0, -1.0, -0.5, 1.5, 1.5, -0.5],
2154 vec![0.3, 0.3, 0.0, 1.0, 1.0, 0.0, 0.7, 0.2],
2155 ];
2156 let space = crate::color::ColorSpace::DeviceCmyk;
2157 for values in arrays {
2158 let info = codec_dict("DeviceCMYK", Some(values));
2159 let decode = super::DecodeMap::new(Some(&space), 4, 8, info.decode.as_ref());
2160 let table = super::decode_table(&decode, 4);
2161 for (component, row) in table.iter().enumerate() {
2162 for raw in 0..=255u8 {
2163 let value = decode.apply(component, f32::from(raw));
2164 let expected = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
2165 assert_eq!(
2166 row[usize::from(raw)],
2167 expected,
2168 "component {component}, raw {raw}"
2169 );
2170 }
2171 }
2172 assert_eq!(table.len(), 4, "one row per component");
2173 }
2174 }
2175
2176 #[test]
2177 fn a_trailing_partial_pixel_maps_by_its_position_in_the_pixel() {
2178 let info = codec_dict(
2185 "DeviceCMYK",
2186 Some(vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0]),
2187 );
2188 let space = crate::color::ColorSpace::DeviceCmyk;
2189 let mut data = vec![10u8, 20, 30, 40, 50, 60];
2190 super::apply_codec_decode(&mut data, Some(&space), 4, &info);
2191 assert_eq!(data, vec![245u8, 20, 225, 40, 205, 60]);
2193 }
2194
2195 #[test]
2196 fn a_codec_decode_needs_a_space_and_some_components() {
2197 let info = codec_dict("DeviceGray", Some(vec![1.0, 0.0]));
2198 let original = vec![10u8, 200];
2199 let mut data = original.clone();
2201 super::apply_codec_decode(&mut data, None, 1, &info);
2202 assert_eq!(data, original);
2203 let mut data = original.clone();
2204 let gray = crate::color::ColorSpace::DeviceGray;
2205 super::apply_codec_decode(&mut data, Some(&gray), 0, &info);
2206 assert_eq!(data, original);
2207 let mut data = original.clone();
2209 super::apply_codec_decode(&mut data, Some(&gray), 1, &info);
2210 assert_eq!(data, vec![245u8, 55]);
2211 }
2212
2213 fn all_white_g4(rows: usize) -> Vec<u8> {
2219 let mut byte = 0u8;
2220 for i in 0..rows.min(8) {
2221 byte |= 1 << (7 - i);
2222 }
2223 vec![byte]
2224 }
2225
2226 fn black_then_white_g4(rows: usize) -> Vec<u8> {
2233 let mut bits = String::from("001001101010001011");
2234 for _ in 1..rows {
2235 bits.push('1');
2236 }
2237 while bits.len() % 8 != 0 {
2238 bits.push('0');
2239 }
2240 bits.as_bytes()
2241 .chunks(8)
2242 .filter_map(|c| {
2243 let s = std::str::from_utf8(c).ok()?;
2244 u8::from_str_radix(s, 2).ok()
2245 })
2246 .collect()
2247 }
2248
2249 fn ccitt_stream(width: i64, height: i64, mask: bool, data: &[u8]) -> Stream {
2250 let parms = Dict::from_pairs(vec![
2251 (Name::from("K"), Object::Int(-1)),
2252 (Name::from("Columns"), Object::Int(width)),
2253 (Name::from("Rows"), Object::Int(height)),
2254 ]);
2255 let mut pairs = vec![
2256 (Name::from("Width"), Object::Int(width)),
2257 (Name::from("Height"), Object::Int(height)),
2258 (Name::from("BitsPerComponent"), Object::Int(1)),
2259 (
2260 Name::from("Filter"),
2261 Object::Name(Name::from("CCITTFaxDecode")),
2262 ),
2263 (Name::from("DecodeParms"), Object::Dict(parms)),
2264 ];
2265 if mask {
2266 pairs.push((Name::from("ImageMask"), Object::Bool(true)));
2267 } else {
2268 pairs.push((
2269 Name::from("ColorSpace"),
2270 Object::Name(Name::from("DeviceGray")),
2271 ));
2272 }
2273 stream(pairs, data)
2274 }
2275
2276 #[test]
2277 fn a_fax_image_reaches_the_decoder_at_all() {
2278 let s = ccitt_stream(20, 3, false, &all_white_g4(3));
2285 let image = decode(&s).expect("should decode");
2286 assert_eq!((image.width, image.height), (20, 3));
2287 for y in 0..3 {
2288 for x in 0..20 {
2289 assert_eq!(
2290 sample_at(&image.samples, x, y, 20),
2291 [255, 255, 255],
2292 "({x},{y}) should be white"
2293 );
2294 }
2295 }
2296 }
2297
2298 #[test]
2299 fn a_fax_row_is_repacked_from_four_byte_padding_to_the_images_pitch() {
2300 let s = ccitt_stream(20, 3, false, &black_then_white_g4(3));
2313 let image = decode(&s).expect("should decode");
2314 let black = [0_u8, 0, 0];
2315 let white = [255_u8, 255, 255];
2316 for y in 0..3 {
2317 for x in 0..20 {
2318 let want = if y < 2 && x < 8 { black } else { white };
2319 assert_eq!(
2320 sample_at(&image.samples, x, y, 20),
2321 want,
2322 "({x},{y}) — a shear puts the black run somewhere else"
2323 );
2324 }
2325 }
2326 }
2327
2328 #[test]
2329 fn a_fax_stream_that_will_not_decode_leaves_the_image_white() {
2330 let s = ccitt_stream(20, 3, false, &[0x00, 0x00]);
2333 let image = decode(&s).expect("damage is not a failure");
2334 assert_eq!(sample_at(&image.samples, 0, 0, 20), [255, 255, 255]);
2335 }
2336
2337 fn separation_cmyk(c1: [f32; 4]) -> Object {
2340 let mut c0 = Array::default();
2341 for _ in 0..4 {
2342 c0.push(Object::Real(0.0));
2343 }
2344 let mut c1_arr = Array::default();
2345 for v in c1 {
2346 c1_arr.push(Object::Real(v));
2347 }
2348 let mut domain = Array::default();
2349 domain.push(Object::Int(0));
2350 domain.push(Object::Int(1));
2351 let mut range = Array::default();
2352 for _ in 0..4 {
2353 range.push(Object::Int(0));
2354 range.push(Object::Int(1));
2355 }
2356 let tint = Dict::from_pairs(vec![
2357 (Name::from("FunctionType"), Object::Int(2)),
2358 (Name::from("N"), Object::Real(1.0)),
2359 (Name::from("Domain"), Object::Array(domain)),
2360 (Name::from("Range"), Object::Array(range)),
2361 (Name::from("C0"), Object::Array(c0)),
2362 (Name::from("C1"), Object::Array(c1_arr)),
2363 ]);
2364 let mut space = Array::default();
2365 space.push(Object::Name(Name::from("Separation")));
2366 space.push(Object::Name(Name::from("Spot")));
2367 space.push(Object::Name(Name::from("DeviceCMYK")));
2368 space.push(Object::Dict(tint));
2369 Object::Array(space)
2370 }
2371
2372 #[test]
2373 fn a_separation_image_runs_its_samples_through_the_tint_transform() {
2374 let s = stream(
2385 vec![
2386 (Name::from("Width"), Object::Int(1)),
2387 (Name::from("Height"), Object::Int(1)),
2388 (Name::from("BitsPerComponent"), Object::Int(8)),
2389 (
2390 Name::from("ColorSpace"),
2391 separation_cmyk([1.0, 0.0, 0.600_006, 0.0]),
2392 ),
2393 ],
2394 &[0xC6],
2395 );
2396 let image = decode(&s).expect("should decode");
2397 assert_eq!(
2398 sample_at(&image.samples, 0, 0, 1),
2399 [0, 182, 162],
2400 "the tint must reach the alternate space, not the page as grey"
2401 );
2402 let Samples::Whole(Pixels::Indexed { palette, .. }) = &image.samples else {
2406 panic!(
2407 "a resolved Separation image is a palette, got {:?}",
2408 image.samples
2409 );
2410 };
2411 assert_eq!(palette.len(), 256);
2412 assert_eq!(palette[0].to_bytes(), [255, 255, 255]);
2414 }
2415
2416 #[test]
2417 fn a_separation_decode_array_is_folded_into_the_palette() {
2418 let mut decode_arr = Array::default();
2423 decode_arr.push(Object::Int(1));
2424 decode_arr.push(Object::Int(0));
2425 let s = stream(
2426 vec![
2427 (Name::from("Width"), Object::Int(1)),
2428 (Name::from("Height"), Object::Int(1)),
2429 (Name::from("BitsPerComponent"), Object::Int(8)),
2430 (
2431 Name::from("ColorSpace"),
2432 separation_cmyk([1.0, 0.0, 0.600_006, 0.0]),
2433 ),
2434 (Name::from("Decode"), Object::Array(decode_arr)),
2435 ],
2436 &[0xC6],
2437 );
2438 let image = decode(&s).expect("should decode");
2439 let inverted = sample_at(&image.samples, 0, 0, 1);
2440 let s_plain = stream(
2441 vec![
2442 (Name::from("Width"), Object::Int(1)),
2443 (Name::from("Height"), Object::Int(1)),
2444 (Name::from("BitsPerComponent"), Object::Int(8)),
2445 (
2446 Name::from("ColorSpace"),
2447 separation_cmyk([1.0, 0.0, 0.600_006, 0.0]),
2448 ),
2449 ],
2450 &[255 - 0xC6],
2451 );
2452 let plain = decode(&s_plain).expect("should decode");
2453 assert_eq!(
2454 inverted,
2455 sample_at(&plain.samples, 0, 0, 1),
2456 "`/Decode [1 0]` on a tint is the complement of the sample"
2457 );
2458 }
2459
2460 #[test]
2461 fn a_devicen_image_converts_per_pixel_rather_than_through_a_palette() {
2462 let mut names = Array::default();
2469 names.push(Object::Name(Name::from("SpotA")));
2470 names.push(Object::Name(Name::from("SpotB")));
2471 let mut domain = Array::default();
2472 for _ in 0..2 {
2473 domain.push(Object::Int(0));
2474 domain.push(Object::Int(1));
2475 }
2476 let mut range = Array::default();
2477 for _ in 0..4 {
2478 range.push(Object::Int(0));
2479 range.push(Object::Int(1));
2480 }
2481 let mut c0 = Array::default();
2482 let mut c1 = Array::default();
2483 for _ in 0..4 {
2484 c0.push(Object::Real(0.0));
2485 }
2486 for v in [0.0_f32, 1.0, 1.0, 0.0] {
2487 c1.push(Object::Real(v));
2488 }
2489 let tint = Dict::from_pairs(vec![
2490 (Name::from("FunctionType"), Object::Int(2)),
2491 (Name::from("N"), Object::Real(1.0)),
2492 (Name::from("Domain"), Object::Array(domain)),
2493 (Name::from("Range"), Object::Array(range)),
2494 (Name::from("C0"), Object::Array(c0)),
2495 (Name::from("C1"), Object::Array(c1)),
2496 ]);
2497 let mut space = Array::default();
2498 space.push(Object::Name(Name::from("DeviceN")));
2499 space.push(Object::Array(names));
2500 space.push(Object::Name(Name::from("DeviceCMYK")));
2501 space.push(Object::Dict(tint));
2502 let s = stream(
2503 vec![
2504 (Name::from("Width"), Object::Int(2)),
2505 (Name::from("Height"), Object::Int(1)),
2506 (Name::from("BitsPerComponent"), Object::Int(8)),
2507 (Name::from("ColorSpace"), Object::Array(space)),
2508 ],
2509 &[0x00, 0x00, 0xFF, 0xFF],
2510 );
2511 let image = decode(&s).expect("should decode");
2512 assert!(
2513 matches!(image.samples, Samples::Whole(Pixels::Rgb8(_))),
2514 "a two-colorant DeviceN resolves per pixel and cannot stay packed, \
2515 got {:?}",
2516 image.samples
2517 );
2518 assert_eq!(sample_at(&image.samples, 0, 0, 2), [255, 255, 255]);
2522 let full = sample_at(&image.samples, 1, 0, 2);
2523 assert!(
2524 full[0] > 200 && full[1] < 80 && full[2] < 80,
2525 "a full tint must reach the alternate space's red, got {full:?}"
2526 );
2527 }
2528}