1use crate::color::{ColorSpace, Rgb};
21use pdfrum_object::Array;
22
23#[derive(Debug, Clone, PartialEq, Default)]
26pub struct ColorKey {
27 pub ranges: Box<[(u32, u32)]>,
29}
30
31impl ColorKey {
32 #[must_use]
40 pub fn from_array(array: &Array, components: usize, max_data: u32) -> Self {
41 let complete = array.len() >= components * 2;
42 let ranges = (0..components)
43 .map(|i| {
44 if !complete {
45 return (0u32, 0u32);
46 }
47 let lo = array.int_at(i * 2).unwrap_or(0).max(0);
48 let hi = array
49 .int_at(i * 2 + 1)
50 .unwrap_or(0)
51 .clamp(0, i64::from(max_data));
52 (
53 u32::try_from(lo).unwrap_or(0),
54 u32::try_from(hi).unwrap_or(0),
55 )
56 })
57 .collect();
58 Self { ranges }
59 }
60
61 #[must_use]
63 pub fn is_complete(array: &Array, components: usize) -> bool {
64 array.len() >= components * 2
65 }
66
67 #[must_use]
70 pub fn is_transparent(&self, samples: &[u32]) -> bool {
71 if self.ranges.is_empty() {
72 return false;
73 }
74 self.ranges.iter().enumerate().all(|(i, (lo, hi))| {
75 let v = samples.get(i).copied().unwrap_or(0);
76 v >= *lo && v <= *hi
77 })
78 }
79}
80
81#[derive(Debug, Clone, PartialEq)]
83#[non_exhaustive]
84pub enum ImageMask {
85 ColorKey(ColorKey),
87 Alpha {
91 width: u32,
93 height: u32,
95 alpha: Box<[u8]>,
97 stencil: bool,
100 },
101}
102
103impl ImageMask {
104 #[must_use]
106 pub fn alpha_at(&self, x: u32, y: u32) -> u8 {
107 match self {
108 Self::ColorKey(_) => 255,
109 Self::Alpha {
110 width,
111 height,
112 alpha,
113 stencil,
114 } => {
115 if x >= *width || y >= *height {
116 return 255;
117 }
118 let index = usize::try_from(y)
119 .ok()
120 .and_then(|row| row.checked_mul(usize::try_from(*width).ok()?))
121 .and_then(|base| base.checked_add(usize::try_from(x).ok()?));
122 let v = index.and_then(|i| alpha.get(i)).copied().unwrap_or(255);
123 if *stencil { 255 - v } else { v }
124 }
125 }
126 }
127}
128
129#[must_use]
134pub fn matte_color(
135 matte: Option<&Array>,
136 space: Option<&ColorSpace>,
137 components: usize,
138) -> Option<Rgb> {
139 let matte = matte?;
140 let space = space?;
141 if matches!(space, ColorSpace::Pattern(_)) {
142 return None;
143 }
144 if matte.len() != components {
146 return None;
147 }
148 if space.n_components() > components {
149 return None;
150 }
151 let comps: Vec<f32> = (0..components)
152 .map(|i| matte.number_at_or_zero(i))
153 .collect();
154 Some(space.to_rgb(&comps))
155}
156
157#[cfg(test)]
158mod tests {
159 #![allow(
163 clippy::unreadable_literal,
164 clippy::float_cmp,
165 clippy::indexing_slicing,
166 clippy::cast_precision_loss,
167 clippy::cast_possible_truncation,
168 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
169 )]
170
171 use super::{ColorKey, ImageMask, matte_color};
172 use crate::color::ColorSpace;
173 use pdfrum_object::{Array, Object};
174
175 fn array(values: &[i64]) -> Array {
176 Array::of(values.iter().copied().map(Object::Int))
177 }
178
179 #[test]
180 fn a_colour_key_needs_every_component_inside_its_range() {
181 let key = ColorKey::from_array(&array(&[10, 20, 30, 40, 50, 60]), 3, 255);
182 assert!(key.is_transparent(&[15, 35, 55]));
183 assert!(!key.is_transparent(&[15, 35, 61]));
185 assert!(!key.is_transparent(&[9, 35, 55]));
186 assert!(key.is_transparent(&[10, 30, 50]));
188 assert!(key.is_transparent(&[20, 40, 60]));
189 }
190
191 #[test]
192 fn a_short_array_still_masks_but_only_exactly_zero_samples() {
193 let short = array(&[10, 20]);
194 assert!(!ColorKey::is_complete(&short, 3));
195 let key = ColorKey::from_array(&short, 3, 255);
196 assert!(key.is_transparent(&[0, 0, 0]));
197 assert!(!key.is_transparent(&[15, 0, 0]));
198 }
199
200 #[test]
201 fn colour_key_bounds_are_clamped_to_the_sample_range() {
202 let key = ColorKey::from_array(&array(&[-5, 999]), 1, 255);
203 assert_eq!(key.ranges.first(), Some(&(0, 255)));
204 }
205
206 #[test]
207 fn an_alpha_mask_reads_out_of_bounds_as_opaque() {
208 let mask = ImageMask::Alpha {
209 width: 2,
210 height: 2,
211 alpha: Box::from(&[0u8, 64, 128, 255][..]),
212 stencil: false,
213 };
214 assert_eq!(mask.alpha_at(0, 0), 0);
215 assert_eq!(mask.alpha_at(1, 1), 255);
216 assert_eq!(mask.alpha_at(5, 5), 255);
217 }
218
219 #[test]
220 fn a_stencil_mask_inverts_its_sense() {
221 let mask = ImageMask::Alpha {
222 width: 1,
223 height: 1,
224 alpha: Box::from(&[0u8][..]),
225 stencil: true,
226 };
227 assert_eq!(mask.alpha_at(0, 0), 255);
228 }
229
230 #[test]
231 fn matte_needs_all_four_preconditions() {
232 let rgb = ColorSpace::DeviceRgb;
233 let three = array(&[0, 0, 0]);
234 assert!(matte_color(Some(&three), Some(&rgb), 3).is_some());
235 assert!(matte_color(Some(&array(&[0, 0])), Some(&rgb), 3).is_none());
237 assert!(matte_color(Some(&array(&[0, 0, 0, 0])), Some(&rgb), 3).is_none());
238 assert!(matte_color(Some(&three), None, 3).is_none());
240 let pattern = ColorSpace::Pattern(Box::default());
242 assert!(matte_color(Some(&three), Some(&pattern), 3).is_none());
243 assert!(matte_color(Some(&array(&[0])), Some(&rgb), 1).is_none());
245 assert!(matte_color(None, Some(&rgb), 3).is_none());
247 }
248}