strand_dynamic_frame/
lib.rs

1// Copyright 2016-2025 Andrew D. Straw.
2//
3// Licensed under the Apache License, Version 2.0
4// <http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Images from machine vision cameras used in [Strand
9//! Camera](https://strawlab.org/strand-cam).
10//!
11//! Building on the [`machine_vision_formats`] crate which provides compile-time
12//! pixel formats, this crate provides types for images whose pixel format is
13//! determined at runtime. This allows for flexibility in handling images data
14//! whose pixel format is known only dynamically, such as when reading an image
15//! from disk.
16//!
17//! There are two types here:
18//! - [`DynamicFrame`]: A borrowed view of an image with a dynamic pixel format.
19//! - [`DynamicFrameOwned`]: An owned version of `DynamicFrame` that contains
20//!   its own buffer.
21//!
22//! When compiled with the `convert-image` feature, this crate also provides
23//! conversion methods to convert the dynamic frame into a static pixel format
24//! using the [`convert_image`](https://docs.rs/convert-image) crate.
25
26#![warn(missing_docs)]
27#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
28
29use std::borrow::Cow;
30
31use machine_vision_formats as formats;
32
33use formats::{image_ref::ImageRef, ImageStride, PixFmt, PixelFormat, Stride};
34
35#[cfg(feature = "convert-image")]
36use formats::{cow::CowImage, owned::OImage};
37
38// TODO: investigate if we can implement std::borrow::Borrow<DynamicFrame> for
39// DynamicFrameOwned. I think not due to the issues
40// [here](https://users.rust-lang.org/t/how-to-implement-borrow-for-my-own-struct/73023).
41
42#[macro_export]
43/// Macro to match all dynamic pixel formats and execute a block of code with a typed image reference.
44macro_rules! match_all_dynamic_fmts {
45    ($self:expr, $x:ident, $block:expr, $err:expr) => {{
46        use machine_vision_formats::{
47            pixel_format::{
48                BayerBG32f, BayerBG8, BayerGB32f, BayerGB8, BayerGR32f, BayerGR8, BayerRG32f,
49                BayerRG8, Mono32f, Mono8, NV12, RGB8, RGBA8, YUV422, YUV444,
50            },
51            PixFmt,
52        };
53        match $self.pixel_format() {
54            PixFmt::Mono8 => {
55                let $x = $self.as_static::<Mono8>().unwrap();
56                $block
57            }
58            PixFmt::Mono32f => {
59                let $x = $self.as_static::<Mono32f>().unwrap();
60                $block
61            }
62            PixFmt::RGB8 => {
63                let $x = $self.as_static::<RGB8>().unwrap();
64                $block
65            }
66            PixFmt::RGBA8 => {
67                let $x = $self.as_static::<RGBA8>().unwrap();
68                $block
69            }
70            PixFmt::BayerRG8 => {
71                let $x = $self.as_static::<BayerRG8>().unwrap();
72                $block
73            }
74            PixFmt::BayerRG32f => {
75                let $x = $self.as_static::<BayerRG32f>().unwrap();
76                $block
77            }
78            PixFmt::BayerBG8 => {
79                let $x = $self.as_static::<BayerBG8>().unwrap();
80                $block
81            }
82            PixFmt::BayerBG32f => {
83                let $x = $self.as_static::<BayerBG32f>().unwrap();
84                $block
85            }
86            PixFmt::BayerGB8 => {
87                let $x = $self.as_static::<BayerGB8>().unwrap();
88                $block
89            }
90            PixFmt::BayerGB32f => {
91                let $x = $self.as_static::<BayerGB32f>().unwrap();
92                $block
93            }
94            PixFmt::BayerGR8 => {
95                let $x = $self.as_static::<BayerGR8>().unwrap();
96                $block
97            }
98            PixFmt::BayerGR32f => {
99                let $x = $self.as_static::<BayerGR32f>().unwrap();
100                $block
101            }
102            PixFmt::YUV444 => {
103                let $x = $self.as_static::<YUV444>().unwrap();
104                $block
105            }
106            PixFmt::YUV422 => {
107                let $x = $self.as_static::<YUV422>().unwrap();
108                $block
109            }
110            PixFmt::NV12 => {
111                let $x = $self.as_static::<NV12>().unwrap();
112                $block
113            }
114            _ => {
115                return Err($err);
116            }
117        }
118    }};
119}
120
121#[inline]
122const fn calc_min_stride(w: u32, pixfmt: PixFmt) -> usize {
123    w as usize * pixfmt.bits_per_pixel() as usize / 8
124}
125
126#[inline]
127const fn calc_min_buf_size(w: u32, h: u32, stride: usize, pixfmt: PixFmt) -> usize {
128    if h == 0 {
129        return 0;
130    }
131    let all_but_last = (h - 1) as usize * stride;
132    let last = calc_min_stride(w, pixfmt);
133    debug_assert!(stride >= last);
134    all_but_last + last
135}
136
137/// An image whose pixel format is determined at runtime.
138///
139/// This type is used to represent images where the pixel format is not known at
140/// compile time, allowing for flexibility in handling various image formats.
141///
142/// It can be created from raw image data and provides methods to access the
143/// image dimensions, pixel format, and raw data. It also supports conversion to
144/// static pixel formats and encoding to various formats.
145///
146/// # Type Parameters
147/// * `'a` - Lifetime of the borrowed data. If you want to own the data, use
148///   [`DynamicFrameOwned`].
149/// # Notes
150/// * This type is not `Sync` or `Send` because it contains a borrowed buffer.
151///   If you need to share it across threads, use [`DynamicFrameOwned`] instead.
152/// * The pixel format is represented by the [`PixFmt`] enum, which allows for
153///   various pixel formats like `Mono8`, `RGB8`, etc.
154/// * The buffer must be large enough to hold the image data for the specified
155///   dimensions and pixel format.
156///
157/// # Examples
158/// ```rust
159/// # use strand_dynamic_frame::DynamicFrame;
160/// # use machine_vision_formats::PixFmt;
161/// // Create a frame from raw data
162/// let data = vec![0u8; 1920 * 1080];
163/// let frame = DynamicFrame::from_buf(1920, 1080, 1920, data, PixFmt::Mono8).unwrap();
164///
165/// // Check the pixel format
166/// assert_eq!(frame.pixel_format(), PixFmt::Mono8);
167///
168/// // Get dimensions
169/// println!("Size: {}x{}", frame.width(), frame.height());
170/// ```
171pub struct DynamicFrame<'a> {
172    width: u32,
173    height: u32,
174    stride: usize,
175    buf: Cow<'a, [u8]>,
176    pixfmt: PixFmt,
177}
178
179/// An owned version of [`DynamicFrame`] that contains its own buffer.
180#[derive(Clone)]
181pub struct DynamicFrameOwned {
182    width: u32,
183    height: u32,
184    stride: usize,
185    pixfmt: PixFmt,
186    buf: Vec<u8>,
187}
188
189impl std::fmt::Debug for DynamicFrameOwned {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
191        f.debug_struct("DynamicFrameOwned")
192            .field("width", &self.width)
193            .field("height", &self.height)
194            .field("stride", &self.stride)
195            .field("pixfmt", &self.pixfmt)
196            .finish_non_exhaustive()
197    }
198}
199
200impl Stride for DynamicFrameOwned {
201    fn stride(&self) -> usize {
202        self.stride
203    }
204}
205
206impl DynamicFrameOwned {
207    /// Return a new [`DynamicFrameOwned`] from a statically typed frame. This
208    /// moves the input data.
209    pub fn from_static<FRAME, FMT>(frame: FRAME) -> Self
210    where
211        FRAME: ImageStride<FMT> + Into<Vec<u8>>,
212        FMT: PixelFormat,
213    {
214        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
215        let width = frame.width();
216        let height = frame.height();
217        let stride = frame.stride();
218        let min_size = calc_min_buf_size(width, height, stride, pixfmt);
219        let mut buf: Vec<u8> = frame.into();
220        buf.truncate(min_size);
221        Self {
222            width,
223            height,
224            stride,
225            pixfmt,
226            buf,
227        }
228    }
229
230    /// Return a new [`DynamicFrameOwned`] from a reference to a statically
231    /// typed frame. This copies the input data.
232    pub fn from_static_ref<FMT: PixelFormat>(frame: &dyn ImageStride<FMT>) -> Self {
233        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
234        let image_data = frame.image_data();
235        let min_size = calc_min_buf_size(frame.width(), frame.height(), frame.stride(), pixfmt);
236        Self {
237            width: frame.width(),
238            height: frame.height(),
239            stride: frame.stride(),
240            buf: image_data[..min_size].to_vec(),
241            pixfmt,
242        }
243    }
244
245    /// Creates a new [`DynamicFrameOwned`] from raw image data.
246    ///
247    /// This function moves the provided buffer into the new frame without
248    /// copying. The buffer size must be appropriate for the given dimensions
249    /// and pixel format.
250    ///
251    /// # Parameters
252    /// * `w` - Image width in pixels
253    /// * `h` - Image height in pixels
254    /// * `s` - Row stride in bytes (must be >= width * `bytes_per_pixel`)
255    /// * `buf` - Raw image data buffer
256    /// * `pixfmt` - Pixel format of the image data
257    ///
258    /// # Returns
259    /// * `Some(DynamicFrameOwned)` if the buffer is valid for the given parameters
260    /// * `None` if the buffer is too small.
261    #[must_use]
262    pub fn from_buf(w: u32, h: u32, stride: usize, buf: Vec<u8>, pixfmt: PixFmt) -> Option<Self> {
263        let min_size = calc_min_buf_size(w, h, stride, pixfmt);
264        if buf.len() < min_size {
265            return None; // Buffer too small
266        }
267        Some(Self {
268            width: w,
269            height: h,
270            stride,
271            buf,
272            pixfmt,
273        })
274    }
275
276    /// Return a borrowed view of this frame as a [`DynamicFrame`].
277    #[must_use]
278    pub fn borrow(&self) -> DynamicFrame<'_> {
279        DynamicFrame {
280            width: self.width,
281            height: self.height,
282            stride: self.stride,
283            buf: Cow::Borrowed(&self.buf),
284            pixfmt: self.pixfmt,
285        }
286    }
287
288    // /// Return a mutable borrowed view of this frame as a [`DynamicFrame`].
289    // pub fn borrow_mut(&mut self) -> DynamicFrame<'_> {
290    //     DynamicFrame {
291    //         width: self.width,
292    //         height: self.height,
293    //         stride: self.stride,
294    //         buf: Cow::Borrowed(&self.buf),
295    //         pixfmt: self.pixfmt,
296    //     }
297    // }
298
299    /// Moves data into a new [`DynamicFrameOwned`] containing a region of
300    /// interest (ROI) within the image without copying.
301    ///
302    /// The ROI is defined by the specified left, top, width, and height
303    /// parameters. If the specified ROI is out of bounds or the buffer is too
304    /// small, this method returns `None`.
305    ///
306    /// # Parameters
307    /// * `left` - The left coordinate of the ROI in pixels
308    /// * `top` - The top coordinate of the ROI in pixels
309    /// * `width` - The width of the ROI in pixels
310    /// * `height` - The height of the ROI in pixels
311    ///
312    /// # Returns
313    /// * `Some(DynamicFrameOwned)` if the ROI is valid and the buffer is large
314    ///   enough
315    /// * `None` if the ROI is out of bounds or the buffer is too small
316    ///
317    /// To create a view with a ROI, use [`Self::borrow().roi()`].
318    #[must_use]
319    pub fn roi(self, left: u32, top: u32, width: u32, height: u32) -> Option<DynamicFrameOwned> {
320        if left != 0 || top != 0 {
321            todo!();
322        }
323        if left + width > self.width || top + height > self.height {
324            return None; // ROI out of bounds
325        }
326        let stride = self.stride;
327        let new_min_size = calc_min_buf_size(width, height, stride, self.pixfmt);
328        if self.buf.len() < new_min_size {
329            return None; // Buffer too small for ROI
330        }
331        Some(DynamicFrameOwned {
332            width,
333            height,
334            stride,
335            buf: self.buf,
336            pixfmt: self.pixfmt,
337        })
338    }
339
340    /// Moves the `DynamicFrameOwned` into a static pixel format.
341    #[must_use]
342    pub fn as_static<FMT: PixelFormat>(self) -> Option<formats::owned::OImage<FMT>> {
343        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
344        if pixfmt == self.pixfmt {
345            // Simply return the image data as a borrowed view
346            Some(
347                formats::owned::OImage::new(self.width, self.height, self.stride, self.buf)
348                    .unwrap(),
349            )
350        } else {
351            // Cannot convert to static format
352            None
353        }
354    }
355
356    #[cfg(feature = "convert-image")]
357    /// Converts the image to the specified pixel format, returning an
358    /// [`OImage`] that owns the data.
359    ///
360    /// If the requested pixel format matches the current format, this method
361    /// moves the data without copying. Otherwise, the data is converted and a
362    /// new owned image is returned. In both cases, the original image data is
363    /// consumed.
364    ///
365    /// # Type Parameters
366    /// * `FMT` - The target pixel format type
367    ///
368    /// # Returns
369    /// * `Ok(OImage<FMT>)` - If conversion is successful, returns an owned image in the specified format
370    /// * `Err(convert_image::Error)` - If conversion fails
371    ///
372    /// # Examples
373    /// ```rust
374    /// # use strand_dynamic_frame::DynamicFrameOwned;
375    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8};
376    /// let data = vec![64u8; 2000];
377    /// let frame = DynamicFrameOwned::from_buf(40, 50, 40, data, PixFmt::Mono8).unwrap();
378    ///
379    /// // No conversion or copying needed - returns original data
380    /// let owned_image = frame.into_pixel_format::<Mono8>().unwrap();
381    /// ```
382    pub fn into_pixel_format<FMT>(self) -> Result<OImage<FMT>, convert_image::Error>
383    where
384        FMT: PixelFormat,
385    {
386        let dest_fmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
387        let self_ = self.borrow();
388        if dest_fmt == self_.pixel_format() {
389            // Fast path. Simply return the data.
390            Ok(OImage::new(self_.width(), self_.height(), self_.stride(), self.buf).unwrap())
391        } else {
392            // Conversion path. Allocate a new buffer and convert the data.
393            let width = self_.width();
394            let dest_stride = calc_min_stride(width, dest_fmt);
395            let mut dest = OImage::zeros(width, self_.height(), dest_stride).unwrap();
396            self_.into_pixel_format_dest(&mut dest)?;
397            Ok(dest)
398        }
399    }
400}
401
402impl<'a> DynamicFrame<'a> {
403    /// Return a new [`DynamicFrameOwned`] by copying data.
404    #[must_use]
405    pub fn copy_to_owned(&self) -> DynamicFrameOwned {
406        let pixfmt = self.pixfmt;
407        let width = self.width;
408        let height = self.height;
409        let stride = self.stride;
410        let buf = self.buf.to_vec();
411        DynamicFrameOwned {
412            width,
413            height,
414            stride,
415            pixfmt,
416            buf,
417        }
418    }
419
420    /// Return a new [`DynamicFrame`] from a reference to a statically
421    /// typed frame. This does not copy the input data.
422    pub fn from_static_ref<FMT: PixelFormat>(frame: &'a dyn ImageStride<FMT>) -> Self {
423        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
424        let image_data = frame.image_data();
425        let min_size = calc_min_buf_size(frame.width(), frame.height(), frame.stride(), pixfmt);
426        let image_data = &image_data[..min_size];
427        Self {
428            width: frame.width(),
429            height: frame.height(),
430            stride: frame.stride(),
431            buf: std::borrow::Cow::Borrowed(image_data),
432            pixfmt,
433        }
434    }
435
436    /// Creates a new [`DynamicFrame`] from raw image data.
437    ///
438    /// This function moves the provided buffer into the new frame without
439    /// copying. The buffer size must be appropriate for the given dimensions
440    /// and pixel format.
441    ///
442    /// # Parameters
443    /// * `w` - Image width in pixels
444    /// * `h` - Image height in pixels
445    /// * `s` - Row stride in bytes (must be >= width * `bytes_per_pixel`)
446    /// * `buf` - Raw image data buffer
447    /// * `pixfmt` - Pixel format of the image data
448    ///
449    /// # Returns
450    /// * `Some(DynamicFrame)` if the buffer is valid for the given parameters
451    /// * `None` if the buffer is too small.
452    ///
453    /// # Examples
454    /// ```rust
455    /// # use strand_dynamic_frame::DynamicFrame;
456    /// # use machine_vision_formats::PixFmt;
457    /// let data = vec![128u8; 640 * 480]; // Gray image data
458    /// let frame = DynamicFrame::from_buf(640, 480, 640, data, PixFmt::Mono8);
459    /// assert!(frame.is_some());
460    /// ```
461    #[must_use]
462    pub fn from_buf(w: u32, h: u32, stride: usize, buf: Vec<u8>, pixfmt: PixFmt) -> Option<Self> {
463        let min_size = calc_min_buf_size(w, h, stride, pixfmt);
464        if buf.len() < min_size {
465            return None; // Buffer too small
466        }
467        Some(Self {
468            width: w,
469            height: h,
470            stride,
471            buf: Cow::Owned(buf),
472            pixfmt,
473        })
474    }
475
476    /// Returns the width of the image in pixels.
477    ///
478    /// # Examples
479    /// ```rust
480    /// # use strand_dynamic_frame::DynamicFrame;
481    /// # use machine_vision_formats::PixFmt;
482    /// let data = vec![0u8; 1500];
483    /// let frame = DynamicFrame::from_buf(50, 10, 150, data, PixFmt::RGB8).unwrap();
484    /// assert_eq!(frame.width(), 50);
485    /// ```
486    #[must_use]
487    pub fn width(&self) -> u32 {
488        self.width
489    }
490
491    /// Returns the height of the image in pixels.
492    ///
493    /// # Examples
494    /// ```rust
495    /// # use strand_dynamic_frame::DynamicFrame;
496    /// # use machine_vision_formats::PixFmt;
497    /// let data = vec![0u8; 2000];
498    /// let frame = DynamicFrame::from_buf(40, 50, 40, data, PixFmt::Mono8).unwrap();
499    /// assert_eq!(frame.height(), 50);
500    /// ```
501    #[must_use]
502    pub fn height(&self) -> u32 {
503        self.height
504    }
505
506    /// Returns a view of the raw image data as bytes.
507    ///
508    /// This method provides access to the underlying pixel data without any
509    /// type information about the pixel format. The returned slice contains
510    /// the raw bytes that make up the image.
511    ///
512    /// The data layout depends on the pixel format and stride. Use [`pixel_format()`](Self::pixel_format)
513    /// to determine how to interpret the bytes.
514    fn minimum_image_data_without_format(&self) -> &[u8] {
515        let min_size = calc_min_buf_size(self.width, self.height, self.stride, self.pixfmt);
516        &self.buf[..min_size]
517    }
518
519    /// Creates a new [`DynamicFrame`] from an existing frame using borrowed data.
520    ///
521    /// This function copies the image data from the source frame and creates a
522    /// new [`DynamicFrame`]. The original frame remains unchanged.
523    ///
524    /// # Type Parameters
525    /// * `FMT` - The pixel format type of the source frame
526    ///
527    /// # Parameters
528    /// * `frame` - Reference to the source frame implementing [`ImageStride`]
529    ///
530    /// # Examples
531    /// ```rust
532    /// # use strand_dynamic_frame::DynamicFrame;
533    /// # use machine_vision_formats::owned::OImage;
534    /// # use machine_vision_formats::pixel_format::Mono8;
535    /// let source = OImage::<Mono8>::new(100, 100, 100, vec![0u8; 10000]).unwrap();
536    /// let dynamic_frame = DynamicFrame::copy_from(&source);
537    /// assert_eq!(dynamic_frame.width(), 100);
538    /// ```
539    pub fn copy_from<FMT: PixelFormat>(frame: &'a dyn ImageStride<FMT>) -> Self {
540        let width = frame.width();
541        let height = frame.height();
542        let stride = frame.stride();
543        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
544        let min_size = calc_min_buf_size(width, height, stride, pixfmt);
545        let data = frame.image_data();
546        debug_assert!(
547            data.len() >= min_size,
548            "Buffer too small for image dimensions and pixel format"
549        );
550        let min_data = &data[..min_size];
551        Self {
552            width,
553            height,
554            stride,
555            buf: Cow::Borrowed(min_data),
556            pixfmt,
557        }
558    }
559
560    #[cfg(feature = "convert-image")]
561    /// Converts the image to the specified pixel format, returning a [`CowImage`] that may borrow or own the data.
562    ///
563    /// If the requested pixel format matches the current format, this method returns
564    /// a borrowed view of the data without copying. Otherwise, the data is converted
565    /// and a new owned image is returned.
566    ///
567    /// # Type Parameters
568    /// * `FMT` - The target pixel format type
569    ///
570    /// # Returns
571    /// * `Ok(CowImage<FMT>)` - Either a borrowed view or owned converted image
572    /// * `Err(convert_image::Error)` - If conversion fails
573    ///
574    /// # Examples
575    /// ```rust
576    /// # use strand_dynamic_frame::DynamicFrame;
577    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8};
578    /// let data = vec![64u8; 2000];
579    /// let frame = DynamicFrame::from_buf(20, 10, 200, data, PixFmt::Mono8).unwrap();
580    ///
581    /// // No conversion needed - returns borrowed view
582    /// let cow_image = frame.into_pixel_format::<Mono8>().unwrap();
583    /// ```
584    pub fn into_pixel_format<FMT>(&self) -> Result<CowImage<'_, FMT>, convert_image::Error>
585    where
586        FMT: PixelFormat,
587    {
588        let dest_fmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
589        if dest_fmt == self.pixel_format() {
590            // Fast path. Simply return the data.
591            Ok(CowImage::Borrowed(
592                ImageRef::new(
593                    self.width(),
594                    self.height(),
595                    self.stride(),
596                    self.minimum_image_data_without_format(),
597                )
598                .unwrap(),
599            ))
600        } else {
601            // Conversion path. Allocate a new buffer and convert the data.
602            let width = self.width();
603            let dest_stride = calc_min_stride(width, dest_fmt);
604            let mut dest = OImage::zeros(width, self.height(), dest_stride).unwrap();
605            self.into_pixel_format_dest(&mut dest)?;
606            Ok(CowImage::Owned(dest))
607        }
608    }
609
610    /// Return a borrowed view of the image data as a static pixel format.
611    ///
612    /// This method allows you to treat the dynamic frame as a specific pixel format
613    /// without copying the data, as long as the pixel format matches.
614    ///
615    /// # Type Parameters
616    /// * `FMT` - The target pixel format type
617    ///
618    /// # Returns
619    /// * `Some(ImageRef<FMT>)` if the pixel format matches
620    /// * `None` if the pixel format does not match
621    ///
622    /// # Examples
623    /// ```rust
624    /// # use strand_dynamic_frame::DynamicFrame;
625    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8, image_ref::ImageRef, ImageData};
626    /// // Create a dynamic frame with Mono8 pixel format.
627    /// let data = vec![128u8; 1000];
628    /// let frame = DynamicFrame::from_buf(100, 10, 100, data, PixFmt::Mono8).unwrap();
629    ///
630    /// // Convert to a static Mono8 view
631    /// let static_view: Option<ImageRef<Mono8>> = frame.as_static();
632    /// assert!(static_view.is_some());
633    /// assert_eq!(static_view.unwrap().width(), 100);
634    /// ```
635    #[must_use]
636    pub fn as_static<FMT: PixelFormat>(&'a self) -> Option<ImageRef<'a, FMT>> {
637        let pixfmt = formats::pixel_format::pixfmt::<FMT>().unwrap();
638        if pixfmt == self.pixel_format() {
639            // Simply return the image data as a borrowed view
640            Some(
641                ImageRef::new(
642                    self.width(),
643                    self.height(),
644                    self.stride(),
645                    self.minimum_image_data_without_format(),
646                )
647                .unwrap(),
648            )
649        } else {
650            // Cannot convert to static format
651            None
652        }
653    }
654
655    /// Converts the image data into a mutable destination buffer of the
656    /// specified pixel format.
657    ///
658    /// This method will convert the data in-place, modifying the destination
659    /// buffer to match the pixel format of the source image.
660    ///
661    /// # Parameters
662    /// * `dest` - A mutable reference to the destination buffer implementing
663    ///   [`machine_vision_formats::iter::HasRowChunksExactMut`] for the target
664    ///   pixel format.
665    ///
666    /// # Returns
667    /// * `Ok(())` if the conversion was successful
668    /// * `Err(convert_image::Error)` if the conversion fails
669    ///
670    /// # Examples
671    /// ```rust
672    /// # use strand_dynamic_frame::DynamicFrame;
673    /// # use machine_vision_formats::{PixFmt, pixel_format::Mono8, iter::HasRowChunksExactMut,owned::OImage, ImageData, Stride};
674    /// // Create a dynamic frame with RGB8 pixel format.
675    /// let data = vec![255u8; 3000]; // RGB8 data for 100x10 image
676    /// let frame = DynamicFrame::from_buf(100, 10, 300, data, PixFmt::RGB8).unwrap();
677    ///
678    /// // Create a destination buffer for Mono8 format
679    /// let mut dest = OImage::<Mono8>::zeros(100, 10, 100).unwrap();
680    ///
681    /// // Convert the frame into the destination buffer
682    /// frame.into_pixel_format_dest(&mut dest).unwrap();
683    /// assert_eq!(dest.width(), 100);
684    /// assert_eq!(dest.height(), 10);
685    /// assert_eq!(dest.stride(), 100);
686    /// ```
687    #[cfg(feature = "convert-image")]
688    pub fn into_pixel_format_dest<FMT>(
689        &self,
690        dest: &mut dyn machine_vision_formats::iter::HasRowChunksExactMut<FMT>,
691    ) -> Result<(), convert_image::Error>
692    where
693        FMT: PixelFormat,
694    {
695        let pixfmt = self.pixel_format();
696        match_all_dynamic_fmts!(
697            self,
698            x,
699            convert_image::convert_into(&x, dest),
700            convert_image::Error::UnimplementedPixelFormat(pixfmt)
701        )
702    }
703
704    /// Converts the image to a byte buffer encoded in the specified format.
705    ///
706    /// This method encodes the image data into a format suitable for storage or transmission.
707    /// The encoding options can be specified using [`convert_image::EncoderOptions`].
708    ///
709    /// # Parameters
710    /// * `opts` - Encoding options for the output format
711    ///
712    /// # Returns
713    /// * `Ok(Vec<u8>)` - The encoded image data as a byte vector
714    /// * `Err(convert_image::Error)` - If the encoding fails
715    ///
716    /// # Examples
717    /// ```rust
718    /// # use strand_dynamic_frame::DynamicFrame;
719    /// # use machine_vision_formats::PixFmt;
720    /// let data = vec![255u8; 3000]; // RGB8 data for 100x10 image
721    /// let frame = DynamicFrame::from_buf(100, 10, 300, data, PixFmt::RGB8).unwrap();
722    ///
723    /// // Encode the frame to PNG bytes
724    /// let encoded_buffer = frame.to_encoded_buffer(convert_image::EncoderOptions::Png).unwrap();
725    /// assert!(!encoded_buffer.is_empty());
726    /// ```
727    #[cfg(feature = "convert-image")]
728    pub fn to_encoded_buffer(
729        &self,
730        opts: convert_image::EncoderOptions,
731    ) -> Result<Vec<u8>, convert_image::Error> {
732        let pixfmt = self.pixel_format();
733        match_all_dynamic_fmts!(
734            self,
735            x,
736            convert_image::frame_to_encoded_buffer(&x, opts),
737            convert_image::Error::UnimplementedPixelFormat(pixfmt)
738        )
739    }
740
741    /// Returns the pixel format of this image.
742    ///
743    /// # Examples
744    /// ```rust
745    /// # use strand_dynamic_frame::DynamicFrame;
746    /// # use machine_vision_formats::PixFmt;
747    /// let data = vec![0u8; 300];
748    /// let frame = DynamicFrame::from_buf(10, 10, 30, data, PixFmt::RGB8).unwrap();
749    /// assert_eq!(frame.pixel_format(), PixFmt::RGB8);
750    /// ```
751    #[must_use]
752    pub fn pixel_format(&self) -> PixFmt {
753        self.pixfmt
754    }
755
756    /// Forces the image data to be interpreted as a different pixel format without converting the data.
757    ///
758    /// Use this method with caution - the resulting image may not be valid if the buffer
759    /// size is incompatible with the new pixel format requirements.
760    ///
761    /// # Parameters
762    /// * `pixfmt` - The new pixel format to interpret the data as
763    ///
764    /// # Returns
765    /// * `Some(DynamicFrame)` if the buffer size is compatible with the new format
766    /// * `None` if the buffer is too small for the new format
767    ///
768    /// # Examples
769    /// ```rust
770    /// # use strand_dynamic_frame::DynamicFrame;
771    /// # use machine_vision_formats::PixFmt;
772    /// // Create a Mono8 image
773    /// let data = vec![128u8; 1000];
774    /// let frame = DynamicFrame::from_buf(100, 10, 100, data, PixFmt::Mono8).unwrap();
775    ///
776    /// // Force it to be interpreted as a different format (if buffer size allows)
777    /// let forced_frame = frame.force_pixel_format(PixFmt::Mono8);
778    /// assert!(forced_frame.is_some());
779    /// ```
780    #[must_use]
781    pub fn force_pixel_format(self, pixfmt: PixFmt) -> Option<DynamicFrame<'a>> {
782        let new_min_size = calc_min_buf_size(self.width, self.height, self.stride, pixfmt);
783        if self.buf.len() < new_min_size {
784            None // Buffer too small for new pixel format
785        } else {
786            Some(DynamicFrame {
787                width: self.width,
788                height: self.height,
789                stride: self.stride,
790                buf: self.buf,
791                pixfmt,
792            })
793        }
794    }
795
796    /// Returns a new [`DynamicFrame`] representing a region of interest (ROI) within the image.
797    ///
798    /// The ROI is defined by the specified left, top, width, and height parameters.
799    /// If the specified ROI is out of bounds or the buffer is too small, this method returns `None`.
800    ///
801    /// # Parameters
802    /// * `left` - The left coordinate of the ROI in pixels
803    /// * `top` - The top coordinate of the ROI in pixels
804    /// * `width` - The width of the ROI in pixels
805    /// * `height` - The height of the ROI in pixels
806    ///
807    /// # Returns
808    /// * `Some(DynamicFrame)` if the ROI is valid and the buffer is large enough
809    /// * `None` if the ROI is out of bounds or the buffer is too small
810    #[must_use]
811    pub fn roi(&'a self, left: u32, top: u32, width: u32, height: u32) -> Option<DynamicFrame<'a>> {
812        if left + width > self.width || top + height > self.height {
813            return None; // ROI out of bounds
814        }
815        if left != 0 || top != 0 {
816            todo!();
817        }
818        let stride = self.stride;
819        let new_min_size = calc_min_buf_size(width, height, stride, self.pixfmt);
820        if self.buf.len() < new_min_size {
821            return None; // Buffer too small for ROI
822        }
823        Some(DynamicFrame {
824            width,
825            height,
826            stride,
827            buf: Cow::Borrowed(&self.buf[..new_min_size]),
828            pixfmt: self.pixfmt,
829        })
830    }
831}
832
833/// Compile-time test to ensure [`DynamicFrame`] implements the [`Send`] trait.
834fn _test_dynamic_frame_is_send() {
835    fn implements<T: Send>() {}
836    implements::<DynamicFrame>();
837}
838
839impl std::fmt::Debug for DynamicFrame<'_> {
840    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
841        f.debug_struct("DynamicFrame")
842            .field("width", &self.width)
843            .field("height", &self.height)
844            .field("stride", &self.stride)
845            .field("pixfmt", &self.pixfmt)
846            .finish_non_exhaustive()
847    }
848}
849
850impl Stride for DynamicFrame<'_> {
851    /// Returns the stride (bytes per row) of the image.
852    ///
853    /// The stride represents the number of bytes from the start of one row
854    /// to the start of the next row. This may be larger than the minimum
855    /// required by the pixel format due to alignment requirements.
856    ///
857    /// # Examples
858    /// ```rust
859    /// # use strand_dynamic_frame::DynamicFrame;
860    /// # use machine_vision_formats::{PixFmt, Stride};
861    /// let data = vec![0u8; 1000];
862    /// let frame = DynamicFrame::from_buf(10, 10, 100, data, PixFmt::Mono8).unwrap();
863    /// assert_eq!(frame.stride(), 100);
864    /// ```
865    fn stride(&self) -> usize {
866        self.stride
867    }
868}