Skip to main content

otf_pixels_ops/
extend.rs

1//! [`Extend`] — grow the canvas by a margin on each side (sharp's `extend`).
2//!
3//! A layout op like [`crate::Crop`]: every output pixel is either an input
4//! pixel, moved, or the background colour. Whole pixels are copied as opaque
5//! byte runs, so one implementation serves every pixel format.
6//!
7//! # The four ways to fill a margin
8//!
9//! sharp's `extendWith`, which is libvips' `extend`:
10//!
11//! - [`ExtendWith::Background`] paints the margin one colour.
12//! - [`ExtendWith::Copy`] repeats the edge pixel outwards.
13//! - [`ExtendWith::Repeat`] tiles the image, so the left margin shows the
14//!   image's right edge.
15//! - [`ExtendWith::Mirror`] tiles it reflected, the edge pixel appearing
16//!   twice: one pixel left of the image is its first column, not its second.
17
18use 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/// What fills the margin [`Extend`] adds.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
28#[non_exhaustive]
29pub enum ExtendWith {
30    /// One colour, [`Extend::with_background`]'s.
31    #[default]
32    Background,
33    /// The nearest edge pixel, repeated outwards.
34    Copy,
35    /// The image tiled.
36    Repeat,
37    /// The image tiled and reflected, so each copy meets its neighbour at a
38    /// mirrored edge.
39    Mirror,
40}
41
42impl ExtendWith {
43    /// The input coordinate an output coordinate `at` takes its pixel from,
44    /// on an axis where the image starts at `before` and is `size` long;
45    /// `None` for the background.
46    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    /// The input span `start..end` that output span `from..to` reads, or
62    /// `None` if it is all background.
63    fn span(self, from: u32, to: u32, before: u32, size: u32) -> Option<(u32, u32)> {
64        // A span inside the image maps to itself. Only one touching a margin
65        // needs the walk below, which is a tile's width long.
66        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/// Add `top`, `right`, `bottom` and `left` pixels around an image.
82///
83/// Like [`crate::Resize`], an instance belongs to one graph edge: copy,
84/// repeat and mirror wrap around the input's size, which is bound when the
85/// op is chained, and chaining the same instance onto a differently sized
86/// image is a [`PixelsError::Graph`].
87#[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    /// The input's width and height, filled by `output_descriptor`.
96    bound: OnceLock<(u32, u32)>,
97}
98
99impl Clone for Extend {
100    fn clone(&self) -> Self {
101        // Not the binding: a clone may be chained onto another image.
102        Self {
103            bound: OnceLock::new(),
104            ..*self
105        }
106    }
107}
108
109impl Extend {
110    /// Margins of `top`, `right`, `bottom` and `left` pixels, filled with
111    /// opaque black, sharp's default.
112    #[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    /// The same margin on every side (sharp's `extend(n)`).
126    #[must_use]
127    pub const fn uniform(margin: u32) -> Self {
128        Self::new(margin, margin, margin, margin)
129    }
130
131    /// Fill the margin by `with` instead of the background colour.
132    #[must_use]
133    pub const fn extend_with(mut self, with: ExtendWith) -> Self {
134        self.with = with;
135        self
136    }
137
138    /// The RGBA colour [`ExtendWith::Background`] paints. Grey formats take
139    /// its luma, and formats without alpha ignore its alpha.
140    #[must_use]
141    pub const fn with_background(mut self, rgba: [u8; 4]) -> Self {
142        self.background = rgba;
143        self
144    }
145
146    /// The margins, as `(top, right, bottom, left)`.
147    #[must_use]
148    pub const fn margins(&self) -> (u32, u32, u32, u32) {
149        (self.top, self.right, self.bottom, self.left)
150    }
151
152    /// How the margin is filled.
153    #[must_use]
154    pub const fn method(&self) -> ExtendWith {
155        self.with
156    }
157
158    /// The background colour.
159    #[must_use]
160    pub const fn background(&self) -> [u8; 4] {
161        self.background
162    }
163
164    /// Bind to `input`'s size, or check it matches the size already bound.
165    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
178/// Fetch the sole input descriptor.
179fn 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    /// Never rescaled: the margins are in output pixels, and a margin of 10
187    /// around a source decoded eight times smaller would be 80 at full size.
188    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            // A tile of pure margin still has to name a valid region, so it
223            // asks for one pixel it will not read.
224            _ => Region::new(0, 0, 1, 1),
225        }])
226    }
227
228    fn access_pattern(&self) -> AccessPattern {
229        // One input row per output row, as for crop.
230        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        // Where each output column reads from, as a byte offset into an input
248        // row, or `None` for background. The same for every row of the tile.
249        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        // A tile whose columns are all margin was given a placeholder input,
258        // and reads no row of it.
259        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            // Columns that read consecutive input pixels are copied as one
272            // run, which is the whole image part of every row.
273            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    /// Extend a whole image.
315    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    /// A one-row grey image `[10, 20, 30]` extended by four on the left and
337    /// right.
338    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        // The edge pixel appears twice where a copy meets its reflection.
361        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        // A clone starts unbound.
465        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}