pineapple_core/im/
buffer.rs

1// Copyright (c) 2025, Tom Ouellette
2// Licensed under the BSD 3-Clause License
3
4use std::iter::Iterator;
5use std::marker::PhantomData;
6use std::ops::Deref;
7use std::slice::ChunksExact;
8
9use num::{FromPrimitive, ToPrimitive};
10
11use crate::error::PineappleError;
12use crate::im::{MaskingStyle, PineappleMaskView, PineappleViewBuffer};
13
14/// A row-major container storing an image buffer or grid of pixels.
15///
16/// The struct is generic over the data type `T` and over the container that
17/// holds raw pixel/subpixel data as a slice (`[T]`) or vector (`Vec<T>`).
18/// The container holding the pixel data must implement `Deref<Target = [T]>`
19/// to allow for slice-like access to the data. The length of the container
20/// must also be equal to the product of `w` * `h` * `c`.
21///
22/// # Examples
23///
24/// ```
25/// use pineapple_core::im::PineappleBuffer;
26///
27/// let width = 10;
28/// let height = 10;
29/// let channels = 3; // RGB
30/// let data = vec![0u8; (width * height * channels) as usize];
31///
32/// let buffer = PineappleBuffer::new(width, height, channels, data);
33///
34/// assert_eq!(buffer.unwrap().len(), (width * height * channels) as usize);
35/// ```
36///
37/// ```
38/// use pineapple_core::im::PineappleBuffer;
39///
40/// let width = 10;
41/// let height = 10;
42/// let channels = 3; // RGB
43/// let data = vec![0u8; (width * height * 3 * channels) as usize];
44///
45/// let buffer = PineappleBuffer::new(width, height, channels, data);
46///
47/// assert!(buffer.is_err()); // Buffer size does not match dimensions
48/// ```
49#[derive(Debug, Clone)]
50pub struct PineappleBuffer<T, Container> {
51    w: u32,                   // Width
52    h: u32,                   // Height
53    c: u32,                   // Channels
54    pub buffer: Container,    // Slice
55    _phantom: PhantomData<T>, // Pixel
56}
57
58impl<T, Container> PineappleBuffer<T, Container>
59where
60    T: ToPrimitive + FromPrimitive,
61    Container: Deref<Target = [T]>,
62{
63    /// Initializes a buffer from a generic data container
64    ///
65    /// # Arguments
66    ///
67    /// * `width` - Image width
68    /// * `height` - Image height
69    /// * `channels` - Number of image channels (e.g. 1 for grayscale)
70    /// * `buffer` - A generic container (e.g. `Vec` or slice)
71    ///
72    /// # Examples
73    ///
74    /// ```no_run
75    /// use pineapple_core::im::PineappleBuffer;
76    /// let buffer = [0, 1, 2, 3, 4];
77    /// let buffer = PineappleBuffer::new(2, 2, 1, buffer.as_slice());
78    /// ```
79    pub fn new(
80        width: u32,
81        height: u32,
82        channels: u32,
83        buffer: Container,
84    ) -> Result<PineappleBuffer<T, Container>, PineappleError> {
85        if width * height * channels == buffer.len() as u32 {
86            Ok(PineappleBuffer {
87                w: width,
88                h: height,
89                c: channels,
90                buffer,
91                _phantom: PhantomData,
92            })
93        } else {
94            Err(PineappleError::BufferSizeError)
95        }
96    }
97}
98
99// >>> PROPERTY METHODS
100
101impl<T, Container> PineappleBuffer<T, Container>
102where
103    T: ToPrimitive + FromPrimitive,
104    Container: Deref<Target = [T]>,
105{
106    /// Width of the image
107    pub fn width(&self) -> u32 {
108        self.w
109    }
110
111    /// Height of the image
112    pub fn height(&self) -> u32 {
113        self.h
114    }
115
116    /// Number of channels in the image
117    pub fn channels(&self) -> u32 {
118        self.c
119    }
120
121    /// Shape/dimensions of the image
122    pub fn shape(&self) -> (u32, u32, u32) {
123        (self.h, self.w, self.c)
124    }
125
126    /// Length of the raw image
127    pub fn len(&self) -> usize {
128        (self.w * self.h * self.c) as usize
129    }
130
131    /// Check if buffer is empty
132    pub fn is_empty(&self) -> bool {
133        self.len() == 0
134    }
135}
136
137// <<< PROPERTY METHODS
138
139// >>> CONVERSION METHODS
140
141impl<T, Container> PineappleBuffer<T, Container>
142where
143    T: ToPrimitive + FromPrimitive,
144    Container: Deref<Target = [T]>,
145{
146    /// Returns the raw image
147    pub fn into_raw(self) -> Container {
148        self.buffer
149    }
150
151    /// Returns a reference to the raw image
152    pub fn as_raw(&self) -> &Container {
153        &self.buffer
154    }
155
156    /// Cast subpixels to u8 and return the buffer
157    pub fn to_u8(&self) -> Vec<u8> {
158        self.buffer
159            .iter()
160            .map(|x| x.to_u8().unwrap_or(0u8))
161            .collect()
162    }
163
164    /// Cast subpixels to u16 and return the buffer
165    pub fn to_u16(&self) -> Vec<u16> {
166        self.buffer
167            .iter()
168            .map(|x| x.to_u16().unwrap_or(0u16))
169            .collect()
170    }
171
172    /// Cast subpixels to u16 and return the buffer
173    pub fn to_u32(&self) -> Vec<u32> {
174        self.buffer
175            .iter()
176            .map(|x| x.to_u32().unwrap_or(0u32))
177            .collect()
178    }
179
180    /// Cast subpixels to f32 and return the buffer
181    pub fn to_f32(&self) -> Vec<f32> {
182        self.buffer
183            .iter()
184            .map(|x| x.to_f32().unwrap_or(0f32))
185            .collect()
186    }
187
188    /// Cast subpixels to f64 and return the buffer
189    pub fn to_f64(&self) -> Vec<f64> {
190        self.buffer
191            .iter()
192            .map(|x| x.to_f64().unwrap_or(0f64))
193            .collect()
194    }
195
196    // An iterator over the raw buffer
197    pub fn iter(&self) -> impl Iterator<Item = &T> {
198        self.buffer.iter()
199    }
200
201    // An iterator over a raw channel buffer
202    pub fn iter_channel(&self, channel: u32) -> Result<impl Iterator<Item = &T>, PineappleError>
203    where
204        Container: Deref<Target = [T]>,
205    {
206        if channel >= self.channels() {
207            return Err(PineappleError::ChannelBoundsError);
208        }
209
210        Ok(self
211            .iter()
212            .skip(channel as usize)
213            .step_by(self.channels() as usize))
214    }
215
216    // An iterator over pixel-level chunks of the raw buffer
217    pub fn iter_pixels(&self) -> ChunksExact<'_, T> {
218        self.buffer.chunks_exact(self.channels() as usize)
219    }
220}
221
222// <<< CONVERSION METHODS
223
224// >>> TRANSFORM METHODS
225
226impl<T, Container> PineappleBuffer<T, Container>
227where
228    Container: Deref<Target = [T]> + FromIterator<T>,
229    T: Clone + ToPrimitive + FromPrimitive,
230{
231    /// Generate a zero-copy crop of an image subregion
232    ///
233    /// # Arguments
234    ///
235    /// * `x` - Minimum x-coordinate (left)
236    /// * `y` - Minimum y-coordinate (bottom)
237    /// * `w` - Width of crop
238    /// * `h` - Height of crop
239    pub fn crop_view(&self, x: u32, y: u32, w: u32, h: u32) -> PineappleViewBuffer<'_, T, Container> {
240        PineappleViewBuffer::new(x, y, w, h, self)
241    }
242
243    /// Create a new buffer with copied cropped contents
244    ///
245    /// # Arguments
246    ///
247    /// * `x` - Minimum x-coordinate (left)
248    /// * `y` - Minimum y-coordinate (bottom)
249    /// * `w` - Width of crop
250    /// * `h` - Height of crop
251    pub fn crop(
252        &self,
253        x: u32,
254        y: u32,
255        w: u32,
256        h: u32,
257    ) -> Result<PineappleBuffer<T, Container>, PineappleError> {
258        if x + w > self.w || y + h > self.h {
259            return Err(PineappleError::ImageError("Cropping coordinates out of bounds"));
260        }
261
262        let c = self.c as usize;
263        let orig_w = self.w as usize;
264        let orig_buffer = self.buffer.as_ref();
265
266        let mut new_buffer = Vec::with_capacity((w * h * self.c) as usize);
267
268        for row in y..y + h {
269            let start = ((row as usize) * orig_w + (x as usize)) * c;
270            let end = start + (w as usize) * c;
271            new_buffer.extend_from_slice(&orig_buffer[start..end]);
272        }
273
274        Ok(PineappleBuffer {
275            w,
276            h,
277            c: self.c,
278            buffer: Container::from_iter(new_buffer),
279            _phantom: PhantomData,
280        })
281    }
282
283    /// Crops the buffer while applying a mask to either foreground or background pixels
284    ///
285    /// # Arguments
286    ///
287    /// * `x` - Minimum x-coordinate (left)
288    /// * `y` - Minimum y-coordinate (bottom)
289    /// * `w` - Width of crop
290    /// * `h` - Height of crop
291    /// * `mask` - A cropped mask view
292    /// * `mask_style` - Foreground or background masking style
293    pub fn crop_masked(
294        &self,
295        x: u32,
296        y: u32,
297        w: u32,
298        h: u32,
299        mask: &PineappleMaskView,
300        mask_style: MaskingStyle,
301    ) -> Result<PineappleBuffer<T, Container>, PineappleError> {
302        let crop = self.crop_view(x, y, w, h);
303        let mut masked = Vec::with_capacity(crop.len());
304
305        match mask_style {
306            MaskingStyle::Foreground => {
307                for (m, b) in mask.iter().zip(crop.iter_pixels()) {
308                    if m == &0u32 {
309                        for _ in 0..self.c {
310                            masked.push(T::from_u32(0u32).unwrap());
311                        }
312                    } else {
313                        masked.extend_from_slice(b);
314                    }
315                }
316            }
317            MaskingStyle::Background => {
318                for (m, b) in mask.iter().zip(crop.iter_pixels()) {
319                    if m != &0u32 {
320                        for _ in 0..self.c {
321                            masked.push(T::from_u32(0u32).unwrap());
322                        }
323                    } else {
324                        masked.extend_from_slice(b);
325                    }
326                }
327            }
328        }
329
330        Ok(PineappleBuffer {
331            w,
332            h,
333            c: self.c,
334            buffer: Container::from_iter(masked),
335            _phantom: PhantomData,
336        })
337    }
338}
339
340// <<< TRANSFORM METHODS
341
342#[cfg(test)]
343mod test {
344
345    use super::*;
346
347    #[test]
348    fn test_buffer_new_success() {
349        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
350        assert!(buffer.is_ok());
351    }
352
353    #[test]
354    fn test_buffer_new_error() {
355        let buffer = PineappleBuffer::new(2, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
356        assert!(buffer.is_err());
357    }
358
359    #[test]
360    fn test_buffer_width() {
361        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
362        assert_eq!(buffer.unwrap().width(), 1);
363    }
364
365    #[test]
366    fn test_buffer_height() {
367        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
368        assert_eq!(buffer.unwrap().height(), 3);
369    }
370
371    #[test]
372    fn test_buffer_channels() {
373        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
374        assert_eq!(buffer.unwrap().channels(), 2);
375    }
376
377    #[test]
378    fn test_buffer_shape() {
379        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
380        assert_eq!(buffer.unwrap().shape(), (3, 1, 2));
381    }
382
383    #[test]
384    fn test_buffer_len() {
385        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
386        assert_eq!(buffer.unwrap().len(), 6);
387    }
388
389    #[test]
390    fn test_buffer_into_raw() {
391        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
392        assert_eq!(buffer.unwrap().into_raw(), [1, 2, 3, 4, 5, 6]);
393    }
394
395    #[test]
396    fn test_buffer_as_raw() {
397        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice());
398        assert_eq!(buffer.unwrap().as_raw(), &[1, 2, 3, 4, 5, 6]);
399    }
400
401    #[test]
402    fn test_buffer_to_u8() {
403        let buffer = PineappleBuffer::new(1, 2, 2, [2.5, 3.9, 4.8, 2.2].as_slice()).unwrap();
404
405        let u8_vec = buffer.to_u8();
406        assert_eq!(u8_vec, [2, 3, 4, 2]);
407    }
408
409    #[test]
410    fn test_buffer_to_u16() {
411        let buffer = PineappleBuffer::new(1, 2, 2, [2.5, 3.9, 4.8, 2.2].as_slice()).unwrap();
412
413        let u16_vec = buffer.to_u16();
414        assert_eq!(u16_vec, [2, 3, 4, 2]);
415    }
416
417    #[test]
418    fn test_iter() {
419        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice()).unwrap();
420
421        for (a, b) in buffer.iter().zip([1, 2, 3, 4, 5, 6]) {
422            assert_eq!(a, &b);
423        }
424    }
425
426    #[test]
427    fn test_iter_channel() {
428        let buffer = PineappleBuffer::new(1, 3, 2, [1, 2, 3, 4, 5, 6].as_slice()).unwrap();
429
430        for (a, b) in buffer.iter_channel(0).unwrap().zip([1, 3, 5]) {
431            assert_eq!(a, &b)
432        }
433
434        for (a, b) in buffer.iter_channel(1).unwrap().zip([2, 4, 6]) {
435            assert_eq!(a, &b)
436        }
437
438        let buffer = PineappleBuffer::new(2, 1, 3, [1, 2, 3, 4, 5, 6].as_slice()).unwrap();
439
440        for (a, b) in buffer.iter_channel(0).unwrap().zip([1, 4]) {
441            assert_eq!(a, &b)
442        }
443
444        for (a, b) in buffer.iter_channel(1).unwrap().zip([2, 5]) {
445            assert_eq!(a, &b)
446        }
447
448        for (a, b) in buffer.iter_channel(2).unwrap().zip([3, 6]) {
449            assert_eq!(a, &b)
450        }
451    }
452
453    #[test]
454    fn test_iter_pixels() {
455        let buffer = PineappleBuffer::new(1, 4, 2, [1, 2, 3, 4, 5, 6, 7, 8].as_slice()).unwrap();
456
457        for (a, b) in buffer.iter_pixels().zip([[1, 2], [3, 4], [5, 6], [7, 8]]) {
458            assert_eq!(a[0], b[0]);
459            assert_eq!(a[1], b[1]);
460        }
461    }
462}