1use crate::names;
20use kurbo::{Affine, Rect};
21use pdfrum_common::{DiagKind, Diagnostics, Limits, Severity};
22use pdfrum_object::{ByteSpan, Dict, Resolve, Stream};
23
24#[derive(Debug, Clone, PartialEq)]
26pub struct TilingPattern {
27 pub colored: bool,
31 pub x_step: f32,
33 pub y_step: f32,
35 pub bbox: Rect,
38 pub matrix: Affine,
40 pub resources: Option<Dict>,
42 pub content: ByteSpan,
44 pub objects: Vec<crate::page::PageObject>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct TileRange {
59 pub min_col: i32,
61 pub max_col: i32,
63 pub min_row: i32,
65 pub max_row: i32,
67}
68
69impl TilingPattern {
70 pub(super) fn load<R: Resolve>(
72 stream: &Stream,
73 matrix: Affine,
74 r: &R,
75 limits: &Limits,
76 diags: &mut Diagnostics,
77 ) -> Self {
78 let _ = (limits, diags);
79 let dict = &stream.dict;
80 Self {
81 colored: dict.int(names::PAINT_TYPE, r) == Some(1),
82 x_step: dict.number(names::X_STEP, r).unwrap_or(0.0).abs(),
84 y_step: dict.number(names::Y_STEP, r).unwrap_or(0.0).abs(),
85 bbox: dict
87 .array(names::BBOX, r)
88 .filter(|a| a.len() == 4)
89 .map_or(Rect::ZERO, |a| a.as_rect()),
90 matrix,
91 resources: dict.dict(names::RESOURCES, r),
92 content: stream.data.clone(),
93 objects: Vec::new(),
95 }
96 }
97
98 #[must_use]
103 pub fn steps_are_drawable(&self) -> bool {
104 self.x_step.is_finite()
105 && self.y_step.is_finite()
106 && self.x_step != 0.0
107 && self.y_step != 0.0
108 }
109
110 #[must_use]
115 pub fn tile_range(&self, clip: Rect, diags: &mut Diagnostics) -> Option<TileRange> {
116 if !self.steps_are_drawable() {
117 diags.record(Severity::Suspicious, DiagKind::TilingStepInvalid, None);
118 return None;
119 }
120 let x_step = f64::from(self.x_step);
121 let y_step = f64::from(self.y_step);
122 let to_i32 = |v: f64| -> Option<i32> {
125 if !v.is_finite() || v < f64::from(i32::MIN) || v > f64::from(i32::MAX) {
126 return None;
127 }
128 #[expect(
129 clippy::cast_possible_truncation,
130 reason = "the range check above is the C++'s checked conversion"
131 )]
132 Some(v as i32)
133 };
134 let range = (|| {
135 Some(TileRange {
136 min_col: to_i32(((clip.x0 - self.bbox.x1) / x_step).ceil())?,
137 max_col: to_i32(((clip.x1 - self.bbox.x0) / x_step).floor())?,
138 min_row: to_i32(((clip.y0 - self.bbox.y1) / y_step).ceil())?,
139 max_row: to_i32(((clip.y1 - self.bbox.y0) / y_step).floor())?,
140 })
141 })();
142 if range.is_none() {
143 diags.record(Severity::Suspicious, DiagKind::TilingRangeOverflow, None);
144 }
145 range
146 }
147
148 #[must_use]
163 pub fn cell_size(&self, to_device: Affine) -> Option<(i32, i32)> {
164 let cell = (to_device * self.matrix).transform_rect_bbox(self.bbox);
165 #[expect(
166 clippy::cast_possible_truncation,
167 reason = "the narrowing is the point: `CFX_FloatRect` is `float`"
168 )]
169 let narrow = |edge: f64| f64::from(edge as f32);
170 let width = narrow(cell.width()).ceil();
171 let height = narrow(cell.height()).ceil();
172 if !width.is_finite()
173 || !height.is_finite()
174 || width > f64::from(i32::MAX)
175 || height > f64::from(i32::MAX)
176 {
177 return None;
178 }
179 #[expect(
180 clippy::cast_possible_truncation,
181 reason = "the range check above is the C++'s checked conversion"
182 )]
183 let (w, h) = (width as i32, height as i32);
184 Some((w.max(1), h.max(1)))
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 #![allow(
194 clippy::unreadable_literal,
195 clippy::float_cmp,
196 clippy::indexing_slicing,
197 clippy::cast_precision_loss,
198 clippy::cast_possible_truncation,
199 reason = "test fixtures quote oracle vectors verbatim and compare exactly"
200 )]
201
202 use super::TilingPattern;
203 use kurbo::{Affine, Rect};
204 use pdfrum_common::{DiagKind, Diagnostics};
205 use pdfrum_object::ByteSpan;
206
207 fn pattern(x_step: f32, y_step: f32) -> TilingPattern {
208 TilingPattern {
209 colored: true,
210 x_step,
211 y_step,
212 bbox: Rect::new(0.0, 0.0, 10.0, 10.0),
213 matrix: Affine::IDENTITY,
214 resources: None,
215 content: ByteSpan::empty(),
216 objects: Vec::new(),
217 }
218 }
219
220 #[test]
221 fn zero_and_non_finite_steps_draw_nothing() {
222 let mut diags = Diagnostics::default();
223 assert!(!pattern(0.0, 10.0).steps_are_drawable());
224 assert!(!pattern(10.0, 0.0).steps_are_drawable());
225 assert!(!pattern(f32::NAN, 10.0).steps_are_drawable());
226 assert!(!pattern(f32::INFINITY, 10.0).steps_are_drawable());
227 assert!(pattern(10.0, 10.0).steps_are_drawable());
228 assert!(
229 pattern(0.0, 10.0)
230 .tile_range(Rect::new(0.0, 0.0, 100.0, 100.0), &mut diags)
231 .is_none()
232 );
233 assert!(diags.contains(&DiagKind::TilingStepInvalid));
234 }
235
236 #[test]
237 fn a_tiny_step_aborts_rather_than_asking_for_endless_tiles() {
238 let mut diags = Diagnostics::default();
239 let p = pattern(1e-30, 1e-30);
240 assert!(
241 p.tile_range(Rect::new(0.0, 0.0, 100.0, 100.0), &mut diags)
242 .is_none()
243 );
244 assert!(diags.contains(&DiagKind::TilingRangeOverflow));
245 }
246
247 #[test]
248 fn a_reasonable_step_covers_the_clip() {
249 let mut diags = Diagnostics::default();
250 let p = pattern(10.0, 10.0);
251 let range = p
252 .tile_range(Rect::new(0.0, 0.0, 100.0, 100.0), &mut diags)
253 .expect("a tile range");
254 assert!(range.min_col <= 0);
255 assert!(range.max_col >= 9);
256 assert!(diags.is_empty());
257 }
258
259 #[test]
260 fn a_degenerate_bbox_still_yields_a_one_pixel_cell() {
261 let p = TilingPattern {
262 bbox: Rect::ZERO,
263 ..pattern(10.0, 10.0)
264 };
265 assert_eq!(p.cell_size(Affine::IDENTITY), Some((1, 1)));
266 let p = TilingPattern {
268 bbox: Rect::new(10.0, 10.0, 0.0, 0.0),
269 ..pattern(10.0, 10.0)
270 };
271 let (w, h) = p.cell_size(Affine::IDENTITY).expect("a cell");
272 assert!(w >= 1 && h >= 1);
273 }
274
275 #[test]
276 fn a_scale_that_is_whole_only_in_single_precision_gives_a_whole_cell() {
277 let scale = f64::from(0.4f32);
281 let p = TilingPattern {
282 bbox: Rect::new(0.0, 0.0, 100.0, 100.0),
283 ..pattern(100.0, 100.0)
284 };
285 assert_eq!(
286 p.cell_size(Affine::scale(scale)),
287 Some((40, 40)),
288 "the ceiling must not see the widening error"
289 );
290 }
291
292 #[test]
293 fn an_enormous_cell_aborts() {
294 let p = TilingPattern {
295 bbox: Rect::new(0.0, 0.0, 1e30, 1e30),
296 ..pattern(10.0, 10.0)
297 };
298 assert!(p.cell_size(Affine::IDENTITY).is_none());
299 }
300}