Skip to main content

mozjpeg_rs/
imgref_ext.rs

1//! Integration with the `imgref` crate for type-safe pixel formats.
2//!
3//! This module provides [`Encoder`] methods that accept [`imgref::ImgRef`] directly,
4//! giving you:
5//! - Type-safe pixel formats (compile-time RGB vs RGBA vs Grayscale distinction)
6//! - Automatic stride handling (subimages work without copying)
7//! - No dimension mix-ups (width/height baked into the type)
8//!
9//! # Example
10//!
11//! ```ignore
12//! use mozjpeg_rs::{Encoder, Preset};
13//! use imgref::ImgVec;
14//! use rgb::RGB8;
15//!
16//! // Create an image buffer
17//! let pixels: Vec<RGB8> = vec![RGB8::new(128, 64, 32); 640 * 480];
18//! let img = ImgVec::new(pixels, 640, 480);
19//!
20//! // Encode with automatic format detection
21//! let encoder = Encoder::new(Preset::default()).quality(85);
22//! let jpeg = encoder.encode_imgref(img.as_ref())?;
23//! # Ok::<(), mozjpeg_rs::Error>(())
24//! ```
25
26use crate::encode::{Encoder, try_alloc_vec};
27use crate::error::Result;
28use imgref::ImgRef;
29use rgb::{Gray, RGB, RGBA};
30
31/// Trait for pixel types that can be encoded to JPEG.
32///
33/// This trait is implemented for common pixel types from the `rgb` crate.
34/// The encoder will convert RGBA to RGB (discarding alpha) and encode
35/// Gray as grayscale JPEG.
36pub trait EncodeablePixel: Copy {
37    /// Whether this is a grayscale format
38    const IS_GRAYSCALE: bool;
39
40    /// Extract grayscale value (for Gray types)
41    fn to_gray(&self) -> u8;
42
43    /// Extract RGB values (for color types)
44    fn to_rgb(&self) -> (u8, u8, u8);
45}
46
47impl EncodeablePixel for RGB<u8> {
48    const IS_GRAYSCALE: bool = false;
49
50    #[inline]
51    fn to_gray(&self) -> u8 {
52        // Standard grayscale conversion (BT.601)
53        ((self.r as u32 * 77 + self.g as u32 * 150 + self.b as u32 * 29) >> 8) as u8
54    }
55
56    #[inline]
57    fn to_rgb(&self) -> (u8, u8, u8) {
58        (self.r, self.g, self.b)
59    }
60}
61
62impl EncodeablePixel for RGBA<u8> {
63    const IS_GRAYSCALE: bool = false;
64
65    #[inline]
66    fn to_gray(&self) -> u8 {
67        ((self.r as u32 * 77 + self.g as u32 * 150 + self.b as u32 * 29) >> 8) as u8
68    }
69
70    #[inline]
71    fn to_rgb(&self) -> (u8, u8, u8) {
72        (self.r, self.g, self.b)
73    }
74}
75
76impl EncodeablePixel for Gray<u8> {
77    const IS_GRAYSCALE: bool = true;
78
79    #[inline]
80    fn to_gray(&self) -> u8 {
81        *self.as_ref()
82    }
83
84    #[inline]
85    fn to_rgb(&self) -> (u8, u8, u8) {
86        let v = *self.as_ref();
87        (v, v, v)
88    }
89}
90
91impl EncodeablePixel for [u8; 3] {
92    const IS_GRAYSCALE: bool = false;
93
94    #[inline]
95    fn to_gray(&self) -> u8 {
96        ((self[0] as u32 * 77 + self[1] as u32 * 150 + self[2] as u32 * 29) >> 8) as u8
97    }
98
99    #[inline]
100    fn to_rgb(&self) -> (u8, u8, u8) {
101        (self[0], self[1], self[2])
102    }
103}
104
105impl EncodeablePixel for [u8; 4] {
106    const IS_GRAYSCALE: bool = false;
107
108    #[inline]
109    fn to_gray(&self) -> u8 {
110        ((self[0] as u32 * 77 + self[1] as u32 * 150 + self[2] as u32 * 29) >> 8) as u8
111    }
112
113    #[inline]
114    fn to_rgb(&self) -> (u8, u8, u8) {
115        (self[0], self[1], self[2])
116    }
117}
118
119impl EncodeablePixel for u8 {
120    const IS_GRAYSCALE: bool = true;
121
122    #[inline]
123    fn to_gray(&self) -> u8 {
124        *self
125    }
126
127    #[inline]
128    fn to_rgb(&self) -> (u8, u8, u8) {
129        (*self, *self, *self)
130    }
131}
132
133impl Encoder {
134    /// Encode an image from an [`ImgRef`] with automatic format detection.
135    ///
136    /// This method provides type-safe encoding with automatic stride handling.
137    /// The pixel type determines the encoding format:
138    /// - `RGB<u8>` or `[u8; 3]` → Color JPEG
139    /// - `RGBA<u8>` or `[u8; 4]` → Color JPEG (alpha discarded)
140    /// - `Gray<u8>` or `u8` → Grayscale JPEG
141    ///
142    /// # Arguments
143    /// * `img` - Image reference with dimensions and optional stride
144    ///
145    /// # Returns
146    /// JPEG-encoded data as a `Vec<u8>`.
147    ///
148    /// # Example
149    /// ```ignore
150    /// use mozjpeg_rs::{Encoder, Preset};
151    /// use imgref::ImgVec;
152    /// use rgb::RGB8;
153    ///
154    /// let pixels: Vec<RGB8> = vec![RGB8::new(128, 64, 32); 100 * 100];
155    /// let img = ImgVec::new(pixels, 100, 100);
156    ///
157    /// let jpeg = Encoder::new(Preset::default())
158    ///     .quality(85)
159    ///     .encode_imgref(img.as_ref())?;
160    /// # Ok::<(), mozjpeg_rs::Error>(())
161    /// ```
162    pub fn encode_imgref<P: EncodeablePixel>(&self, img: ImgRef<'_, P>) -> Result<Vec<u8>> {
163        let width = img.width() as u32;
164        let height = img.height() as u32;
165
166        if P::IS_GRAYSCALE {
167            // Grayscale path - extract gray values
168            let mut gray_data = try_alloc_vec(0u8, (width * height) as usize)?;
169            for (y, row) in img.rows().enumerate() {
170                for (x, pixel) in row.iter().enumerate() {
171                    gray_data[y * width as usize + x] = pixel.to_gray();
172                }
173            }
174            self.encode_gray(&gray_data, width, height)
175        } else {
176            // Color path - extract RGB values
177            let mut rgb_data = try_alloc_vec(0u8, (width * height * 3) as usize)?;
178            for (y, row) in img.rows().enumerate() {
179                for (x, pixel) in row.iter().enumerate() {
180                    let (r, g, b) = pixel.to_rgb();
181                    let i = (y * width as usize + x) * 3;
182                    rgb_data[i] = r;
183                    rgb_data[i + 1] = g;
184                    rgb_data[i + 2] = b;
185                }
186            }
187            self.encode_rgb(&rgb_data, width, height)
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::Preset;
196    use imgref::ImgVec;
197    use rgb::{Gray, RGB8, RGBA8};
198
199    #[test]
200    fn test_encode_imgref_rgb8() {
201        let pixels: Vec<RGB8> = (0..64 * 48)
202            .map(|i| {
203                RGB8::new(
204                    (i % 256) as u8,
205                    ((i * 2) % 256) as u8,
206                    ((i * 3) % 256) as u8,
207                )
208            })
209            .collect();
210        let img = ImgVec::new(pixels, 64, 48);
211
212        let encoder = Encoder::new(Preset::BaselineBalanced).quality(85);
213        let jpeg = encoder.encode_imgref(img.as_ref()).unwrap();
214
215        // Verify we got valid JPEG
216        assert!(jpeg.len() > 100);
217        assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]); // SOI marker
218
219        // Verify it decodes
220        let mut decoder = jpeg_decoder::Decoder::new(&jpeg[..]);
221        let decoded = decoder.decode().unwrap();
222        let info = decoder.info().unwrap();
223        assert_eq!(info.width, 64);
224        assert_eq!(info.height, 48);
225        assert_eq!(info.pixel_format, jpeg_decoder::PixelFormat::RGB24);
226        assert_eq!(decoded.len(), 64 * 48 * 3);
227    }
228
229    #[test]
230    fn test_encode_imgref_rgba8() {
231        let pixels: Vec<RGBA8> = (0..64 * 48)
232            .map(|i| {
233                RGBA8::new(
234                    (i % 256) as u8,
235                    ((i * 2) % 256) as u8,
236                    ((i * 3) % 256) as u8,
237                    255,
238                )
239            })
240            .collect();
241        let img = ImgVec::new(pixels, 64, 48);
242
243        let encoder = Encoder::new(Preset::BaselineBalanced).quality(85);
244        let jpeg = encoder.encode_imgref(img.as_ref()).unwrap();
245
246        // Verify we got valid JPEG
247        assert!(jpeg.len() > 100);
248        assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]);
249
250        // Verify it decodes as RGB (not RGBA)
251        let mut decoder = jpeg_decoder::Decoder::new(&jpeg[..]);
252        decoder.decode().unwrap();
253        let info = decoder.info().unwrap();
254        assert_eq!(info.pixel_format, jpeg_decoder::PixelFormat::RGB24);
255    }
256
257    #[test]
258    fn test_encode_imgref_gray() {
259        let pixels: Vec<Gray<u8>> = (0..64 * 48).map(|i| Gray::new((i % 256) as u8)).collect();
260        let img = ImgVec::new(pixels, 64, 48);
261
262        let encoder = Encoder::new(Preset::BaselineBalanced).quality(85);
263        let jpeg = encoder.encode_imgref(img.as_ref()).unwrap();
264
265        // Verify we got valid JPEG
266        assert!(jpeg.len() > 100);
267        assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]);
268
269        // Verify it decodes as grayscale
270        let mut decoder = jpeg_decoder::Decoder::new(&jpeg[..]);
271        decoder.decode().unwrap();
272        let info = decoder.info().unwrap();
273        assert_eq!(info.pixel_format, jpeg_decoder::PixelFormat::L8);
274    }
275
276    #[test]
277    fn test_encode_imgref_u8_slice() {
278        let pixels: Vec<u8> = (0..64 * 48).map(|i| (i % 256) as u8).collect();
279        let img = ImgVec::new(pixels, 64, 48);
280
281        let encoder = Encoder::new(Preset::BaselineBalanced).quality(85);
282        let jpeg = encoder.encode_imgref(img.as_ref()).unwrap();
283
284        // Verify we got valid grayscale JPEG
285        let mut decoder = jpeg_decoder::Decoder::new(&jpeg[..]);
286        decoder.decode().unwrap();
287        let info = decoder.info().unwrap();
288        assert_eq!(info.pixel_format, jpeg_decoder::PixelFormat::L8);
289    }
290
291    #[test]
292    fn test_encode_imgref_with_stride() {
293        // Create a larger buffer and take a subimage
294        let full_width = 128;
295        let full_height = 96;
296        let mut full_pixels: Vec<RGB8> = vec![RGB8::new(0, 0, 0); full_width * full_height];
297
298        // Fill a 64x48 region starting at (16, 8) with test pattern
299        for y in 0..48 {
300            for x in 0..64 {
301                let i = (y + 8) * full_width + (x + 16);
302                full_pixels[i] = RGB8::new((x * 4) as u8, (y * 5) as u8, 128);
303            }
304        }
305
306        let full_img = ImgVec::new(full_pixels, full_width, full_height);
307        let sub_img = full_img.sub_image(16, 8, 64, 48);
308
309        // Encode the subimage (which has stride != width)
310        let encoder = Encoder::new(Preset::BaselineBalanced).quality(85);
311        let jpeg = encoder.encode_imgref(sub_img).unwrap();
312
313        // Verify dimensions
314        let mut decoder = jpeg_decoder::Decoder::new(&jpeg[..]);
315        decoder.decode().unwrap();
316        let info = decoder.info().unwrap();
317        assert_eq!(info.width, 64);
318        assert_eq!(info.height, 48);
319    }
320
321    #[test]
322    fn test_encode_imgref_matches_encode_rgb() {
323        let pixels: Vec<RGB8> = (0..64 * 48)
324            .map(|i| {
325                RGB8::new(
326                    (i % 256) as u8,
327                    ((i * 2) % 256) as u8,
328                    ((i * 3) % 256) as u8,
329                )
330            })
331            .collect();
332        let img = ImgVec::new(pixels.clone(), 64, 48);
333
334        // Convert to raw bytes for encode_rgb
335        let rgb_bytes: Vec<u8> = pixels.iter().flat_map(|p| [p.r, p.g, p.b]).collect();
336
337        let encoder = Encoder::new(Preset::BaselineBalanced).quality(85);
338        let jpeg_imgref = encoder.encode_imgref(img.as_ref()).unwrap();
339        let jpeg_rgb = encoder.encode_rgb(&rgb_bytes, 64, 48).unwrap();
340
341        // Should be identical
342        assert_eq!(jpeg_imgref, jpeg_rgb);
343    }
344}