1use otf_pixels_core::{
19 AccessPattern, ImageDescriptor, Op, PixelsError, Region, Result, Tile, TileMut,
20};
21
22use std::sync::OnceLock;
23
24use crate::resize::background_pixel;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
28#[non_exhaustive]
29pub enum ExtendWith {
30 #[default]
32 Background,
33 Copy,
35 Repeat,
37 Mirror,
40}
41
42impl ExtendWith {
43 fn source(self, at: u32, before: u32, size: u32) -> Option<u32> {
47 let i = i64::from(at) - i64::from(before);
48 let n = i64::from(size);
49 let mapped = match self {
50 Self::Background => (0..n).contains(&i).then_some(i)?,
51 Self::Copy => i.clamp(0, n - 1),
52 Self::Repeat => i.rem_euclid(n),
53 Self::Mirror => {
54 let m = i.rem_euclid(2 * n);
55 if m < n { m } else { 2 * n - 1 - m }
56 }
57 };
58 u32::try_from(mapped).ok()
59 }
60
61 fn span(self, from: u32, to: u32, before: u32, size: u32) -> Option<(u32, u32)> {
64 if from >= before && to <= before.saturating_add(size) {
67 return Some((from - before, to - before));
68 }
69 let mut low = u32::MAX;
70 let mut high = 0;
71 for at in from..to {
72 if let Some(source) = self.source(at, before, size) {
73 low = low.min(source);
74 high = high.max(source + 1);
75 }
76 }
77 (low < high).then_some((low, high))
78 }
79}
80
81#[derive(Debug)]
88pub struct Extend {
89 top: u32,
90 right: u32,
91 bottom: u32,
92 left: u32,
93 with: ExtendWith,
94 background: [u8; 4],
95 bound: OnceLock<(u32, u32)>,
97}
98
99impl Clone for Extend {
100 fn clone(&self) -> Self {
101 Self {
103 bound: OnceLock::new(),
104 ..*self
105 }
106 }
107}
108
109impl Extend {
110 #[must_use]
113 pub const fn new(top: u32, right: u32, bottom: u32, left: u32) -> Self {
114 Self {
115 top,
116 right,
117 bottom,
118 left,
119 with: ExtendWith::Background,
120 background: [0, 0, 0, 255],
121 bound: OnceLock::new(),
122 }
123 }
124
125 #[must_use]
127 pub const fn uniform(margin: u32) -> Self {
128 Self::new(margin, margin, margin, margin)
129 }
130
131 #[must_use]
133 pub const fn extend_with(mut self, with: ExtendWith) -> Self {
134 self.with = with;
135 self
136 }
137
138 #[must_use]
141 pub const fn with_background(mut self, rgba: [u8; 4]) -> Self {
142 self.background = rgba;
143 self
144 }
145
146 #[must_use]
148 pub const fn margins(&self) -> (u32, u32, u32, u32) {
149 (self.top, self.right, self.bottom, self.left)
150 }
151
152 #[must_use]
154 pub const fn method(&self) -> ExtendWith {
155 self.with
156 }
157
158 #[must_use]
160 pub const fn background(&self) -> [u8; 4] {
161 self.background
162 }
163
164 fn bind(&self, input: &ImageDescriptor) -> Result<(u32, u32)> {
166 let size = *self.bound.get_or_init(|| (input.width, input.height));
167 if size != (input.width, input.height) {
168 return Err(PixelsError::graph(format!(
169 "`extend` is bound to a {}x{} input but was given {}x{}; \
170 build a new Extend per image",
171 size.0, size.1, input.width, input.height
172 )));
173 }
174 Ok(size)
175 }
176}
177
178fn sole(inputs: &[ImageDescriptor]) -> Result<&ImageDescriptor> {
180 inputs
181 .first()
182 .ok_or_else(|| PixelsError::graph("`extend` takes one input, got none"))
183}
184
185impl Op for Extend {
186 fn rescaled(&self) -> Option<std::sync::Arc<dyn Op>> {
189 None
190 }
191
192 fn name(&self) -> &'static str {
193 "extend"
194 }
195
196 fn output_descriptor(&self, inputs: &[ImageDescriptor]) -> Result<ImageDescriptor> {
197 let input = sole(inputs)?;
198 self.bind(input)?;
199 let grow = |size: u32, a: u32, b: u32| {
200 size.checked_add(a)
201 .and_then(|s| s.checked_add(b))
202 .ok_or_else(|| {
203 PixelsError::invalid_argument("margin", "the extended image is too large")
204 })
205 };
206 input.resized(
207 grow(input.width, self.left, self.right)?,
208 grow(input.height, self.top, self.bottom)?,
209 )
210 }
211
212 fn input_regions(&self, output: Region, inputs: &[ImageDescriptor]) -> Result<Vec<Region>> {
213 let (width, height) = self.bind(sole(inputs)?)?;
214 let columns = self
215 .with
216 .span(output.x, output.x + output.width, self.left, width);
217 let rows = self
218 .with
219 .span(output.y, output.y + output.height, self.top, height);
220 Ok(vec![match (columns, rows) {
221 (Some((x0, x1)), Some((y0, y1))) => Region::new(x0, y0, x1 - x0, y1 - y0),
222 _ => Region::new(0, 0, 1, 1),
225 }])
226 }
227
228 fn access_pattern(&self) -> AccessPattern {
229 AccessPattern::Sequential
231 }
232
233 fn compute(&self, inputs: &[Tile<'_>], output: &mut TileMut<'_>) -> Result<()> {
234 let input = inputs
235 .first()
236 .ok_or_else(|| PixelsError::graph("`extend` takes one input tile, got none"))?;
237 let (width, height) = self
238 .bound
239 .get()
240 .copied()
241 .ok_or_else(|| PixelsError::graph("`extend` computed before it was chained"))?;
242 let bpp = output.pixel().bytes_per_pixel();
243 let fill = background_pixel(output.pixel(), self.background);
244 let region = output.region();
245 let area = input.region();
246
247 let columns: Vec<Option<usize>> = (region.x..region.x + region.width)
250 .map(|x| {
251 self.with
252 .source(x, self.left, width)
253 .map(|sx| (sx - area.x) as usize * bpp)
254 })
255 .collect();
256
257 let reads = columns.iter().any(Option::is_some);
260
261 for y in region.y..region.y + region.height {
262 let source = match self.with.source(y, self.top, height).filter(|_| reads) {
263 Some(sy) => Some(input.row(sy).ok_or_else(|| {
264 PixelsError::graph(format!("extend input {area} is missing row {sy}"))
265 })?),
266 None => None,
267 };
268 let target = output
269 .row_mut(y)
270 .ok_or_else(|| PixelsError::graph(format!("extend output is missing row {y}")))?;
271 let mut i = 0;
274 while i < columns.len() {
275 let start = columns.get(i).copied().flatten();
276 let (Some(row), Some(start)) = (source, start) else {
277 if let Some(slot) = target.get_mut(i * bpp..(i + 1) * bpp) {
278 slot.copy_from_slice(&fill);
279 }
280 i += 1;
281 continue;
282 };
283 let mut end = i + 1;
284 while columns.get(end).copied().flatten() == Some(start + (end - i) * bpp) {
285 end += 1;
286 }
287 let span = (end - i) * bpp;
288 let (Some(from), Some(into)) = (
289 row.get(start..start + span),
290 target.get_mut(i * bpp..i * bpp + span),
291 ) else {
292 return Err(PixelsError::graph(
293 "extend input row is too short for its tile",
294 ));
295 };
296 into.copy_from_slice(from);
297 i = end;
298 }
299 }
300 Ok(())
301 }
302}
303
304#[cfg(test)]
305#[allow(
306 clippy::unwrap_used,
307 clippy::indexing_slicing,
308 reason = "tests operate on known-good values and assert shapes directly"
309)]
310mod tests {
311 use super::*;
312 use otf_pixels_core::{PixelFormat, TileBuf, copy_region};
313
314 fn apply(op: &Extend, descriptor: ImageDescriptor, bytes: &[u8]) -> (ImageDescriptor, Vec<u8>) {
316 let out = op.output_descriptor(&[descriptor]).unwrap();
317 let source =
318 TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes.to_vec()).unwrap();
319 let demand = op.input_regions(out.region(), &[descriptor]).unwrap();
320 let mut cut = TileBuf::zeroed(demand[0], descriptor.pixel).unwrap();
321 copy_region(
322 &source.as_tile().unwrap(),
323 &mut cut.as_tile_mut().unwrap(),
324 demand[0],
325 )
326 .unwrap();
327 let mut target = TileBuf::for_image(&out).unwrap();
328 op.compute(
329 &[cut.as_tile().unwrap()],
330 &mut target.as_tile_mut().unwrap(),
331 )
332 .unwrap();
333 (out, target.into_bytes())
334 }
335
336 fn row(with: ExtendWith) -> Vec<u8> {
339 let descriptor = ImageDescriptor::new(3, 1, PixelFormat::Gray8).unwrap();
340 let op = Extend::new(0, 4, 0, 4)
341 .extend_with(with)
342 .with_background([1, 1, 1, 255]);
343 apply(&op, descriptor, &[10, 20, 30]).1
344 }
345
346 #[test]
347 fn each_method_fills_the_margin_as_libvips_does() {
348 assert_eq!(
349 row(ExtendWith::Background),
350 [1, 1, 1, 1, 10, 20, 30, 1, 1, 1, 1]
351 );
352 assert_eq!(
353 row(ExtendWith::Copy),
354 [10, 10, 10, 10, 10, 20, 30, 30, 30, 30, 30]
355 );
356 assert_eq!(
357 row(ExtendWith::Repeat),
358 [30, 10, 20, 30, 10, 20, 30, 10, 20, 30, 10]
359 );
360 assert_eq!(
362 row(ExtendWith::Mirror),
363 [30, 30, 20, 10, 10, 20, 30, 30, 20, 10, 10]
364 );
365 }
366
367 #[test]
368 fn the_background_is_placed_around_the_image() {
369 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Rgba8).unwrap();
370 let op = Extend::new(1, 2, 3, 4).with_background([9, 8, 7, 6]);
371 let (out, bytes) = apply(&op, descriptor, &[255; 16]);
372 assert_eq!((out.width, out.height), (8, 6));
373 let at = |x: usize, y: usize| &bytes[(y * 8 + x) * 4..(y * 8 + x) * 4 + 4];
374 assert_eq!(at(0, 0), [9, 8, 7, 6]);
375 assert_eq!(at(4, 1), [255; 4], "the image's top-left");
376 assert_eq!(at(5, 2), [255; 4], "the image's bottom-right");
377 assert_eq!(at(6, 2), [9, 8, 7, 6]);
378 assert_eq!(at(4, 3), [9, 8, 7, 6]);
379 }
380
381 #[test]
382 fn wide_formats_take_the_background_at_their_depth() {
383 let descriptor = ImageDescriptor::new(1, 1, PixelFormat::Rgb16).unwrap();
384 let op = Extend::new(0, 1, 0, 0).with_background([255, 0, 0, 255]);
385 let (_, bytes) = apply(&op, descriptor, &[0; 6]);
386 let red = u16::from_ne_bytes([bytes[6], bytes[7]]);
387 assert_eq!(red, 65535);
388 }
389
390 #[test]
391 fn a_margin_wider_than_the_image_wraps_more_than_once() {
392 let descriptor = ImageDescriptor::new(2, 1, PixelFormat::Gray8).unwrap();
393 let op = Extend::new(0, 5, 0, 5).extend_with(ExtendWith::Mirror);
394 let (_, bytes) = apply(&op, descriptor, &[1, 2]);
395 assert_eq!(bytes, [1, 1, 2, 2, 1, 1, 2, 2, 1, 1, 2, 2]);
396 }
397
398 #[test]
399 fn the_output_is_independent_of_how_the_image_is_tiled() {
400 let descriptor = ImageDescriptor::new(7, 5, PixelFormat::Rgb8).unwrap();
401 let bytes: Vec<u8> = (0..7 * 5 * 3).map(|i| (i * 37 % 251) as u8).collect();
402 let source =
403 TileBuf::from_vec(descriptor.region(), descriptor.pixel, bytes.clone()).unwrap();
404 for with in [
405 ExtendWith::Background,
406 ExtendWith::Copy,
407 ExtendWith::Repeat,
408 ExtendWith::Mirror,
409 ] {
410 let op = Extend::new(3, 9, 6, 2).extend_with(with);
411 let (out, whole) = apply(&op, descriptor, &bytes);
412 for (tw, th) in [(4_u32, 4_u32), (1, 1), (18, 1), (5, 3)] {
413 let mut target = TileBuf::for_image(&out).unwrap();
414 for y in (0..out.height).step_by(th as usize) {
415 for x in (0..out.width).step_by(tw as usize) {
416 let region =
417 Region::new(x, y, tw.min(out.width - x), th.min(out.height - y));
418 let demand = op.input_regions(region, &[descriptor]).unwrap();
419 let mut cut = TileBuf::zeroed(demand[0], descriptor.pixel).unwrap();
420 copy_region(
421 &source.as_tile().unwrap(),
422 &mut cut.as_tile_mut().unwrap(),
423 demand[0],
424 )
425 .unwrap();
426 let mut sub = TileBuf::zeroed(region, out.pixel).unwrap();
427 op.compute(&[cut.as_tile().unwrap()], &mut sub.as_tile_mut().unwrap())
428 .unwrap();
429 copy_region(
430 &sub.as_tile().unwrap(),
431 &mut target.as_tile_mut().unwrap(),
432 region,
433 )
434 .unwrap();
435 }
436 }
437 assert_eq!(target.bytes(), whole.as_slice(), "{with:?} at {tw}x{th}");
438 }
439 }
440 }
441
442 #[test]
443 fn a_tile_of_pure_margin_asks_for_almost_nothing() {
444 let descriptor = ImageDescriptor::new(100, 100, PixelFormat::Rgb8).unwrap();
445 let op = Extend::uniform(50);
446 op.output_descriptor(&[descriptor]).unwrap();
447 let demand = op
448 .input_regions(Region::new(0, 0, 40, 40), &[descriptor])
449 .unwrap();
450 assert_eq!(demand[0], Region::new(0, 0, 1, 1));
451 let demand = op
452 .input_regions(Region::new(60, 60, 10, 10), &[descriptor])
453 .unwrap();
454 assert_eq!(demand[0], Region::new(10, 10, 10, 10));
455 }
456
457 #[test]
458 fn one_instance_on_two_shapes_is_a_graph_error() {
459 let op = Extend::uniform(1);
460 let small = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
461 let large = ImageDescriptor::new(3, 3, PixelFormat::Gray8).unwrap();
462 op.output_descriptor(&[small]).unwrap();
463 assert!(op.output_descriptor(&[large]).is_err());
464 assert!(op.clone().output_descriptor(&[large]).is_ok());
466 }
467
468 #[test]
469 fn an_overflowing_size_is_an_invalid_argument() {
470 let descriptor = ImageDescriptor::new(2, 2, PixelFormat::Gray8).unwrap();
471 assert!(
472 Extend::new(0, u32::MAX, 0, 0)
473 .output_descriptor(&[descriptor])
474 .is_err()
475 );
476 }
477}