Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/images/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,67 @@ where
pub fn put_pixel(&mut self, x: u32, y: u32, pixel: P) {
*self.get_pixel_mut(x, y) = pixel;
}

/// Crop this image in place, removing pixels outside of the bounding rectangle.
///
/// This behaves similar to [`imageops::crop`](crate::imageops::crop) except no additional
/// allocation takes place. The width and height of this buffer are adjusted as part of the
/// operation. The selection is shrunk to the overlap with this image if it is out of bounds.
///
/// The pixel buffer is *not* shrunk and will continue to occupy the same amount of memory as
/// before. See [`ImageBuffer::shrink_to_fit`] if the container is a [`Vec`].
///
/// # Examples
///
/// ```
/// use image::{RgbImage, math::Rect};
///
/// let mut img = RgbImage::new(128, 128);
/// img.put_pixel(64, 64, image::Rgb([255, 0, 0]));
///
/// let selection = Rect::from_xy_ranges(64..128, 64..128);
/// img.crop_in_place(selection);
///
/// assert_eq!(img.dimensions(), (64, 64));
/// assert_eq!(img.get_pixel(0, 0), &image::Rgb([255, 0, 0]));
/// ```
///
/// Selections beyond the image bounds are clamped.
///
/// ```
/// use image::{RgbImage, math::Rect};
///
/// let mut img = RgbImage::new(32, 32);
/// let selection = Rect::from_xy_ranges(16..40, 16..24);
/// # use image::GenericImageView as _;
/// # assert_eq!(image::imageops::crop_imm(&img, selection).dimensions(), (16, 8));
///
/// img.crop_in_place(selection);
/// assert_eq!(img.dimensions(), (16, 8));
/// ```
pub fn crop_in_place(&mut self, selection: Rect) {
let selection = selection.crop_dimms(self);
// We're now running essentially `copy_within` with differing source and destination row
// pitches. The above ensures that the target row pitch is smaller than our current one and
// we copy to offset `0` so always all data backwards.
//
// Since `selection` describes a smaller layout than our own, all indices that are computed
// from the pitches as type `usize` are within the bounds of the type and the underlying
// storage and we assume them to be valid. (If the `DerefMut` is malicious you'll get
// panics at runtime but that is not our problem, violating the contract of the container
// type for channels).
let rowlen = (selection.width as usize) * usize::from(<P as Pixel>::CHANNEL_COUNT);

for y in 0..selection.height {
let sy = selection.y + y;
let source = self.pixel_indices_unchecked(selection.x, sy).start;
let dst = y as usize * rowlen;
self.data.copy_within(source..source + rowlen, dst);
}

self.width = selection.width;
self.height = selection.height;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: we can't shrink the container here as this is generic over it. We could do it in DynamicImage on top of the original transform to avoid issues as rust-lang/rust#120091 even though additional conditions may be necessary for the behavior to be considered particular surprising (i.e. the linked issues is because an internal use of vec reserve/truncate leaves it with a lot of capacity, an analogy would thus be an operation that internally crops, but for instance ImageReader #2672 could truncate itself quite well).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's at least introduce the necessary method for this in the same patch.

}

impl<P: Pixel, Container> ImageBuffer<P, Container> {
Expand Down Expand Up @@ -1419,6 +1480,27 @@ impl<P: Pixel> ImageBuffer<P, Vec<P::Subpixel>> {
self.into_raw()
}

/// Shrink the length and capacity of the data buffer to fit the image size.
///
/// This is useful after shrinking an image in-place, e.g. via cropping, to free unused memory
/// or in case the image was created from a buffer with excess capacity.
///
/// ```
/// use image::RgbImage;
///
/// let data = vec![0u8; 10000];
/// // `from_raw` allows excess data
/// let mut img = RgbImage::from_raw(16, 16, data).unwrap();
/// img.shrink_to_fit();
///
/// assert_eq!(img.into_vec().len(), 16 * 16 * 3);
/// ```
pub fn shrink_to_fit(&mut self) {
let need = self.inner_pixels().len();
self.data.truncate(need);
self.data.shrink_to_fit();
}

/// Transfer the meta data, not the pixel values.
///
/// This will reinterpret all the pixels.
Expand Down
17 changes: 17 additions & 0 deletions src/images/dynimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,15 @@ impl DynamicImage {
dynamic_map!(*self, ref p => imageops::crop_imm(p, selection).to_image())
}

/// Crop this image in place, removing pixels outside of the bounding rectangle.
///
/// See [`ImageBuffer::crop_in_place`] for more details. This changes the image with its
/// current color type. The pixel buffer is *not* shrunk and will continue to occupy the same
/// amount of memory as before. See [`Self::shrink_to_fit`].
pub fn crop_in_place(&mut self, selection: Rect) {
dynamic_map!(self, ref mut p, p.crop_in_place(selection))
}

/// Return a reference to an 8bit RGB image
#[must_use]
pub fn as_rgb8(&self) -> Option<&RgbImage> {
Expand Down Expand Up @@ -715,6 +724,14 @@ impl DynamicImage {
)
}

/// Shrink the capacity of the underlying [`Vec`] buffer to fit its length.
///
/// The data may have excess capacity or padding for a number of reasons, depending on how it
/// was created or from in-place manipulation such as [`Self::crop_in_place`].
pub fn shrink_to_fit(&mut self) {
dynamic_map!(self, ref mut p, p.shrink_to_fit());
}

/// Return this image's pixels as a byte vector. If the `ImageBuffer`
/// container is `Vec<u8>`, this operation is free. Otherwise, a copy
/// is returned.
Expand Down
Loading