/rust/registry/src/index.crates.io-1949cf8c6b5b557f/exr-1.74.2/src/image/crop.rs
Line | Count | Source |
1 | | //! Crop away unwanted pixels. Includes automatic detection of bounding |
2 | | //! rectangle. Currently does not support deep data and resolution levels. |
3 | | |
4 | | use crate::{ |
5 | | block::BlockIndex, |
6 | | image::{ |
7 | | write::channels::{ChannelsWriter, GetPixel, WritableChannels}, |
8 | | AnyChannel, AnyChannels, FlatSamples, FlatSamplesPixel, Layer, SpecificChannels, |
9 | | }, |
10 | | math::{RoundingMode, Vec2}, |
11 | | meta::{ |
12 | | attribute::{ChannelList, IntegerBounds, LevelMode}, |
13 | | header::{Header, LayerAttributes}, |
14 | | }, |
15 | | }; |
16 | | |
17 | | /// Something that has a two-dimensional rectangular shape |
18 | | pub trait GetBounds { |
19 | | /// The bounding rectangle of this pixel grid. |
20 | | fn bounds(&self) -> IntegerBounds; |
21 | | } |
22 | | |
23 | | /// Inspect the pixels in this image to determine where to crop some away |
24 | | pub trait InspectSample: GetBounds { |
25 | | /// The type of pixel in this pixel grid. |
26 | | type Sample; |
27 | | |
28 | | /// Index is not in world coordinates, but within the data window. |
29 | | /// Position `(0,0)` always represents the top left pixel. |
30 | | fn inspect_sample(&self, local_index: Vec2<usize>) -> Self::Sample; |
31 | | } |
32 | | |
33 | | /// Crop some pixels ways when specifying a smaller rectangle |
34 | | pub trait Crop: Sized { |
35 | | /// The type of this image after cropping (probably the same as before) |
36 | | type Cropped; |
37 | | |
38 | | /// Crop the image to exclude unwanted pixels. |
39 | | /// Panics for invalid (larger than previously) bounds. |
40 | | /// The bounds are specified in absolute coordinates. |
41 | | /// Does not reduce allocation size of the current image, but instead only |
42 | | /// adjust a few boundary numbers. Use `reallocate_cropped()` on the |
43 | | /// return value to actually reduce the memory footprint. |
44 | | fn crop(self, bounds: IntegerBounds) -> Self::Cropped; |
45 | | |
46 | | /// Reduce your image to a smaller part, usually to save memory. |
47 | | /// Crop if bounds are specified, return the original if no bounds are |
48 | | /// specified. Does not reduce allocation size of the current image, but |
49 | | /// instead only adjust a few boundary numbers. |
50 | | /// Use `reallocate_cropped()` on the return value to actually reduce the |
51 | | /// memory footprint. |
52 | 0 | fn try_crop(self, bounds: Option<IntegerBounds>) -> CropResult<Self::Cropped, Self> { |
53 | 0 | match bounds { |
54 | 0 | Some(bounds) => CropResult::Cropped(self.crop(bounds)), |
55 | 0 | None => CropResult::Empty { |
56 | 0 | original: self, |
57 | 0 | }, |
58 | | } |
59 | 0 | } |
60 | | } |
61 | | |
62 | | /// Cropping an image fails if the image is fully transparent. |
63 | | /// Use [`or_crop_to_1x1_if_empty`] or [`or_none_if_empty`] to obtain a normal |
64 | | /// image again. |
65 | | #[must_use] |
66 | | #[derive(Debug, Clone, Copy, Eq, PartialEq)] |
67 | | pub enum CropResult<Cropped, Old> { |
68 | | /// The image contained some pixels and has been cropped or left untouched |
69 | | Cropped(Cropped), |
70 | | |
71 | | /// All pixels in the image would be discarded, removing the whole image |
72 | | Empty { |
73 | | /// The fully discarded image which caused the cropping to fail |
74 | | original: Old, |
75 | | }, |
76 | | } |
77 | | |
78 | | /// Crop away unwanted pixels from the border if they match the specified rule. |
79 | | pub trait CropWhere<Sample>: Sized { |
80 | | /// The type of the cropped image (probably the same as the original image). |
81 | | type Cropped; |
82 | | |
83 | | /// Crop away unwanted pixels from the border if they match the specified |
84 | | /// rule. Does not reduce allocation size of the current image, but |
85 | | /// instead only adjust a few boundary numbers. |
86 | | /// Use `reallocate_cropped()` on the return value to actually reduce the |
87 | | /// memory footprint. |
88 | | fn crop_where(self, discard_if: impl Fn(Sample) -> bool) -> CropResult<Self::Cropped, Self>; |
89 | | |
90 | | /// Crop away unwanted pixels from the border if they match the specified |
91 | | /// color. If you want discard based on a rule, use `crop_where` with a |
92 | | /// closure instead. Does not reduce allocation size of the current |
93 | | /// image, but instead only adjust a few boundary numbers. |
94 | | /// Use `reallocate_cropped()` on the return value to actually reduce the |
95 | | /// memory footprint. |
96 | | fn crop_where_eq(self, discard_color: impl Into<Sample>) -> CropResult<Self::Cropped, Self> |
97 | | where |
98 | | Sample: PartialEq; |
99 | | |
100 | | /// Convert this data to cropped data without discarding any pixels. |
101 | | fn crop_nowhere(self) -> Self::Cropped; |
102 | | } |
103 | | |
104 | | impl<Channels> Crop for Layer<Channels> { |
105 | | type Cropped = Layer<CroppedChannels<Channels>>; |
106 | | |
107 | 0 | fn crop(self, bounds: IntegerBounds) -> Self::Cropped { |
108 | 0 | CroppedChannels::crop_layer(bounds, self) |
109 | 0 | } |
110 | | } |
111 | | |
112 | | impl<T> CropWhere<T::Sample> for T |
113 | | where |
114 | | T: Crop + InspectSample, |
115 | | { |
116 | | type Cropped = <Self as Crop>::Cropped; |
117 | | |
118 | 0 | fn crop_where(self, discard_if: impl Fn(T::Sample) -> bool) -> CropResult<Self::Cropped, Self> { |
119 | 0 | let smaller_bounds = { |
120 | 0 | let keep_if = |position| !discard_if(self.inspect_sample(position)); |
121 | 0 | try_find_smaller_bounds(self.bounds(), keep_if) |
122 | | }; |
123 | | |
124 | 0 | self.try_crop(smaller_bounds) |
125 | 0 | } |
126 | | |
127 | 0 | fn crop_where_eq(self, discard_color: impl Into<T::Sample>) -> CropResult<Self::Cropped, Self> |
128 | 0 | where |
129 | 0 | T::Sample: PartialEq, |
130 | | { |
131 | 0 | let discard_color: T::Sample = discard_color.into(); |
132 | 0 | self.crop_where(|sample| sample == discard_color) |
133 | 0 | } |
134 | | |
135 | 0 | fn crop_nowhere(self) -> Self::Cropped { |
136 | 0 | let current_bounds = self.bounds(); |
137 | 0 | self.crop(current_bounds) |
138 | 0 | } |
139 | | } |
140 | | |
141 | | /// A smaller window into an existing pixel storage |
142 | | #[derive(Debug, Clone, Eq, PartialEq)] |
143 | | pub struct CroppedChannels<Channels> { |
144 | | /// The uncropped pixel storage |
145 | | pub full_channels: Channels, |
146 | | |
147 | | /// The uncropped pixel storage bounds |
148 | | pub full_bounds: IntegerBounds, |
149 | | |
150 | | /// The cropped pixel storage bounds |
151 | | pub cropped_bounds: IntegerBounds, |
152 | | } |
153 | | |
154 | | impl<Channels> CroppedChannels<Channels> { |
155 | | /// Wrap a layer in a cropped view with adjusted bounds, but without |
156 | | /// reallocating your pixels |
157 | 0 | pub fn crop_layer( |
158 | 0 | new_bounds: IntegerBounds, |
159 | 0 | layer: Layer<Channels>, |
160 | 0 | ) -> Layer<CroppedChannels<Channels>> { |
161 | 0 | Layer { |
162 | 0 | channel_data: CroppedChannels { |
163 | 0 | cropped_bounds: new_bounds, |
164 | 0 | full_bounds: layer.absolute_bounds(), |
165 | 0 | full_channels: layer.channel_data, |
166 | 0 | }, |
167 | 0 |
|
168 | 0 | size: new_bounds.size, |
169 | 0 |
|
170 | 0 | attributes: LayerAttributes { |
171 | 0 | layer_position: new_bounds.position, |
172 | 0 | ..layer.attributes |
173 | 0 | }, |
174 | 0 |
|
175 | 0 | encoding: layer.encoding, |
176 | 0 | } |
177 | 0 | } |
178 | | } |
179 | | |
180 | | // TODO make cropped view readable if you only need a specific section of the |
181 | | // image? |
182 | | |
183 | | // make cropped view writable: |
184 | | |
185 | | impl<'slf, Channels: 'slf> WritableChannels<'slf> for CroppedChannels<Channels> |
186 | | where |
187 | | Channels: WritableChannels<'slf>, |
188 | | { |
189 | | type Writer = CroppedWriter<Channels::Writer>; |
190 | | |
191 | 0 | fn infer_channel_list(&self) -> ChannelList { |
192 | 0 | self.full_channels.infer_channel_list() // no need for adjustments, as |
193 | | // the layer content already |
194 | | // reflects the changes |
195 | 0 | } |
196 | | |
197 | 0 | fn infer_level_modes(&self) -> (LevelMode, RoundingMode) { |
198 | 0 | self.full_channels.infer_level_modes() |
199 | 0 | } |
200 | | |
201 | 0 | fn create_writer(&'slf self, header: &Header) -> Self::Writer { |
202 | 0 | let offset = (self.cropped_bounds.position - self.full_bounds.position) |
203 | 0 | .to_usize("invalid cropping bounds for cropped view") |
204 | 0 | .unwrap(); |
205 | | |
206 | 0 | CroppedWriter { |
207 | 0 | channels: self.full_channels.create_writer(header), |
208 | 0 | offset, |
209 | 0 | } |
210 | 0 | } |
211 | | } |
212 | | |
213 | | /// A writer for the cropped view layer |
214 | | #[derive(Debug, Clone, PartialEq)] |
215 | | pub struct CroppedWriter<ChannelsWriter> { |
216 | | channels: ChannelsWriter, |
217 | | offset: Vec2<usize>, |
218 | | } |
219 | | |
220 | | impl<'c, Channels> ChannelsWriter for CroppedWriter<Channels> |
221 | | where |
222 | | Channels: ChannelsWriter, |
223 | | { |
224 | 0 | fn extract_uncompressed_block(&self, header: &Header, block: BlockIndex) -> Vec<u8> { |
225 | 0 | let block = BlockIndex { |
226 | 0 | pixel_position: block.pixel_position + self.offset, |
227 | 0 | ..block |
228 | 0 | }; |
229 | | |
230 | 0 | self.channels.extract_uncompressed_block(header, block) |
231 | 0 | } |
232 | | } |
233 | | |
234 | | impl<Samples, Channels> InspectSample for Layer<SpecificChannels<Samples, Channels>> |
235 | | where |
236 | | Samples: GetPixel, |
237 | | { |
238 | | type Sample = Samples::Pixel; |
239 | | |
240 | 0 | fn inspect_sample(&self, local_index: Vec2<usize>) -> Samples::Pixel { |
241 | 0 | self.channel_data.pixels.get_pixel(local_index) |
242 | 0 | } |
243 | | } |
244 | | |
245 | | impl InspectSample for Layer<AnyChannels<FlatSamples>> { |
246 | | type Sample = FlatSamplesPixel; |
247 | | |
248 | 0 | fn inspect_sample(&self, local_index: Vec2<usize>) -> FlatSamplesPixel { |
249 | 0 | self.sample_vec_at(local_index) |
250 | 0 | } |
251 | | } |
252 | | |
253 | | // ALGORITHM IDEA: for arbitrary channels, find the most desired channel, |
254 | | // and process that first, keeping the processed bounds as starting point for |
255 | | // the other layers |
256 | | |
257 | | /// Realize a cropped view of the original data, |
258 | | /// by actually removing the unwanted original pixels, |
259 | | /// reducing the memory consumption. |
260 | | /// Currently not supported for `SpecificChannels`. |
261 | | pub trait ApplyCroppedView { |
262 | | /// The simpler type after cropping is realized |
263 | | type Reallocated; |
264 | | |
265 | | /// Make the cropping real by reallocating the underlying storage, |
266 | | /// with the goal of reducing total memory usage. |
267 | | /// Currently not supported for `SpecificChannels`. |
268 | | fn reallocate_cropped(self) -> Self::Reallocated; |
269 | | } |
270 | | |
271 | | impl ApplyCroppedView for Layer<CroppedChannels<AnyChannels<FlatSamples>>> { |
272 | | type Reallocated = Layer<AnyChannels<FlatSamples>>; |
273 | | |
274 | 0 | fn reallocate_cropped(self) -> Self::Reallocated { |
275 | 0 | let cropped_absolute_bounds = self.channel_data.cropped_bounds; |
276 | 0 | let cropped_relative_bounds = |
277 | 0 | cropped_absolute_bounds.with_origin(-self.channel_data.full_bounds.position); |
278 | | |
279 | 0 | assert!( |
280 | 0 | self.absolute_bounds().contains(cropped_absolute_bounds), |
281 | | "bounds not valid for layer dimensions" |
282 | | ); |
283 | 0 | assert!(cropped_relative_bounds.size.area() > 0, "the cropped image would be empty"); |
284 | | |
285 | | Layer { |
286 | 0 | channel_data: if cropped_relative_bounds.size == self.channel_data.full_bounds.size { |
287 | 0 | assert_eq!( |
288 | | cropped_absolute_bounds.position, self.channel_data.full_bounds.position, |
289 | 0 | "crop bounds size equals, but position does not" |
290 | | ); |
291 | | |
292 | | // the cropping would not remove any pixels |
293 | 0 | self.channel_data.full_channels |
294 | | } else { |
295 | 0 | let start_x = cropped_relative_bounds.position.x() as usize; // safe, because just checked above |
296 | 0 | let start_y = cropped_relative_bounds.position.y() as usize; // safe, because just checked above |
297 | 0 | let x_range = start_x..start_x + cropped_relative_bounds.size.width(); |
298 | 0 | let old_width = self.channel_data.full_bounds.size.width(); |
299 | 0 | let new_height = cropped_relative_bounds.size.height(); |
300 | | |
301 | 0 | let channels = self |
302 | 0 | .channel_data |
303 | 0 | .full_channels |
304 | 0 | .list |
305 | 0 | .into_iter() |
306 | 0 | .map(|channel: AnyChannel<FlatSamples>| { |
307 | 0 | fn crop_samples<T: Copy>( |
308 | 0 | samples: Vec<T>, |
309 | 0 | old_width: usize, |
310 | 0 | new_height: usize, |
311 | 0 | x_range: std::ops::Range<usize>, |
312 | 0 | y_start: usize, |
313 | 0 | ) -> Vec<T> { |
314 | 0 | let filtered_lines = |
315 | 0 | samples.chunks_exact(old_width).skip(y_start).take(new_height); |
316 | 0 | let trimmed_lines = filtered_lines.map(|line| &line[x_range.clone()]); Unexecuted instantiation: <exr::image::Layer<exr::image::crop::CroppedChannels<exr::image::AnyChannels<exr::image::FlatSamples>>> as exr::image::crop::ApplyCroppedView>::reallocate_cropped::{closure#0}::crop_samples::<half::binary16::f16>::{closure#0}Unexecuted instantiation: <exr::image::Layer<exr::image::crop::CroppedChannels<exr::image::AnyChannels<exr::image::FlatSamples>>> as exr::image::crop::ApplyCroppedView>::reallocate_cropped::{closure#0}::crop_samples::<f32>::{closure#0}Unexecuted instantiation: <exr::image::Layer<exr::image::crop::CroppedChannels<exr::image::AnyChannels<exr::image::FlatSamples>>> as exr::image::crop::ApplyCroppedView>::reallocate_cropped::{closure#0}::crop_samples::<u32>::{closure#0} |
317 | 0 | trimmed_lines.flatten().map(|x| *x).collect() // TODO does this use memcpy? |
318 | 0 | } Unexecuted instantiation: <exr::image::Layer<exr::image::crop::CroppedChannels<exr::image::AnyChannels<exr::image::FlatSamples>>> as exr::image::crop::ApplyCroppedView>::reallocate_cropped::{closure#0}::crop_samples::<half::binary16::f16>Unexecuted instantiation: <exr::image::Layer<exr::image::crop::CroppedChannels<exr::image::AnyChannels<exr::image::FlatSamples>>> as exr::image::crop::ApplyCroppedView>::reallocate_cropped::{closure#0}::crop_samples::<f32>Unexecuted instantiation: <exr::image::Layer<exr::image::crop::CroppedChannels<exr::image::AnyChannels<exr::image::FlatSamples>>> as exr::image::crop::ApplyCroppedView>::reallocate_cropped::{closure#0}::crop_samples::<u32> |
319 | | |
320 | 0 | let samples = match channel.sample_data { |
321 | 0 | FlatSamples::F16(samples) => FlatSamples::F16(crop_samples( |
322 | 0 | samples, |
323 | 0 | old_width, |
324 | 0 | new_height, |
325 | 0 | x_range.clone(), |
326 | 0 | start_y, |
327 | 0 | )), |
328 | | |
329 | 0 | FlatSamples::F32(samples) => FlatSamples::F32(crop_samples( |
330 | 0 | samples, |
331 | 0 | old_width, |
332 | 0 | new_height, |
333 | 0 | x_range.clone(), |
334 | 0 | start_y, |
335 | 0 | )), |
336 | | |
337 | 0 | FlatSamples::U32(samples) => FlatSamples::U32(crop_samples( |
338 | 0 | samples, |
339 | 0 | old_width, |
340 | 0 | new_height, |
341 | 0 | x_range.clone(), |
342 | 0 | start_y, |
343 | 0 | )), |
344 | | }; |
345 | | |
346 | 0 | AnyChannel { |
347 | 0 | sample_data: samples, |
348 | 0 | ..channel |
349 | 0 | } |
350 | 0 | }) |
351 | 0 | .collect(); |
352 | | |
353 | 0 | AnyChannels { |
354 | 0 | list: channels, |
355 | 0 | } |
356 | | }, |
357 | | |
358 | 0 | attributes: self.attributes, |
359 | 0 | encoding: self.encoding, |
360 | 0 | size: self.size, |
361 | | } |
362 | 0 | } |
363 | | } |
364 | | |
365 | | /// Return the smallest bounding rectangle including all pixels that satisfy the |
366 | | /// predicate. Worst case: Fully transparent image, visits each pixel once. |
367 | | /// Best case: Fully opaque image, visits two pixels. |
368 | | /// Returns `None` if the image is fully transparent. |
369 | | /// Returns `[(0,0), size]` if the image is fully opaque. |
370 | | /// Designed to be cache-friendly linear search. Optimized for row-major image |
371 | | /// vectors. |
372 | 0 | pub fn try_find_smaller_bounds( |
373 | 0 | current_bounds: IntegerBounds, |
374 | 0 | pixel_at: impl Fn(Vec2<usize>) -> bool, |
375 | 0 | ) -> Option<IntegerBounds> { |
376 | 0 | assert_ne!( |
377 | 0 | current_bounds.size.area(), |
378 | | 0, |
379 | 0 | "cannot find smaller bounds of an image with zero width or height" |
380 | | ); |
381 | 0 | let Vec2(width, height) = current_bounds.size; |
382 | | |
383 | | // scans top to bottom (left to right) |
384 | 0 | let first_top_left_pixel = (0..height) |
385 | 0 | .flat_map(|y| (0..width).map(move |x| Vec2(x, y))) |
386 | 0 | .find(|&position| pixel_at(position))?; // return none if no pixel should be kept |
387 | | |
388 | | // scans bottom to top (right to left) |
389 | 0 | let first_bottom_right_pixel = (first_top_left_pixel.y() + 1..height) // excluding the top line |
390 | 0 | .flat_map(|y| (0..width).map(move |x| Vec2(x, y))) // x search cannot start at first_top.x, because this must catch all bottom pixels |
391 | 0 | .rev() |
392 | 0 | .find(|&position| pixel_at(position)) |
393 | 0 | .unwrap_or(first_top_left_pixel); // did not find any at bottom, but we know top has some pixel |
394 | | |
395 | | // now we know exactly how much we can throw away top and bottom, |
396 | | // but we don't know exactly about left or right |
397 | 0 | let top = first_top_left_pixel.y(); |
398 | 0 | let bottom = first_bottom_right_pixel.y(); |
399 | | |
400 | | // we only now some arbitrary left and right bounds which we need to refine. |
401 | | // because the actual image contents might be wider than the corner points. |
402 | | // we know that we do not need to look in the center between min x and max x, |
403 | | // as these must be included in any case. |
404 | 0 | let mut min_left_x = first_top_left_pixel.x().min(first_bottom_right_pixel.x()); |
405 | 0 | let mut max_right_x = first_bottom_right_pixel.x().max(first_top_left_pixel.x()); |
406 | | |
407 | | // requires for loop, because bounds change while searching |
408 | 0 | for y in top..=bottom { |
409 | | // escape the loop if there is nothing left to crop |
410 | 0 | if min_left_x == 0 && max_right_x == width - 1 { |
411 | 0 | break; |
412 | 0 | } |
413 | | |
414 | | // search from right image edge towards image center, until known max x, for |
415 | | // existing pixels, possibly including some pixels that would have been |
416 | | // cropped otherwise |
417 | 0 | if max_right_x != width - 1 { |
418 | 0 | max_right_x = (max_right_x + 1..width) |
419 | 0 | .rev() // excluding current max |
420 | 0 | .find(|&x| pixel_at(Vec2(x, y))) |
421 | 0 | .unwrap_or(max_right_x); |
422 | 0 | } |
423 | | |
424 | | // search from left image edge towards image center, until known min x, for |
425 | | // existing pixels, possibly including some pixels that would have been |
426 | | // cropped otherwise |
427 | 0 | if min_left_x != 0 { |
428 | 0 | min_left_x = (0..min_left_x) // excluding current min |
429 | 0 | .find(|&x| pixel_at(Vec2(x, y))) |
430 | 0 | .unwrap_or(min_left_x); |
431 | 0 | } |
432 | | } |
433 | | |
434 | | // TODO add 1px margin to avoid interpolation issues? |
435 | 0 | let local_start = Vec2(min_left_x, top); |
436 | 0 | let local_end = Vec2(max_right_x + 1, bottom + 1); |
437 | 0 | Some(IntegerBounds::new( |
438 | 0 | current_bounds.position + local_start.to_i32(), |
439 | 0 | local_end - local_start, |
440 | 0 | )) |
441 | 0 | } |
442 | | |
443 | | impl<S> GetBounds for Layer<S> { |
444 | 0 | fn bounds(&self) -> IntegerBounds { |
445 | 0 | self.absolute_bounds() |
446 | 0 | } |
447 | | } |
448 | | |
449 | | impl<Cropped, Original> CropResult<Cropped, Original> { |
450 | | /// If the image was fully empty, return `None`, otherwise return |
451 | | /// `Some(cropped_image)`. |
452 | 0 | pub fn or_none_if_empty(self) -> Option<Cropped> { |
453 | 0 | match self { |
454 | 0 | CropResult::Cropped(cropped) => Some(cropped), |
455 | | CropResult::Empty { |
456 | | .. |
457 | 0 | } => None, |
458 | | } |
459 | 0 | } |
460 | | |
461 | | /// If the image was fully empty, crop to one single pixel of all the |
462 | | /// transparent pixels instead, leaving the layer intact while reducing |
463 | | /// memory usage. |
464 | | /// |
465 | | /// # Panics |
466 | | /// Panics if the layer has zero width and height (which indicates an |
467 | | /// invalid layer state). This should never happen with properly |
468 | | /// constructed images. |
469 | 0 | pub fn or_crop_to_1x1_if_empty(self) -> Cropped |
470 | 0 | where |
471 | 0 | Original: Crop<Cropped = Cropped> + GetBounds, |
472 | | { |
473 | 0 | match self { |
474 | 0 | CropResult::Cropped(cropped) => cropped, |
475 | | CropResult::Empty { |
476 | 0 | original, |
477 | | } => { |
478 | 0 | let bounds = original.bounds(); |
479 | 0 | if bounds.size == Vec2(0, 0) { |
480 | 0 | unreachable!("layer has zero width and height - this indicates an invalid layer state that should have been caught during construction") |
481 | 0 | } |
482 | 0 | original.crop(IntegerBounds::new(bounds.position, Vec2(1, 1))) |
483 | | } |
484 | | } |
485 | 0 | } |
486 | | } |
487 | | |
488 | | #[cfg(test)] |
489 | | mod test { |
490 | | use super::*; |
491 | | |
492 | | #[test] |
493 | | fn find_bounds() { |
494 | | fn find_bounds(offset: Vec2<i32>, lines: &Vec<Vec<i32>>) -> IntegerBounds { |
495 | | if let Some(first_line) = lines.first() { |
496 | | assert!( |
497 | | lines.iter().all(|line| line.len() == first_line.len()), |
498 | | "invalid test input" |
499 | | ); |
500 | | IntegerBounds::new(offset, (first_line.len(), lines.len())) |
501 | | } else { |
502 | | IntegerBounds::new(offset, (0, 0)) |
503 | | } |
504 | | } |
505 | | |
506 | | fn assert_found_smaller_bounds( |
507 | | offset: Vec2<i32>, |
508 | | uncropped_lines: Vec<Vec<i32>>, |
509 | | expected_cropped_lines: Vec<Vec<i32>>, |
510 | | ) { |
511 | | let old_bounds = find_bounds(offset, &uncropped_lines); |
512 | | |
513 | | let found_bounds = try_find_smaller_bounds(old_bounds, |position| { |
514 | | uncropped_lines[position.y()][position.x()] != 0 |
515 | | }) |
516 | | .unwrap(); |
517 | | |
518 | | let found_bounds = found_bounds.with_origin(-offset); // make indices local |
519 | | |
520 | | let cropped_lines: Vec<Vec<i32>> = uncropped_lines |
521 | | [found_bounds.position.y() as usize..found_bounds.end().y() as usize] |
522 | | .iter() |
523 | | .map(|uncropped_line| { |
524 | | uncropped_line |
525 | | [found_bounds.position.x() as usize..found_bounds.end().x() as usize] |
526 | | .to_vec() |
527 | | }) |
528 | | .collect(); |
529 | | |
530 | | assert_eq!(cropped_lines, expected_cropped_lines); |
531 | | } |
532 | | |
533 | | assert_found_smaller_bounds( |
534 | | Vec2(-3, -3), |
535 | | vec![vec![2, 3, 4], vec![2, 3, 4]], |
536 | | vec![vec![2, 3, 4], vec![2, 3, 4]], |
537 | | ); |
538 | | |
539 | | assert_found_smaller_bounds(Vec2(-3, -3), vec![vec![2]], vec![vec![2]]); |
540 | | |
541 | | assert_found_smaller_bounds( |
542 | | Vec2(-3, -3), |
543 | | vec![vec![0], vec![2], vec![0], vec![0]], |
544 | | vec![vec![2]], |
545 | | ); |
546 | | |
547 | | assert_found_smaller_bounds(Vec2(-3, -3), vec![vec![0, 0, 0, 3, 0]], vec![vec![3]]); |
548 | | |
549 | | assert_found_smaller_bounds( |
550 | | Vec2(3, 3), |
551 | | vec![vec![0, 1, 1, 2, 1, 0], vec![0, 1, 3, 1, 1, 0], vec![0, 1, 1, 1, 1, 0]], |
552 | | vec![vec![1, 1, 2, 1], vec![1, 3, 1, 1], vec![1, 1, 1, 1]], |
553 | | ); |
554 | | |
555 | | assert_found_smaller_bounds( |
556 | | Vec2(3, 3), |
557 | | vec![ |
558 | | vec![0, 0, 0, 0], |
559 | | vec![1, 1, 2, 1], |
560 | | vec![1, 3, 1, 1], |
561 | | vec![1, 1, 1, 1], |
562 | | vec![0, 0, 0, 0], |
563 | | ], |
564 | | vec![vec![1, 1, 2, 1], vec![1, 3, 1, 1], vec![1, 1, 1, 1]], |
565 | | ); |
566 | | |
567 | | assert_found_smaller_bounds( |
568 | | Vec2(3, 3), |
569 | | vec![vec![0, 1, 1, 2, 1, 0], vec![0, 0, 3, 1, 0, 0], vec![0, 1, 1, 1, 1, 0]], |
570 | | vec![vec![1, 1, 2, 1], vec![0, 3, 1, 0], vec![1, 1, 1, 1]], |
571 | | ); |
572 | | |
573 | | assert_found_smaller_bounds( |
574 | | Vec2(3, 3), |
575 | | vec![vec![0, 0, 1, 2, 0, 0], vec![0, 1, 3, 1, 1, 0], vec![0, 0, 1, 1, 0, 0]], |
576 | | vec![vec![0, 1, 2, 0], vec![1, 3, 1, 1], vec![0, 1, 1, 0]], |
577 | | ); |
578 | | |
579 | | assert_found_smaller_bounds( |
580 | | Vec2(1, 3), |
581 | | vec![vec![1, 0, 0, 0], vec![0, 0, 0, 0], vec![0, 0, 0, 0]], |
582 | | vec![vec![1]], |
583 | | ); |
584 | | |
585 | | assert_found_smaller_bounds( |
586 | | Vec2(1, 3), |
587 | | vec![vec![0, 0, 0, 0], vec![0, 1, 0, 0], vec![0, 0, 0, 0]], |
588 | | vec![vec![1]], |
589 | | ); |
590 | | |
591 | | assert_found_smaller_bounds( |
592 | | Vec2(-1, -3), |
593 | | vec![vec![0, 0, 0, 0], vec![0, 0, 0, 1], vec![0, 0, 0, 0]], |
594 | | vec![vec![1]], |
595 | | ); |
596 | | |
597 | | assert_found_smaller_bounds( |
598 | | Vec2(-1, -3), |
599 | | vec![ |
600 | | vec![0, 0, 0, 0, 0, 0, 0], |
601 | | vec![0, 0, 0, 0, 0, 0, 0], |
602 | | vec![0, 0, 1, 1, 1, 0, 0], |
603 | | vec![0, 0, 1, 1, 1, 0, 0], |
604 | | vec![0, 0, 1, 1, 1, 0, 0], |
605 | | vec![0, 0, 0, 0, 0, 0, 0], |
606 | | vec![0, 0, 0, 0, 0, 0, 0], |
607 | | ], |
608 | | vec![vec![1, 1, 1], vec![1, 1, 1], vec![1, 1, 1]], |
609 | | ); |
610 | | |
611 | | assert_found_smaller_bounds( |
612 | | Vec2(1000, -300), |
613 | | vec![ |
614 | | vec![0, 0, 0, 0, 0, 0, 0], |
615 | | vec![0, 0, 0, 0, 0, 0, 0], |
616 | | vec![0, 0, 1, 1, 1, 0, 0], |
617 | | vec![0, 1, 1, 1, 1, 1, 0], |
618 | | vec![0, 0, 1, 1, 1, 0, 0], |
619 | | vec![0, 0, 0, 0, 0, 0, 0], |
620 | | vec![0, 0, 0, 0, 0, 0, 0], |
621 | | ], |
622 | | vec![vec![0, 1, 1, 1, 0], vec![1, 1, 1, 1, 1], vec![0, 1, 1, 1, 0]], |
623 | | ); |
624 | | |
625 | | assert_found_smaller_bounds( |
626 | | Vec2(-10, -300), |
627 | | vec![ |
628 | | vec![0, 0, 0, 0, 0, 0, 0], |
629 | | vec![0, 0, 0, 0, 0, 0, 0], |
630 | | vec![0, 0, 1, 0, 1, 0, 0], |
631 | | vec![0, 0, 0, 0, 0, 0, 0], |
632 | | vec![0, 0, 1, 0, 1, 0, 0], |
633 | | vec![0, 0, 0, 0, 0, 0, 0], |
634 | | vec![0, 0, 0, 0, 0, 0, 0], |
635 | | ], |
636 | | vec![vec![1, 0, 1], vec![0, 0, 0], vec![1, 0, 1]], |
637 | | ); |
638 | | |
639 | | assert_found_smaller_bounds( |
640 | | Vec2(-10, -300), |
641 | | vec![ |
642 | | vec![0, 0, 0, 0, 0, 0, 0], |
643 | | vec![0, 0, 0, 0, 0, 0, 0], |
644 | | vec![0, 0, 1, 0, 1, 0, 0], |
645 | | vec![0, 0, 0, 0, 0, 0, 0], |
646 | | vec![0, 0, 0, 0, 0, 0, 0], |
647 | | vec![0, 0, 0, 0, 0, 0, 0], |
648 | | vec![0, 0, 0, 0, 0, 0, 0], |
649 | | ], |
650 | | vec![vec![1, 0, 1]], |
651 | | ); |
652 | | |
653 | | assert_found_smaller_bounds( |
654 | | Vec2(-10, -300), |
655 | | vec![ |
656 | | vec![0, 0, 0, 0, 0, 0, 0], |
657 | | vec![0, 0, 0, 1, 0, 0, 0], |
658 | | vec![0, 0, 0, 2, 0, 0, 0], |
659 | | vec![0, 0, 3, 3, 3, 0, 0], |
660 | | vec![0, 0, 0, 4, 0, 0, 0], |
661 | | vec![0, 0, 0, 0, 0, 0, 0], |
662 | | ], |
663 | | vec![vec![0, 1, 0], vec![0, 2, 0], vec![3, 3, 3], vec![0, 4, 0]], |
664 | | ); |
665 | | |
666 | | assert_found_smaller_bounds( |
667 | | Vec2(-10, -300), |
668 | | vec![ |
669 | | vec![0, 0, 0, 0, 0, 0, 0], |
670 | | vec![0, 0, 0, 0, 0, 0, 0], |
671 | | vec![0, 0, 0, 0, 1, 0, 0], |
672 | | vec![0, 0, 0, 0, 0, 0, 0], |
673 | | vec![0, 0, 0, 0, 0, 0, 0], |
674 | | vec![0, 0, 1, 0, 0, 0, 0], |
675 | | vec![0, 0, 0, 0, 0, 0, 0], |
676 | | ], |
677 | | vec![vec![0, 0, 1], vec![0, 0, 0], vec![0, 0, 0], vec![1, 0, 0]], |
678 | | ); |
679 | | |
680 | | assert_found_smaller_bounds( |
681 | | Vec2(-10, -300), |
682 | | vec![ |
683 | | vec![0, 0, 0, 0, 0, 0, 0], |
684 | | vec![0, 0, 0, 0, 0, 0, 0], |
685 | | vec![0, 0, 1, 0, 0, 0, 0], |
686 | | vec![0, 0, 0, 0, 0, 0, 0], |
687 | | vec![0, 0, 0, 0, 0, 1, 0], |
688 | | vec![0, 0, 0, 0, 0, 0, 0], |
689 | | vec![0, 0, 0, 0, 0, 0, 0], |
690 | | ], |
691 | | vec![vec![1, 0, 0, 0], vec![0, 0, 0, 0], vec![0, 0, 0, 1]], |
692 | | ); |
693 | | |
694 | | assert_found_smaller_bounds( |
695 | | Vec2(-10, -300), |
696 | | vec![ |
697 | | vec![0, 0, 0, 0, 0, 0, 0], |
698 | | vec![0, 0, 0, 0, 0, 0, 0], |
699 | | vec![0, 0, 1, 0, 0, 0, 0], |
700 | | vec![0, 0, 0, 0, 0, 0, 0], |
701 | | vec![0, 0, 0, 0, 0, 0, 0], |
702 | | vec![0, 0, 1, 0, 0, 0, 0], |
703 | | vec![0, 0, 0, 0, 0, 0, 0], |
704 | | ], |
705 | | vec![vec![1], vec![0], vec![0], vec![1]], |
706 | | ); |
707 | | |
708 | | assert_found_smaller_bounds( |
709 | | Vec2(-1, -3), |
710 | | vec![vec![0, 0, 1, 0], vec![0, 0, 0, 1], vec![0, 0, 0, 0]], |
711 | | vec![vec![1, 0], vec![0, 1]], |
712 | | ); |
713 | | |
714 | | assert_found_smaller_bounds( |
715 | | Vec2(-1, -3), |
716 | | vec![vec![1, 0, 0, 0], vec![0, 1, 0, 0], vec![0, 0, 0, 0], vec![0, 0, 0, 0]], |
717 | | vec![vec![1, 0], vec![0, 1]], |
718 | | ); |
719 | | } |
720 | | |
721 | | #[test] |
722 | | fn find_no_bounds() { |
723 | | let pixels = vec![vec![0, 0, 0, 0], vec![0, 0, 0, 0], vec![0, 0, 0, 0]]; |
724 | | |
725 | | let bounds = try_find_smaller_bounds(IntegerBounds::new((0, 0), (4, 3)), |position| { |
726 | | pixels[position.y()][position.x()] != 0 |
727 | | }); |
728 | | |
729 | | assert_eq!(bounds, None) |
730 | | } |
731 | | } |