1#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub struct Options {
25 pub bilinear: bool,
28 pub no_smoothing: bool,
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35pub struct Rect {
36 pub left: i32,
37 pub top: i32,
38 pub right: i32,
39 pub bottom: i32,
40}
41
42impl Rect {
43 pub fn new(left: i32, top: i32, right: i32, bottom: i32) -> Self {
44 Rect {
45 left,
46 top,
47 right,
48 bottom,
49 }
50 }
51
52 pub fn width(&self) -> i32 {
53 self.right - self.left
54 }
55
56 pub fn height(&self) -> i32 {
57 self.bottom - self.top
58 }
59
60 pub fn is_empty(&self) -> bool {
61 self.left >= self.right || self.top >= self.bottom
62 }
63
64 pub fn normalize(&mut self) {
65 if self.left > self.right {
66 std::mem::swap(&mut self.left, &mut self.right);
67 }
68 if self.top > self.bottom {
69 std::mem::swap(&mut self.top, &mut self.bottom);
70 }
71 }
72
73 pub fn intersect(&mut self, other: &Rect) {
75 let mut o = *other;
76 o.normalize();
77 self.normalize();
78 self.left = self.left.max(o.left);
79 self.top = self.top.max(o.top);
80 self.right = self.right.min(o.right);
81 self.bottom = self.bottom.min(o.bottom);
82 if self.left > self.right || self.top > self.bottom {
83 *self = Rect::default();
84 }
85 }
86
87 pub fn offset(&mut self, dx: i32, dy: i32) {
88 self.left += dx;
89 self.right += dx;
90 self.top += dy;
91 self.bottom += dy;
92 }
93
94 pub fn swapped_clip_box(&self, width: i32, height: i32, flip_x: bool, flip_y: bool) -> Rect {
98 let mut r = Rect::default();
99 if flip_y {
100 r.left = height - self.top;
101 r.right = height - self.bottom;
102 } else {
103 r.left = self.top;
104 r.right = self.bottom;
105 }
106 if flip_x {
107 r.top = width - self.left;
108 r.bottom = width - self.right;
109 } else {
110 r.top = self.left;
111 r.bottom = self.right;
112 }
113 r.normalize();
114 r
115 }
116}
117
118#[derive(Clone, Copy, Debug)]
120pub enum Source<'a> {
121 Bilevel { data: &'a [u8], stride: usize },
127 Gray8 { data: &'a [u8], stride: usize },
129 Rgb8 { data: &'a [u8], stride: usize },
132}
133
134impl Source<'_> {
135 fn channels(&self) -> usize {
136 match self {
137 Source::Bilevel { .. } | Source::Gray8 { .. } => 1,
138 Source::Rgb8 { .. } => 3,
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
146pub struct Stretched {
147 pub width: usize,
148 pub height: usize,
149 pub channels: usize,
150 pub data: Vec<u8>,
151}
152
153const FIXED_BITS: u32 = 16;
154const FIXED_ONE: u32 = 1 << FIXED_BITS;
155
156fn fixed_from_double(d: f64) -> u32 {
158 (d * f64::from(FIXED_ONE)).round() as i64 as u32
159}
160
161fn pixel_from_fixed(fixed: u32) -> u8 {
163 (fixed >> FIXED_BITS) as u8
164}
165
166struct PixelWeight {
169 src_start: i32,
170 src_end: i32,
171 weights: Vec<u32>,
172}
173
174impl PixelWeight {
175 fn taps(&self) -> &[u32] {
176 if self.src_end < self.src_start {
177 &[]
178 } else {
179 &self.weights[..(self.src_end - self.src_start + 1) as usize]
180 }
181 }
182}
183
184fn calculate_weights(
186 dest_len: i32,
187 dest_min: i32,
188 dest_max: i32,
189 src_len: i32,
190 src_min: i32,
191 src_max: i32,
192 options: Options,
193) -> Option<Vec<PixelWeight>> {
194 let bilinear = options.bilinear;
195 if dest_len == 0 {
196 return Some(Vec::new());
197 }
198 if dest_min > dest_max {
199 return None;
200 }
201 let scale = f64::from(src_len) / f64::from(dest_len);
202 let base = if dest_len < 0 {
203 f64::from(src_len)
204 } else {
205 0.0
206 };
207 let weight_count = (scale.abs().ceil() as usize) + 1;
208 let mut table = Vec::with_capacity((dest_max - dest_min) as usize);
209 if options.no_smoothing || scale.abs() < 1.0 {
210 for dest_pixel in dest_min..dest_max {
211 let src_pos = f64::from(dest_pixel) * scale + scale / 2.0 + base;
212 let mut weights = vec![0u32; weight_count.max(2)];
213 let (src_start, src_end);
214 if bilinear {
215 let mut start = (src_pos - 0.5).floor() as i32;
216 let mut end = (src_pos + 0.5).floor() as i32;
217 start = start.max(src_min);
218 end = end.min(src_max - 1);
219 src_start = start;
220 src_end = end;
221 if start >= end {
222 weights[0] = FIXED_ONE;
223 } else {
224 let second = fixed_from_double(src_pos - f64::from(start) - 0.5);
225 weights[0] = FIXED_ONE.wrapping_sub(second);
226 weights[1] = second;
227 }
228 } else {
229 let pixel_pos = src_pos.floor() as i32;
230 src_start = pixel_pos.max(src_min);
231 src_end = pixel_pos.min(src_max - 1);
232 weights[0] = FIXED_ONE;
233 }
234 table.push(PixelWeight {
235 src_start,
236 src_end,
237 weights,
238 });
239 }
240 return Some(table);
241 }
242
243 for dest_pixel in dest_min..dest_max {
244 let src_start = f64::from(dest_pixel) * scale + base;
245 let src_end = src_start + scale;
246 let mut start_i = src_start.min(src_end).floor() as i32;
247 let mut end_i = src_start.max(src_end).floor() as i32;
248 start_i = start_i.max(src_min);
249 end_i = end_i.min(src_max - 1);
250 if start_i > end_i {
251 start_i = start_i.clamp(0, (src_max - 1).max(0));
252 table.push(PixelWeight {
253 src_start: start_i,
254 src_end: start_i,
255 weights: vec![0; weight_count.max(1)],
256 });
257 continue;
258 }
259 let mut weights = vec![0u32; ((end_i - start_i) as usize + 1).max(weight_count)];
260 let mut remaining = FIXED_ONE;
261 let mut rounding_error = 0.0f64;
262 for j in start_i..end_i {
263 let mut dest_start = (f64::from(j) - base) / scale;
264 let mut dest_end = (f64::from(j + 1) - base) / scale;
265 if dest_start > dest_end {
266 std::mem::swap(&mut dest_start, &mut dest_end);
267 }
268 let area_start = dest_start.max(f64::from(dest_pixel));
269 let area_end = dest_end.min(f64::from(dest_pixel + 1));
270 let weight = (area_end - area_start).max(0.0);
271 let fixed_weight = fixed_from_double(weight + rounding_error);
272 weights[(j - start_i) as usize] = fixed_weight;
273 remaining = remaining.wrapping_sub(fixed_weight);
274 rounding_error = weight - f64::from(fixed_weight) / f64::from(FIXED_ONE);
275 }
276 let mut src_end = end_i;
277 if remaining != 0 && remaining <= FIXED_ONE {
280 weights[(end_i - start_i) as usize] = remaining;
281 } else {
282 if src_end <= start_i {
283 return None;
284 }
285 src_end -= 1;
286 let idx = (src_end - start_i) as usize;
287 weights[idx] = weights[idx].wrapping_add(remaining);
288 }
289 table.push(PixelWeight {
290 src_start: start_i,
291 src_end,
292 weights,
293 });
294 }
295 Some(table)
296}
297
298fn use_interpolate_bilinear(
300 options: Options,
301 dest_width: i32,
302 dest_height: i32,
303 src_width: i32,
304 src_height: i32,
305) -> bool {
306 !options.bilinear
307 && !options.no_smoothing
308 && dest_width != 0
309 && i64::from(dest_height.abs() / 8)
310 < i64::from(src_width) * i64::from(src_height) / i64::from(dest_width.abs())
311}
312
313pub fn stretch(
319 src: &Source<'_>,
320 src_width: i32,
321 src_height: i32,
322 dest_width: i32,
323 dest_height: i32,
324 clip: Rect,
325 options: Options,
326) -> Option<Stretched> {
327 if dest_width == 0 || dest_height == 0 || clip.is_empty() || src_width <= 0 || src_height <= 0 {
328 return None;
329 }
330 let channels = src.channels();
331 let resample = if options.no_smoothing {
332 Options {
333 bilinear: false,
334 no_smoothing: true,
335 }
336 } else if use_interpolate_bilinear(options, dest_width, dest_height, src_width, src_height) {
337 Options {
338 bilinear: true,
339 no_smoothing: false,
340 }
341 } else {
342 options
343 };
344 let scale_x = f64::from(src_width as f32 / dest_width as f32);
347 let scale_y = f64::from(src_height as f32 / dest_height as f32);
348 let base_x = if dest_width > 0 {
349 0.0
350 } else {
351 f64::from(dest_width)
352 };
353 let base_y = if dest_height > 0 {
354 0.0
355 } else {
356 f64::from(dest_height)
357 };
358 let mut src_left = scale_x * (f64::from(clip.left) + base_x);
359 let mut src_right = scale_x * (f64::from(clip.right) + base_x);
360 let mut src_top = scale_y * (f64::from(clip.top) + base_y);
361 let mut src_bottom = scale_y * (f64::from(clip.bottom) + base_y);
362 if src_left > src_right {
363 std::mem::swap(&mut src_left, &mut src_right);
364 }
365 if src_top > src_bottom {
366 std::mem::swap(&mut src_top, &mut src_bottom);
367 }
368 let mut src_clip = Rect::new(
369 src_left.floor() as i32,
370 src_top.floor() as i32,
371 src_right.ceil() as i32,
372 src_bottom.ceil() as i32,
373 );
374 src_clip.intersect(&Rect::new(0, 0, src_width, src_height));
375 if src_clip.is_empty() {
376 return None;
377 }
378
379 let dest_cols = clip.width() as usize;
381 let dest_rows = clip.height() as usize;
382 let horz = calculate_weights(
383 dest_width,
384 clip.left,
385 clip.right,
386 src_width,
387 src_clip.left,
388 src_clip.right,
389 resample,
390 )?;
391 let inter_pitch = dest_cols * channels;
392 let src_rows = src_clip.height() as usize;
393 let mut inter = vec![0u8; inter_pitch * src_rows];
394 let mut expanded = Vec::new();
395 for (r, row) in (src_clip.top..src_clip.bottom).zip(inter.chunks_exact_mut(inter_pitch)) {
396 let r = r as usize;
397 let src_row: &[u8] = match src {
398 Source::Bilevel { data, stride } => {
399 let bytes = data.get(r * stride..(r * stride + stride))?;
400 expanded.clear();
401 expanded.reserve(stride * 8);
402 for &b in bytes {
403 for bit in 0..8 {
404 expanded.push(if (b >> (7 - bit)) & 1 == 1 { 255 } else { 0 });
405 }
406 }
407 &expanded
408 }
409 Source::Gray8 { data, stride } => {
410 data.get(r * stride..r * stride + src_width as usize)?
411 }
412 Source::Rgb8 { data, stride } => {
413 data.get(r * stride..r * stride + 3 * src_width as usize)?
414 }
415 };
416 for (pw, dest) in horz.iter().zip(row.chunks_exact_mut(channels)) {
417 let taps = pw.taps();
418 let start = pw.src_start as usize;
419 for (c, d) in dest.iter_mut().enumerate() {
420 let mut acc: u32 = 0;
421 for (k, &w) in taps.iter().enumerate() {
422 let v = *src_row.get((start + k) * channels + c)?;
423 acc = acc.wrapping_add(w.wrapping_mul(u32::from(v)));
424 }
425 *d = pixel_from_fixed(acc);
426 }
427 }
428 }
429
430 let vert = calculate_weights(
432 dest_height,
433 clip.top,
434 clip.bottom,
435 src_height,
436 src_clip.top,
437 src_clip.bottom,
438 resample,
439 )?;
440 let mut out = vec![0u8; inter_pitch * dest_rows];
441 let mut accum = vec![0u32; inter_pitch];
442 for (pw, dest) in vert.iter().zip(out.chunks_exact_mut(inter_pitch)) {
443 let taps = pw.taps();
444 accum.fill(0);
445 for (k, &w) in taps.iter().enumerate() {
446 let src_row_index = (pw.src_start + k as i32 - src_clip.top) as usize;
447 let row = inter.get(src_row_index * inter_pitch..(src_row_index + 1) * inter_pitch)?;
448 for (a, &v) in accum.iter_mut().zip(row) {
449 *a = a.wrapping_add(w.wrapping_mul(u32::from(v)));
450 }
451 }
452 for (d, &a) in dest.iter_mut().zip(&accum) {
453 *d = pixel_from_fixed(a);
454 }
455 }
456 Some(Stretched {
457 width: dest_cols,
458 height: dest_rows,
459 channels,
460 data: out,
461 })
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
472 fn shrink_by_two_averages_pairs() {
473 let data = [10u8, 20, 30, 41];
474 let out = stretch(
475 &Source::Gray8 {
476 data: &data,
477 stride: 4,
478 },
479 4,
480 1,
481 2,
482 1,
483 Rect::new(0, 0, 2, 1),
484 Options::default(),
485 )
486 .unwrap();
487 assert_eq!(out.data, vec![15, 35]);
488 }
489
490 #[test]
493 fn negative_width_flips() {
494 let data = [10u8, 20, 30, 41];
495 let out = stretch(
496 &Source::Gray8 {
497 data: &data,
498 stride: 4,
499 },
500 4,
501 1,
502 -2,
503 1,
504 Rect::new(0, 0, 2, 1),
505 Options::default(),
506 )
507 .unwrap();
508 assert_eq!(out.data, vec![35, 15]);
509 }
510
511 #[test]
516 fn enlarge_interpolates() {
517 let data = [0u8, 100];
518 let src = Source::Gray8 {
519 data: &data,
520 stride: 2,
521 };
522 let bilinear = Options {
523 bilinear: true,
524 no_smoothing: false,
525 };
526 let out = stretch(&src, 2, 1, 4, 1, Rect::new(0, 0, 4, 1), bilinear).unwrap();
527 assert_eq!(out.data, vec![0, 25, 75, 100]);
529 let out = stretch(&src, 2, 1, 4, 1, Rect::new(0, 0, 4, 1), Options::default()).unwrap();
530 assert_eq!(out.data, vec![0, 0, 100, 100]);
531 }
532
533 #[test]
534 fn bilevel_rows_expand_to_0_and_255() {
535 let data = [0b1010_0000u8];
536 let out = stretch(
537 &Source::Bilevel {
538 data: &data,
539 stride: 1,
540 },
541 4,
542 1,
543 4,
544 1,
545 Rect::new(0, 0, 4, 1),
546 Options::default(),
547 )
548 .unwrap();
549 assert_eq!(out.data, vec![255, 0, 255, 0]);
550 }
551
552 #[test]
553 fn swapped_clip_box_matches_pdfium() {
554 let r = Rect::new(1, 2, 5, 9);
555 let s = r.swapped_clip_box(10, 20, false, false);
556 assert_eq!(s, Rect::new(2, 1, 9, 5));
557 let s = r.swapped_clip_box(10, 20, true, true);
558 assert_eq!(s, Rect::new(11, 5, 18, 9));
559 }
560}