1use otf_pixels_core::{
18 AccessPattern, ImageDescriptor, Op, PixelsError, Region, Result, Tile, TileMut,
19};
20
21fn sole_input<'a>(op: &str, inputs: &'a [ImageDescriptor]) -> Result<&'a ImageDescriptor> {
23 inputs
24 .first()
25 .ok_or_else(|| PixelsError::graph(format!("`{op}` takes one input, got none")))
26}
27
28fn sole_tile<'a, 'b>(op: &str, inputs: &'a [Tile<'b>]) -> Result<&'a Tile<'b>> {
30 inputs
31 .first()
32 .ok_or_else(|| PixelsError::graph(format!("`{op}` takes one input tile, got none")))
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Crop {
43 window: Region,
44}
45
46impl Crop {
47 pub fn new(window: Region) -> Result<Self> {
56 if window.is_empty() {
57 return Err(PixelsError::invalid_argument(
58 "window",
59 format!("crop window {window} has no pixels"),
60 ));
61 }
62 Ok(Self { window })
63 }
64
65 pub fn at(x: u32, y: u32, width: u32, height: u32) -> Result<Self> {
71 Self::new(Region::new(x, y, width, height))
72 }
73
74 #[must_use]
76 pub const fn window(&self) -> Region {
77 self.window
78 }
79}
80
81impl Op for Crop {
82 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
87 None
88 }
89 fn name(&self) -> &'static str {
90 "crop"
91 }
92
93 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
94 let input = sole_input("crop", inputs)?;
95 if !input.region().contains(self.window) {
96 return Err(PixelsError::invalid_argument(
97 "window",
98 format!("crop window {} lies outside the {input} input", self.window),
99 ));
100 }
101 input.resized(self.window.width, self.window.height)
102 }
103
104 fn input_regions(&self, output: Region, _: &[ImageDescriptor]) -> Result<Vec<Region>> {
105 Ok(vec![output.translate(
107 i64::from(self.window.x),
108 i64::from(self.window.y),
109 )])
110 }
111
112 fn access_pattern(&self) -> AccessPattern {
113 AccessPattern::Sequential
115 }
116
117 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
118 let input = sole_tile("crop", inputs)?;
119 let out_region = output.region();
120 let bpp = output.pixel().bytes_per_pixel();
121 let span = out_region.width as usize * bpp;
122 let (dx, dy) = (self.window.x, self.window.y);
124 for y in out_region.y..out_region.y.saturating_add(out_region.height) {
125 let source_y = y.checked_add(dy).ok_or_else(|| {
126 PixelsError::invalid_argument("window", "crop origin overflows the image")
127 })?;
128 let Some(src_row) = input.row(source_y) else {
129 return Err(PixelsError::graph(format!(
130 "crop input {} is missing row {source_y}",
131 input.region()
132 )));
133 };
134 let start = (out_region.x.saturating_add(dx) - input.region().x) as usize * bpp;
135 let Some(from) = src_row.get(start..start + span) else {
136 return Err(PixelsError::graph(
137 "crop input row is too short for the window",
138 ));
139 };
140 let Some(into) = output.row_mut(y) else {
141 return Err(PixelsError::graph(format!(
142 "crop output is missing row {y}"
143 )));
144 };
145 into.copy_from_slice(from);
146 }
147 Ok(())
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
174pub struct Flip;
175
176impl Op for Flip {
177 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
180 Some(std::sync::Arc::new(Self))
181 }
182 fn name(&self) -> &'static str {
183 "flip"
184 }
185
186 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
187 sole_input("flip", inputs).copied()
188 }
189
190 fn input_regions(&self, output: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
191 let input = sole_input("flip", inputs)?;
192 let bottom = u64::from(input.height);
195 if output.bottom() > bottom {
196 return Err(PixelsError::invalid_argument(
197 "output",
198 format!("{output} extends past the {input} input"),
199 ));
200 }
201 let y = (bottom - output.bottom()) as u32;
203 Ok(vec![Region::new(output.x, y, output.width, output.height)])
204 }
205
206 fn access_pattern(&self) -> AccessPattern {
207 AccessPattern::Sequential
211 }
212
213 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
214 let input = sole_tile("flip", inputs)?;
215 let (out_region, in_region) = (output.region(), input.region());
216 if out_region.height != in_region.height || out_region.width != in_region.width {
217 return Err(PixelsError::graph(format!(
218 "flip input {in_region} does not match output {out_region}"
219 )));
220 }
221 for offset in 0..out_region.height {
222 let out_y = out_region.y + offset;
223 let in_y = in_region.y + (in_region.height - 1 - offset);
225 let Some(from) = input.row(in_y) else {
226 return Err(PixelsError::graph(format!(
227 "flip input is missing row {in_y}"
228 )));
229 };
230 let Some(into) = output.row_mut(out_y) else {
231 return Err(PixelsError::graph(format!(
232 "flip output is missing row {out_y}"
233 )));
234 };
235 into.copy_from_slice(from);
236 }
237 Ok(())
238 }
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
243pub struct Flop;
244
245impl Op for Flop {
246 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
248 Some(std::sync::Arc::new(Self))
249 }
250 fn name(&self) -> &'static str {
251 "flop"
252 }
253
254 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
255 sole_input("flop", inputs).copied()
256 }
257
258 fn input_regions(&self, output: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
259 let input = sole_input("flop", inputs)?;
260 let right = u64::from(input.width);
261 if output.right() > right {
262 return Err(PixelsError::invalid_argument(
263 "output",
264 format!("{output} extends past the {input} input"),
265 ));
266 }
267 let x = (right - output.right()) as u32;
269 Ok(vec![Region::new(x, output.y, output.width, output.height)])
270 }
271
272 fn access_pattern(&self) -> AccessPattern {
273 AccessPattern::Sequential
276 }
277
278 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
279 let input = sole_tile("flop", inputs)?;
280 let (out_region, in_region) = (output.region(), input.region());
281 if out_region.width != in_region.width || out_region.height != in_region.height {
282 return Err(PixelsError::graph(format!(
283 "flop input {in_region} does not match output {out_region}"
284 )));
285 }
286 let bpp = output.pixel().bytes_per_pixel();
287 let width = out_region.width as usize;
288 for offset in 0..out_region.height {
289 let Some(from) = input.row(in_region.y + offset) else {
290 return Err(PixelsError::graph("flop input is missing a row"));
291 };
292 let Some(into) = output.row_mut(out_region.y + offset) else {
293 return Err(PixelsError::graph("flop output is missing a row"));
294 };
295 for x in 0..width {
298 let src = x * bpp;
299 let dst = (width - 1 - x) * bpp;
300 let (Some(pixel), Some(slot)) =
301 (from.get(src..src + bpp), into.get_mut(dst..dst + bpp))
302 else {
303 return Err(PixelsError::graph("flop row is too short"));
304 };
305 slot.copy_from_slice(pixel);
306 }
307 }
308 Ok(())
309 }
310}
311
312#[cfg(test)]
313#[allow(
314 clippy::unwrap_used,
315 clippy::indexing_slicing,
316 clippy::panic,
317 reason = "tests operate on known-good values and assert shapes directly"
318)]
319mod tests {
320 use super::*;
321 use otf_pixels_core::{
322 BufferSource, ErrorCode, Format, Image, PixelFormat, Producer, TileBuf, evaluate,
323 };
324 use std::sync::Arc;
325
326 fn ramp(width: u32, height: u32) -> Image {
328 ramp_with(width, height, PixelFormat::Gray8)
329 }
330
331 fn ramp_with(width: u32, height: u32, pixel: PixelFormat) -> Image {
332 let desc = ImageDescriptor::new(width, height, pixel).unwrap();
333 let len = desc.byte_len().unwrap();
334 let bytes: Vec<u8> = (0..len).map(|i| i as u8).collect();
335 let buffer = TileBuf::from_vec(desc.region(), pixel, bytes).unwrap();
336 let source = BufferSource::new(desc, Arc::new(buffer)).unwrap();
337 Image::from_producer(Arc::new(source) as Arc<dyn Producer>, Format::Raw)
338 }
339
340 #[test]
341 fn crop_extracts_the_window() {
342 let image = ramp(4, 4)
344 .apply(Arc::new(Crop::at(1, 1, 2, 2).unwrap()))
345 .unwrap();
346 assert_eq!(image.descriptor().width, 2);
347 assert_eq!(image.descriptor().height, 2);
348 assert_eq!(evaluate(&image).unwrap().bytes(), &[5, 6, 9, 10]);
349 }
350
351 #[test]
352 fn crop_at_the_origin_and_the_far_corner() {
353 let top_left = ramp(4, 4)
354 .apply(Arc::new(Crop::at(0, 0, 2, 2).unwrap()))
355 .unwrap();
356 assert_eq!(evaluate(&top_left).unwrap().bytes(), &[0, 1, 4, 5]);
357 let bottom_right = ramp(4, 4)
358 .apply(Arc::new(Crop::at(2, 2, 2, 2).unwrap()))
359 .unwrap();
360 assert_eq!(evaluate(&bottom_right).unwrap().bytes(), &[10, 11, 14, 15]);
361 let whole = ramp(2, 2)
363 .apply(Arc::new(Crop::at(0, 0, 2, 2).unwrap()))
364 .unwrap();
365 assert_eq!(evaluate(&whole).unwrap().bytes(), &[0, 1, 2, 3]);
366 }
367
368 #[test]
369 fn a_window_outside_the_image_is_rejected_at_build_time() {
370 let err = ramp(4, 4)
371 .apply(Arc::new(Crop::at(3, 3, 2, 2).unwrap()))
372 .unwrap_err();
373 assert_eq!(err.code(), ErrorCode::InvalidArgument);
374 let err = ramp(4, 4)
375 .apply(Arc::new(Crop::at(5, 0, 1, 1).unwrap()))
376 .unwrap_err();
377 assert_eq!(err.code(), ErrorCode::InvalidArgument);
378 }
379
380 #[test]
381 fn an_empty_window_is_rejected_at_construction() {
382 assert_eq!(
383 Crop::at(0, 0, 0, 4).unwrap_err().code(),
384 ErrorCode::InvalidArgument
385 );
386 assert_eq!(
387 Crop::at(0, 0, 4, 0).unwrap_err().code(),
388 ErrorCode::InvalidArgument
389 );
390 }
391
392 #[test]
393 fn crop_demands_only_its_window() {
394 let crop = Crop::at(10, 20, 4, 4).unwrap();
397 let input = ImageDescriptor::new(100, 100, PixelFormat::Gray8).unwrap();
398 let regions = crop
399 .input_regions(Region::from_size(4, 4), &[input])
400 .unwrap();
401 assert_eq!(regions, vec![Region::new(10, 20, 4, 4)]);
402 }
403
404 #[test]
405 fn flip_mirrors_rows() {
406 let image = ramp(2, 3).apply(Arc::new(Flip)).unwrap();
408 assert_eq!(evaluate(&image).unwrap().bytes(), &[4, 5, 2, 3, 0, 1]);
409 }
410
411 #[test]
412 fn flop_mirrors_columns() {
413 let image = ramp(3, 2).apply(Arc::new(Flop)).unwrap();
415 assert_eq!(evaluate(&image).unwrap().bytes(), &[2, 1, 0, 5, 4, 3]);
416 }
417
418 #[test]
419 fn flop_reverses_pixels_not_bytes() {
420 let image = ramp_with(2, 1, PixelFormat::Rgb8)
423 .apply(Arc::new(Flop))
424 .unwrap();
425 assert_eq!(evaluate(&image).unwrap().bytes(), &[3, 4, 5, 0, 1, 2]);
426 }
427
428 #[test]
429 fn flip_preserves_pixels_within_rows() {
430 let image = ramp_with(2, 2, PixelFormat::Rgba8)
432 .apply(Arc::new(Flip))
433 .unwrap();
434 assert_eq!(
435 evaluate(&image).unwrap().bytes(),
436 &[8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7]
437 );
438 }
439
440 #[test]
441 fn flip_and_flop_are_their_own_inverses() {
442 for pixel in [PixelFormat::Gray8, PixelFormat::Rgb8, PixelFormat::Rgba16] {
443 let original = evaluate(&ramp_with(3, 4, pixel)).unwrap();
444 let flipped = ramp_with(3, 4, pixel)
445 .apply(Arc::new(Flip))
446 .unwrap()
447 .apply(Arc::new(Flip))
448 .unwrap();
449 assert_eq!(
450 evaluate(&flipped).unwrap().bytes(),
451 original.bytes(),
452 "flip² for {pixel}"
453 );
454 let flopped = ramp_with(3, 4, pixel)
455 .apply(Arc::new(Flop))
456 .unwrap()
457 .apply(Arc::new(Flop))
458 .unwrap();
459 assert_eq!(
460 evaluate(&flopped).unwrap().bytes(),
461 original.bytes(),
462 "flop² for {pixel}"
463 );
464 }
465 }
466
467 #[test]
468 fn flip_and_flop_preserve_dimensions_and_format() {
469 for pixel in PixelFormat::ALL {
470 let image = ramp_with(3, 5, *pixel).apply(Arc::new(Flip)).unwrap();
471 assert_eq!(image.descriptor().width, 3, "{pixel}");
472 assert_eq!(image.descriptor().height, 5, "{pixel}");
473 assert_eq!(image.descriptor().pixel, *pixel);
474 }
475 }
476
477 #[test]
478 fn flip_demand_mirrors_the_requested_band() {
479 let input = ImageDescriptor::new(4, 10, PixelFormat::Gray8).unwrap();
480 let regions = Flip
482 .input_regions(Region::new(0, 0, 4, 2), &[input])
483 .unwrap();
484 assert_eq!(regions, vec![Region::new(0, 8, 4, 2)]);
485 let regions = Flip
487 .input_regions(Region::new(0, 3, 4, 2), &[input])
488 .unwrap();
489 assert_eq!(regions, vec![Region::new(0, 5, 4, 2)]);
490 }
491
492 #[test]
493 fn flop_demand_mirrors_the_requested_band() {
494 let input = ImageDescriptor::new(10, 4, PixelFormat::Gray8).unwrap();
495 let regions = Flop
496 .input_regions(Region::new(0, 0, 2, 4), &[input])
497 .unwrap();
498 assert_eq!(regions, vec![Region::new(8, 0, 2, 4)]);
499 }
500
501 #[test]
502 fn demand_beyond_the_input_is_an_error_not_a_wrap() {
503 let input = ImageDescriptor::new(4, 4, PixelFormat::Gray8).unwrap();
504 let err = Flip
505 .input_regions(Region::new(0, 0, 4, 8), &[input])
506 .unwrap_err();
507 assert_eq!(err.code(), ErrorCode::InvalidArgument);
508 let err = Flop
509 .input_regions(Region::new(0, 0, 8, 4), &[input])
510 .unwrap_err();
511 assert_eq!(err.code(), ErrorCode::InvalidArgument);
512 }
513
514 #[test]
515 fn ops_declare_their_access_patterns() {
516 assert_eq!(
518 Crop::at(0, 0, 1, 1).unwrap().access_pattern(),
519 AccessPattern::Sequential
520 );
521 assert_eq!(Flop.access_pattern(), AccessPattern::Sequential);
522 assert_eq!(Flip.access_pattern(), AccessPattern::Sequential);
526 }
527
528 #[test]
529 fn flip_demand_is_a_pure_region_remap() {
530 let input = ImageDescriptor::new(4, 100, PixelFormat::Gray8).unwrap();
534 for y in (0..100).step_by(10) {
535 let output = Region::new(0, y, 4, 10);
536 let regions = Flip.input_regions(output, &[input]).unwrap();
537 assert_eq!(regions.len(), 1);
538 assert_eq!(regions[0].height, output.height, "band size is preserved");
539 assert_eq!(regions[0].width, output.width);
540 assert_eq!(regions[0].y, 100 - y - 10);
542 }
543 }
544
545 #[test]
546 fn ops_reject_being_called_with_no_input() {
547 assert!(Flip.output_descriptor(&[]).is_err());
548 assert!(Flop.output_descriptor(&[]).is_err());
549 assert!(
550 Crop::at(0, 0, 1, 1)
551 .unwrap()
552 .output_descriptor(&[])
553 .is_err()
554 );
555 assert!(
556 Flip.compute(
557 &[],
558 &mut TileBuf::zeroed(Region::from_size(1, 1), PixelFormat::Gray8)
559 .unwrap()
560 .as_tile_mut()
561 .unwrap()
562 )
563 .is_err()
564 );
565 }
566
567 #[test]
568 fn geometry_ops_compose() {
569 let image = ramp(4, 4)
571 .apply(Arc::new(Crop::at(1, 1, 2, 2).unwrap()))
572 .unwrap()
573 .apply(Arc::new(Flip))
574 .unwrap()
575 .apply(Arc::new(Flop))
576 .unwrap();
577 assert_eq!(evaluate(&image).unwrap().bytes(), &[10, 9, 6, 5]);
579 }
580}