jpegli/encode/
byte_encoders.rs

1//! Encoder implementations for v2 API.
2
3// Allow use of deprecated StreamingEncoder internally - v2 API wraps it
4#![allow(deprecated)]
5
6use core::marker::PhantomData;
7
8#[cfg(feature = "std")]
9use std::io::Write;
10
11use enough::Stop;
12
13use super::encoder_config::EncoderConfig;
14use super::encoder_types::{PixelLayout, YCbCrPlanes};
15use super::streaming::StreamingEncoder;
16use crate::error::{Error, Result};
17
18/// Encoder for raw byte input with explicit pixel layout.
19///
20/// This encoder wraps `StreamingEncoder` to provide true streaming encoding
21/// without buffering the entire image in memory.
22pub struct BytesEncoder {
23    /// v2 config (kept for ICC profile injection)
24    config: EncoderConfig,
25    /// Pixel layout
26    layout: PixelLayout,
27    /// Image dimensions
28    width: u32,
29    height: u32,
30    /// Inner streaming encoder (handles actual encoding)
31    inner: StreamingEncoder,
32}
33
34impl BytesEncoder {
35    pub(crate) fn new(
36        config: EncoderConfig,
37        width: u32,
38        height: u32,
39        layout: PixelLayout,
40    ) -> Result<Self> {
41        // Validate dimensions
42        if width == 0 || height == 0 {
43            return Err(Error::invalid_dimensions(
44                width,
45                height,
46                "dimensions cannot be zero",
47            ));
48        }
49
50        // Check for overflow
51        let pixel_count = (width as u64) * (height as u64);
52        if pixel_count > u32::MAX as u64 {
53            return Err(Error::invalid_dimensions(
54                width,
55                height,
56                "dimensions too large",
57            ));
58        }
59
60        // Build and start the streaming encoder with config from v2
61        let inner = Self::build_streaming_encoder(&config, width, height, layout)?;
62
63        Ok(Self {
64            config,
65            layout,
66            width,
67            height,
68            inner,
69        })
70    }
71
72    /// Build a StreamingEncoder from v2 config.
73    fn build_streaming_encoder(
74        config: &EncoderConfig,
75        width: u32,
76        height: u32,
77        layout: PixelLayout,
78    ) -> Result<StreamingEncoder> {
79        use crate::encode::streaming::{CustomZeroBias, StreamingEncoder as SE};
80        use crate::quant::Quality as LegacyQuality;
81
82        let quality = LegacyQuality::from_quality(config.quality.to_internal());
83        let pixel_format = layout.to_legacy();
84        let subsampling = match config.color_mode {
85            super::encoder_types::ColorMode::YCbCr { subsampling } => subsampling.to_legacy(),
86            super::encoder_types::ColorMode::Xyb { .. } => crate::types::Subsampling::S444,
87            super::encoder_types::ColorMode::Grayscale => crate::types::Subsampling::S444,
88        };
89
90        let mut builder = SE::new(width, height)
91            .quality(quality)
92            .pixel_format(pixel_format)
93            .subsampling(subsampling)
94            .optimize_huffman(config.optimize_huffman)
95            .chroma_downsampling(config.downsampling_method.to_legacy())
96            .restart_interval(config.restart_interval);
97
98        // Apply custom quant tables if configured
99        if let Some(custom_matrices) = config.quant_tables.to_custom_matrices() {
100            builder = builder.custom_quant_matrices(custom_matrices);
101        }
102
103        // Apply custom zero bias if configured
104        let zero_bias = match &config.zero_bias {
105            super::encoder_types::ZeroBiasConfig::Perceptual => CustomZeroBias::Perceptual,
106            super::encoder_types::ZeroBiasConfig::Disabled => CustomZeroBias::Disabled,
107            super::encoder_types::ZeroBiasConfig::Custom { luma, cb, cr } => {
108                CustomZeroBias::Custom {
109                    luma: *luma,
110                    cb: *cb,
111                    cr: *cr,
112                }
113            }
114        };
115        builder = builder.custom_zero_bias(zero_bias);
116
117        if config.progressive {
118            builder = builder.progressive(true);
119        }
120
121        if matches!(
122            config.color_mode,
123            super::encoder_types::ColorMode::Xyb { .. }
124        ) {
125            builder = builder.use_xyb(true);
126        }
127
128        #[cfg(feature = "parallel")]
129        if config.parallel.is_some() {
130            // ParallelEncoding::Auto means enable parallel encoding
131            // Future variants may have different behaviors
132            builder = builder.parallel(true);
133        }
134
135        builder.start()
136    }
137
138    /// Push rows with explicit stride.
139    ///
140    /// - `data`: Raw pixel bytes
141    /// - `rows`: Number of scanlines to push
142    /// - `stride_bytes`: Bytes per row in buffer (>= width * bytes_per_pixel)
143    /// - `stop`: Cancellation token (use `enough::Unstoppable` if not needed)
144    pub fn push(
145        &mut self,
146        data: &[u8],
147        rows: usize,
148        stride_bytes: usize,
149        stop: impl Stop,
150    ) -> Result<()> {
151        // Check cancellation
152        if stop.should_stop() {
153            return Err(Error::cancelled());
154        }
155
156        let bpp = self.layout.bytes_per_pixel();
157        let min_stride = self.width as usize * bpp;
158
159        // Validate stride
160        if stride_bytes < min_stride {
161            return Err(Error::stride_too_small(self.width, stride_bytes));
162        }
163
164        // Validate row count
165        let current_rows = self.inner.rows_pushed() as u32;
166        let new_total = current_rows + rows as u32;
167        if new_total > self.height {
168            return Err(Error::too_many_rows(self.height, new_total));
169        }
170
171        // Validate buffer size
172        let expected_size = rows * stride_bytes;
173        if data.len() < expected_size {
174            return Err(Error::invalid_buffer_size(expected_size, data.len()));
175        }
176
177        // Push rows to streaming encoder
178        if stride_bytes == min_stride {
179            // Packed data - can push directly
180            self.inner
181                .push_rows_with_stop(&data[..rows * min_stride], rows, &stop)?;
182        } else {
183            // Strided data - push row by row
184            for row in 0..rows {
185                if stop.should_stop() {
186                    return Err(Error::cancelled());
187                }
188
189                let src_start = row * stride_bytes;
190                let src_end = src_start + min_stride;
191                self.inner
192                    .push_row_with_stop(&data[src_start..src_end], &stop)?;
193            }
194        }
195
196        Ok(())
197    }
198
199    /// Push contiguous (packed) data.
200    ///
201    /// Stride is assumed to be `width * bytes_per_pixel`.
202    /// Rows inferred from `data.len() / (width * bytes_per_pixel)`.
203    pub fn push_packed(&mut self, data: &[u8], stop: impl Stop) -> Result<()> {
204        let bpp = self.layout.bytes_per_pixel();
205        let row_bytes = self.width as usize * bpp;
206
207        if row_bytes == 0 {
208            return Err(Error::invalid_dimensions(
209                self.width,
210                self.height,
211                "row size is zero",
212            ));
213        }
214
215        let rows = data.len() / row_bytes;
216        if rows == 0 && !data.is_empty() {
217            return Err(Error::invalid_buffer_size(row_bytes, data.len()));
218        }
219
220        self.push(data, rows, row_bytes, stop)
221    }
222
223    // === Status ===
224
225    /// Get image width.
226    #[must_use]
227    pub fn width(&self) -> u32 {
228        self.width
229    }
230
231    /// Get image height.
232    #[must_use]
233    pub fn height(&self) -> u32 {
234        self.height
235    }
236
237    /// Get number of rows pushed so far.
238    #[must_use]
239    pub fn rows_pushed(&self) -> u32 {
240        self.inner.rows_pushed() as u32
241    }
242
243    /// Get number of rows remaining.
244    #[must_use]
245    pub fn rows_remaining(&self) -> u32 {
246        self.height - self.inner.rows_pushed() as u32
247    }
248
249    /// Get the pixel layout.
250    #[must_use]
251    pub fn layout(&self) -> PixelLayout {
252        self.layout
253    }
254
255    // === Finish ===
256
257    /// Finish encoding, return JPEG bytes.
258    pub fn finish(self) -> Result<Vec<u8>> {
259        let rows_pushed = self.inner.rows_pushed() as u32;
260        if rows_pushed != self.height {
261            return Err(Error::incomplete_image(self.height, rows_pushed));
262        }
263
264        // Finish streaming encoder
265        let mut jpeg = self.inner.finish()?;
266
267        // Inject metadata in order: EXIF first (right after SOI), then XMP, then ICC
268        // This matches standard JPEG metadata ordering
269        if let Some(ref exif) = self.config.exif_data {
270            if let Some(exif_bytes) = exif.to_bytes() {
271                jpeg = inject_exif(jpeg, &exif_bytes);
272            }
273        }
274
275        if let Some(ref xmp_data) = self.config.xmp_data {
276            jpeg = inject_xmp(jpeg, xmp_data);
277        }
278
279        if let Some(ref icc_data) = self.config.icc_profile {
280            jpeg = inject_icc_profile(jpeg, icc_data);
281        }
282
283        Ok(jpeg)
284    }
285
286    /// Finish encoding to Write destination.
287    #[cfg(feature = "std")]
288    pub fn finish_to<W: Write>(self, mut output: W) -> Result<W> {
289        let jpeg = self.finish()?;
290        output.write_all(&jpeg)?;
291        Ok(output)
292    }
293
294    /// Finish encoding, appending JPEG bytes to an existing Vec.
295    ///
296    /// Useful for no_std environments or buffer reuse.
297    pub fn finish_to_vec(self, output: &mut Vec<u8>) -> Result<()> {
298        let jpeg = self.finish()?;
299        output.extend_from_slice(&jpeg);
300        Ok(())
301    }
302}
303
304/// ICC profile signature for APP2 marker.
305const ICC_PROFILE_SIGNATURE: &[u8; 12] = b"ICC_PROFILE\0";
306
307/// Maximum ICC profile bytes per APP2 marker segment.
308/// APP2 max length is 65535, minus 2 (length) - 12 (signature) - 2 (chunk info) = 65519.
309const MAX_ICC_BYTES_PER_MARKER: usize = 65519;
310
311/// Inject an ICC profile into a JPEG, writing proper APP2 marker chunks.
312///
313/// Inserts APP2 markers right after SOI (and any existing APP0/APP1 markers).
314/// Large profiles are automatically chunked per ICC spec.
315fn inject_icc_profile(jpeg: Vec<u8>, icc_data: &[u8]) -> Vec<u8> {
316    if icc_data.is_empty() {
317        return jpeg;
318    }
319
320    // Find insertion point: after SOI and any APP0/APP1 markers
321    let insert_pos = find_icc_insert_position(&jpeg);
322
323    // Build ICC APP2 marker segments
324    let icc_markers = build_icc_markers(icc_data);
325
326    // Construct new JPEG with ICC markers inserted
327    let mut result = Vec::with_capacity(jpeg.len() + icc_markers.len());
328    result.extend_from_slice(&jpeg[..insert_pos]);
329    result.extend_from_slice(&icc_markers);
330    result.extend_from_slice(&jpeg[insert_pos..]);
331
332    result
333}
334
335/// Find the position to insert ICC markers (after SOI and APP0/APP1).
336fn find_icc_insert_position(jpeg: &[u8]) -> usize {
337    // Start after SOI marker (2 bytes)
338    let mut pos = 2;
339
340    // Skip any existing APP0 (JFIF) and APP1 (EXIF) markers
341    while pos + 4 <= jpeg.len() {
342        if jpeg[pos] != 0xFF {
343            break;
344        }
345
346        let marker = jpeg[pos + 1];
347        // APP0 = 0xE0, APP1 = 0xE1
348        if marker == 0xE0 || marker == 0xE1 {
349            // Get segment length (big-endian, includes length bytes)
350            let length = ((jpeg[pos + 2] as usize) << 8) | (jpeg[pos + 3] as usize);
351            pos += 2 + length;
352        } else {
353            break;
354        }
355    }
356
357    pos
358}
359
360/// Build ICC profile APP2 marker segments with proper chunking.
361fn build_icc_markers(icc_data: &[u8]) -> Vec<u8> {
362    let num_chunks = (icc_data.len() + MAX_ICC_BYTES_PER_MARKER - 1) / MAX_ICC_BYTES_PER_MARKER;
363    let mut markers = Vec::new();
364
365    let mut offset = 0;
366    for chunk_num in 0..num_chunks {
367        let chunk_size = (icc_data.len() - offset).min(MAX_ICC_BYTES_PER_MARKER);
368
369        // APP2 marker
370        markers.push(0xFF);
371        markers.push(0xE2); // APP2
372
373        // Length: 2 (length field) + 12 (signature) + 2 (chunk info) + data
374        let segment_length = 2 + 12 + 2 + chunk_size;
375        markers.push((segment_length >> 8) as u8);
376        markers.push(segment_length as u8);
377
378        // ICC_PROFILE signature
379        markers.extend_from_slice(ICC_PROFILE_SIGNATURE);
380
381        // Chunk number (1-based) and total chunks
382        markers.push((chunk_num + 1) as u8);
383        markers.push(num_chunks as u8);
384
385        // ICC data chunk
386        markers.extend_from_slice(&icc_data[offset..offset + chunk_size]);
387
388        offset += chunk_size;
389    }
390
391    markers
392}
393
394/// EXIF signature for APP1 marker.
395const EXIF_SIGNATURE: &[u8; 6] = b"Exif\0\0";
396
397/// Maximum EXIF data bytes per APP1 marker segment.
398/// APP1 max length is 65535, minus 2 (length) - 6 (signature) = 65527.
399const MAX_EXIF_BYTES: usize = 65527;
400
401/// XMP namespace signature for APP1 marker.
402const XMP_NAMESPACE: &[u8; 29] = b"http://ns.adobe.com/xap/1.0/\0";
403
404/// Maximum XMP data bytes per APP1 marker segment.
405/// APP1 max length is 65535, minus 2 (length) - 29 (namespace) = 65504.
406const MAX_XMP_BYTES: usize = 65504;
407
408/// Inject EXIF data into a JPEG as APP1 marker, right after SOI.
409fn inject_exif(jpeg: Vec<u8>, exif_data: &[u8]) -> Vec<u8> {
410    if exif_data.is_empty() {
411        return jpeg;
412    }
413
414    // Truncate if too large
415    let exif_len = exif_data.len().min(MAX_EXIF_BYTES);
416
417    // Build EXIF APP1 marker
418    let mut marker = Vec::with_capacity(4 + 6 + exif_len);
419    marker.push(0xFF);
420    marker.push(0xE1); // APP1
421
422    // Length: 2 (length field) + 6 (signature) + data
423    let segment_length = 2 + 6 + exif_len;
424    marker.push((segment_length >> 8) as u8);
425    marker.push(segment_length as u8);
426
427    // EXIF signature
428    marker.extend_from_slice(EXIF_SIGNATURE);
429
430    // EXIF data
431    marker.extend_from_slice(&exif_data[..exif_len]);
432
433    // Insert after SOI (2 bytes)
434    let mut result = Vec::with_capacity(jpeg.len() + marker.len());
435    result.extend_from_slice(&jpeg[..2]); // SOI
436    result.extend_from_slice(&marker);
437    result.extend_from_slice(&jpeg[2..]);
438
439    result
440}
441
442/// Inject XMP data into a JPEG as APP1 marker.
443///
444/// Inserts after SOI and any existing APP1 (EXIF) markers.
445fn inject_xmp(jpeg: Vec<u8>, xmp_data: &[u8]) -> Vec<u8> {
446    if xmp_data.is_empty() {
447        return jpeg;
448    }
449
450    // Truncate if too large
451    let xmp_len = xmp_data.len().min(MAX_XMP_BYTES);
452
453    // Build XMP APP1 marker
454    let mut marker = Vec::with_capacity(4 + 29 + xmp_len);
455    marker.push(0xFF);
456    marker.push(0xE1); // APP1
457
458    // Length: 2 (length field) + 29 (namespace) + data
459    let segment_length = 2 + 29 + xmp_len;
460    marker.push((segment_length >> 8) as u8);
461    marker.push(segment_length as u8);
462
463    // XMP namespace
464    marker.extend_from_slice(XMP_NAMESPACE);
465
466    // XMP data
467    marker.extend_from_slice(&xmp_data[..xmp_len]);
468
469    // Find insertion point: after SOI and any existing EXIF APP1 markers
470    let insert_pos = find_xmp_insert_position(&jpeg);
471
472    // Construct new JPEG with XMP marker inserted
473    let mut result = Vec::with_capacity(jpeg.len() + marker.len());
474    result.extend_from_slice(&jpeg[..insert_pos]);
475    result.extend_from_slice(&marker);
476    result.extend_from_slice(&jpeg[insert_pos..]);
477
478    result
479}
480
481/// Find the position to insert XMP marker (after SOI and EXIF APP1).
482fn find_xmp_insert_position(jpeg: &[u8]) -> usize {
483    // Start after SOI marker (2 bytes)
484    let mut pos = 2;
485
486    // Skip any existing EXIF APP1 markers
487    while pos + 4 <= jpeg.len() {
488        if jpeg[pos] != 0xFF {
489            break;
490        }
491
492        let marker = jpeg[pos + 1];
493        // APP1 = 0xE1
494        if marker == 0xE1 {
495            // Check if it's EXIF (not XMP)
496            if pos + 10 <= jpeg.len() && &jpeg[pos + 4..pos + 10] == b"Exif\0\0" {
497                // Get segment length (big-endian, includes length bytes)
498                let length = ((jpeg[pos + 2] as usize) << 8) | (jpeg[pos + 3] as usize);
499                pos += 2 + length;
500                continue;
501            }
502        }
503        break;
504    }
505
506    pos
507}
508
509/// Marker trait for supported rgb crate pixel types.
510pub trait Pixel: Copy + 'static + bytemuck::Pod {
511    /// Equivalent PixelLayout for this type.
512    const LAYOUT: PixelLayout;
513}
514
515// Implement Pixel for rgb crate types
516impl Pixel for rgb::RGB<u8> {
517    const LAYOUT: PixelLayout = PixelLayout::Rgb8Srgb;
518}
519impl Pixel for rgb::RGBA<u8> {
520    const LAYOUT: PixelLayout = PixelLayout::Rgbx8Srgb;
521}
522impl Pixel for rgb::Bgr<u8> {
523    const LAYOUT: PixelLayout = PixelLayout::Bgr8Srgb;
524}
525impl Pixel for rgb::Bgra<u8> {
526    const LAYOUT: PixelLayout = PixelLayout::Bgrx8Srgb;
527}
528impl Pixel for rgb::Gray<u8> {
529    const LAYOUT: PixelLayout = PixelLayout::Gray8Srgb;
530}
531
532impl Pixel for rgb::RGB<u16> {
533    const LAYOUT: PixelLayout = PixelLayout::Rgb16Linear;
534}
535impl Pixel for rgb::RGBA<u16> {
536    const LAYOUT: PixelLayout = PixelLayout::Rgbx16Linear;
537}
538impl Pixel for rgb::Gray<u16> {
539    const LAYOUT: PixelLayout = PixelLayout::Gray16Linear;
540}
541
542impl Pixel for rgb::RGB<f32> {
543    const LAYOUT: PixelLayout = PixelLayout::RgbF32Linear;
544}
545impl Pixel for rgb::RGBA<f32> {
546    const LAYOUT: PixelLayout = PixelLayout::RgbxF32Linear;
547}
548impl Pixel for rgb::Gray<f32> {
549    const LAYOUT: PixelLayout = PixelLayout::GrayF32Linear;
550}
551
552/// Encoder for rgb crate pixel types.
553///
554/// Type parameter P determines pixel layout at compile time.
555/// For RGBA/BGRA types, 4th channel is ignored.
556pub struct RgbEncoder<P: Pixel> {
557    inner: BytesEncoder,
558    _marker: PhantomData<P>,
559}
560
561impl<P: Pixel> RgbEncoder<P> {
562    pub(crate) fn new(config: EncoderConfig, width: u32, height: u32) -> Result<Self> {
563        let inner = BytesEncoder::new(config, width, height, P::LAYOUT)?;
564        Ok(Self {
565            inner,
566            _marker: PhantomData,
567        })
568    }
569
570    /// Push rows with explicit stride (in pixels).
571    ///
572    /// - `data`: Pixel slice
573    /// - `rows`: Number of scanlines to push
574    /// - `stride`: Pixels per row in buffer (>= width)
575    /// - `stop`: Cancellation token
576    pub fn push(&mut self, data: &[P], rows: usize, stride: usize, stop: impl Stop) -> Result<()> {
577        let stride_bytes = stride * core::mem::size_of::<P>();
578        let bytes = bytemuck::cast_slice(data);
579        self.inner.push(bytes, rows, stride_bytes, stop)
580    }
581
582    /// Push contiguous (packed) data.
583    ///
584    /// Stride assumed to be `width`. Rows inferred from `data.len() / width`.
585    pub fn push_packed(&mut self, data: &[P], stop: impl Stop) -> Result<()> {
586        let bytes = bytemuck::cast_slice(data);
587        self.inner.push_packed(bytes, stop)
588    }
589
590    // === Status ===
591
592    /// Get image width.
593    #[must_use]
594    pub fn width(&self) -> u32 {
595        self.inner.width()
596    }
597
598    /// Get image height.
599    #[must_use]
600    pub fn height(&self) -> u32 {
601        self.inner.height()
602    }
603
604    /// Get number of rows pushed so far.
605    #[must_use]
606    pub fn rows_pushed(&self) -> u32 {
607        self.inner.rows_pushed()
608    }
609
610    /// Get number of rows remaining.
611    #[must_use]
612    pub fn rows_remaining(&self) -> u32 {
613        self.inner.rows_remaining()
614    }
615
616    // === Finish ===
617
618    /// Finish encoding, return JPEG bytes.
619    pub fn finish(self) -> Result<Vec<u8>> {
620        self.inner.finish()
621    }
622
623    /// Finish encoding to Write destination.
624    #[cfg(feature = "std")]
625    pub fn finish_to<W: Write>(self, output: W) -> Result<W> {
626        self.inner.finish_to(output)
627    }
628
629    /// Finish encoding, appending JPEG bytes to an existing Vec.
630    ///
631    /// Useful for no_std environments or buffer reuse.
632    pub fn finish_to_vec(self, output: &mut Vec<u8>) -> Result<()> {
633        self.inner.finish_to_vec(output)
634    }
635}
636
637/// Encoder for planar f32 YCbCr input.
638///
639/// Use when you have pre-converted YCbCr from video decoders, etc.
640/// Skips RGB->YCbCr conversion entirely.
641///
642/// Only valid with `ColorMode::YCbCr`. XYB mode requires RGB input.
643pub struct YCbCrPlanarEncoder {
644    #[allow(dead_code)] // Will be used when finish() is implemented
645    config: EncoderConfig,
646    width: u32,
647    height: u32,
648    rows_pushed: u32,
649    y_plane: Vec<f32>,
650    cb_plane: Vec<f32>,
651    cr_plane: Vec<f32>,
652}
653
654impl YCbCrPlanarEncoder {
655    pub(crate) fn new(config: EncoderConfig, width: u32, height: u32) -> Result<Self> {
656        // Validate dimensions
657        if width == 0 || height == 0 {
658            return Err(Error::invalid_dimensions(
659                width,
660                height,
661                "dimensions cannot be zero",
662            ));
663        }
664
665        Ok(Self {
666            config,
667            width,
668            height,
669            rows_pushed: 0,
670            y_plane: Vec::new(),
671            cb_plane: Vec::new(),
672            cr_plane: Vec::new(),
673        })
674    }
675
676    /// Push full-resolution planes. Encoder subsamples chroma as needed.
677    ///
678    /// - `planes`: Y, Cb, Cr plane data with per-plane strides
679    /// - `rows`: Number of luma rows to push
680    /// - `stop`: Cancellation token
681    pub fn push(&mut self, planes: &YCbCrPlanes<'_>, rows: usize, stop: impl Stop) -> Result<()> {
682        if stop.should_stop() {
683            return Err(Error::cancelled());
684        }
685
686        // Validate row count
687        let new_total = self.rows_pushed + rows as u32;
688        if new_total > self.height {
689            return Err(Error::too_many_rows(self.height, new_total));
690        }
691
692        // Copy Y plane
693        for row in 0..rows {
694            if stop.should_stop() {
695                return Err(Error::cancelled());
696            }
697            let src_start = row * planes.y_stride;
698            let src_end = src_start + self.width as usize;
699            if src_end > planes.y.len() {
700                return Err(Error::invalid_buffer_size(src_end, planes.y.len()));
701            }
702            self.y_plane
703                .extend_from_slice(&planes.y[src_start..src_end]);
704        }
705
706        // Copy Cb plane (full resolution, will be subsampled later)
707        for row in 0..rows {
708            let src_start = row * planes.cb_stride;
709            let src_end = src_start + self.width as usize;
710            if src_end > planes.cb.len() {
711                return Err(Error::invalid_buffer_size(src_end, planes.cb.len()));
712            }
713            self.cb_plane
714                .extend_from_slice(&planes.cb[src_start..src_end]);
715        }
716
717        // Copy Cr plane (full resolution, will be subsampled later)
718        for row in 0..rows {
719            let src_start = row * planes.cr_stride;
720            let src_end = src_start + self.width as usize;
721            if src_end > planes.cr.len() {
722                return Err(Error::invalid_buffer_size(src_end, planes.cr.len()));
723            }
724            self.cr_plane
725                .extend_from_slice(&planes.cr[src_start..src_end]);
726        }
727
728        self.rows_pushed = new_total;
729        Ok(())
730    }
731
732    /// Push with pre-subsampled chroma.
733    ///
734    /// Cb/Cr are already at target chroma resolution.
735    /// `y_rows` is luma row count; chroma rows derived from ChromaSubsampling.
736    pub fn push_subsampled(
737        &mut self,
738        planes: &YCbCrPlanes<'_>,
739        y_rows: usize,
740        stop: impl Stop,
741    ) -> Result<()> {
742        // For now, delegate to push() - subsampling handling will be added later
743        // TODO: Properly handle pre-subsampled input
744        self.push(planes, y_rows, stop)
745    }
746
747    // === Status ===
748
749    /// Get image width.
750    #[must_use]
751    pub fn width(&self) -> u32 {
752        self.width
753    }
754
755    /// Get image height.
756    #[must_use]
757    pub fn height(&self) -> u32 {
758        self.height
759    }
760
761    /// Get number of rows pushed so far.
762    #[must_use]
763    pub fn rows_pushed(&self) -> u32 {
764        self.rows_pushed
765    }
766
767    /// Get number of rows remaining.
768    #[must_use]
769    pub fn rows_remaining(&self) -> u32 {
770        self.height - self.rows_pushed
771    }
772
773    // === Finish ===
774
775    /// Finish encoding, return JPEG bytes.
776    pub fn finish(self) -> Result<Vec<u8>> {
777        if self.rows_pushed != self.height {
778            return Err(Error::incomplete_image(self.height, self.rows_pushed));
779        }
780
781        // TODO: Implement actual planar YCbCr encoding
782        // For now, return an error indicating this is not yet implemented
783        Err(Error::unsupported_feature(
784            "planar YCbCr encoding not yet implemented in v2 API",
785        ))
786    }
787
788    /// Finish encoding to Write destination.
789    #[cfg(feature = "std")]
790    pub fn finish_to<W: Write>(self, mut output: W) -> Result<W> {
791        let jpeg = self.finish()?;
792        output.write_all(&jpeg)?;
793        Ok(output)
794    }
795
796    /// Finish encoding, appending JPEG bytes to an existing Vec.
797    ///
798    /// Useful for no_std environments or buffer reuse.
799    pub fn finish_to_vec(self, output: &mut Vec<u8>) -> Result<()> {
800        let jpeg = self.finish()?;
801        output.extend_from_slice(&jpeg);
802        Ok(())
803    }
804}
805
806#[cfg(test)]
807mod tests {
808    use super::*;
809    use crate::encode::ChromaSubsampling;
810    use crate::error::ErrorKind;
811    use enough::Unstoppable;
812    use rgb::RGB;
813
814    #[test]
815    fn test_bytes_encoder_basic() {
816        let config = EncoderConfig::new(85, ChromaSubsampling::Quarter);
817        let mut enc = config
818            .encode_from_bytes(8, 8, PixelLayout::Rgb8Srgb)
819            .unwrap();
820
821        // Create 8x8 red image
822        let pixels = [255u8, 0, 0].repeat(64);
823        enc.push_packed(&pixels, Unstoppable).unwrap();
824
825        let jpeg = enc.finish().unwrap();
826        assert!(!jpeg.is_empty());
827        assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]); // JPEG SOI marker
828    }
829
830    #[test]
831    fn test_rgb_encoder_basic() {
832        let config = EncoderConfig::new(85, ChromaSubsampling::Quarter);
833        let mut enc = config.encode_from_rgb::<RGB<u8>>(8, 8).unwrap();
834
835        // Create 8x8 green image
836        let pixels: Vec<RGB<u8>> = vec![RGB::new(0, 255, 0); 64];
837        enc.push_packed(&pixels, Unstoppable).unwrap();
838
839        let jpeg = enc.finish().unwrap();
840        assert!(!jpeg.is_empty());
841        assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]); // JPEG SOI marker
842    }
843
844    #[test]
845    fn test_stride_validation() {
846        let config = EncoderConfig::new(90.0, ChromaSubsampling::None);
847        let mut enc = config
848            .encode_from_bytes(100, 10, PixelLayout::Rgb8Srgb)
849            .unwrap();
850
851        // Stride too small (less than width * 3)
852        let result = enc.push(&[0u8; 100], 1, 100, Unstoppable);
853        assert!(matches!(
854            result.as_ref().map_err(|e| e.kind()),
855            Err(ErrorKind::StrideTooSmall { .. })
856        ));
857    }
858
859    #[test]
860    fn test_too_many_rows() {
861        let config = EncoderConfig::new(90.0, ChromaSubsampling::None);
862        let mut enc = config
863            .encode_from_bytes(8, 4, PixelLayout::Rgb8Srgb)
864            .unwrap();
865
866        let row_data = vec![0u8; 8 * 3];
867
868        // Push all 4 rows
869        for _ in 0..4 {
870            enc.push_packed(&row_data, Unstoppable).unwrap();
871        }
872
873        // Try to push one more
874        let result = enc.push_packed(&row_data, Unstoppable);
875        assert!(matches!(
876            result.as_ref().map_err(|e| e.kind()),
877            Err(ErrorKind::TooManyRows { .. })
878        ));
879    }
880
881    #[test]
882    fn test_incomplete_image() {
883        let config = EncoderConfig::new(90.0, ChromaSubsampling::None);
884        let mut enc = config
885            .encode_from_bytes(8, 8, PixelLayout::Rgb8Srgb)
886            .unwrap();
887
888        // Only push 4 rows
889        let rows_data = vec![0u8; 8 * 3 * 4];
890        enc.push_packed(&rows_data, Unstoppable).unwrap();
891
892        // Try to finish
893        let result = enc.finish();
894        assert!(matches!(
895            result.as_ref().map_err(|e| e.kind()),
896            Err(ErrorKind::IncompleteImage { .. })
897        ));
898    }
899
900    #[test]
901    fn test_icc_profile_injection() {
902        // Small fake ICC profile (just for testing structure)
903        let fake_icc = vec![0u8; 1000];
904
905        let config =
906            EncoderConfig::new(85, ChromaSubsampling::Quarter).icc_profile(fake_icc.clone());
907        let mut enc = config
908            .encode_from_bytes(8, 8, PixelLayout::Rgb8Srgb)
909            .unwrap();
910
911        let pixels = vec![128u8; 8 * 8 * 3];
912        enc.push_packed(&pixels, Unstoppable).unwrap();
913
914        let jpeg = enc.finish().unwrap();
915
916        // Verify JPEG structure
917        assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]); // SOI
918
919        // Find APP2 ICC profile marker
920        let mut found_icc = false;
921        let mut pos = 2;
922        while pos + 4 < jpeg.len() {
923            if jpeg[pos] == 0xFF && jpeg[pos + 1] == 0xE2 {
924                // APP2 marker - check for ICC signature
925                if jpeg.len() > pos + 16 && &jpeg[pos + 4..pos + 16] == b"ICC_PROFILE\0" {
926                    found_icc = true;
927                    // Verify chunk numbers
928                    assert_eq!(jpeg[pos + 16], 1); // chunk 1
929                    assert_eq!(jpeg[pos + 17], 1); // of 1 total
930                    break;
931                }
932            }
933            if jpeg[pos] == 0xFF && jpeg[pos + 1] != 0x00 && jpeg[pos + 1] != 0xFF {
934                let len = ((jpeg[pos + 2] as usize) << 8) | (jpeg[pos + 3] as usize);
935                pos += 2 + len;
936            } else {
937                pos += 1;
938            }
939        }
940        assert!(found_icc, "ICC profile APP2 marker not found");
941    }
942
943    #[test]
944    fn test_icc_profile_chunking() {
945        // Large ICC profile that requires multiple chunks
946        let large_icc = vec![0xABu8; 100_000]; // > 65519 bytes
947
948        let config = EncoderConfig::new(85, ChromaSubsampling::Quarter).icc_profile(large_icc);
949        let mut enc = config
950            .encode_from_bytes(8, 8, PixelLayout::Rgb8Srgb)
951            .unwrap();
952
953        let pixels = vec![128u8; 8 * 8 * 3];
954        enc.push_packed(&pixels, Unstoppable).unwrap();
955
956        let jpeg = enc.finish().unwrap();
957
958        // Count APP2 ICC chunks
959        let mut chunk_count = 0;
960        let mut pos = 2;
961        while pos + 4 < jpeg.len() {
962            if jpeg[pos] == 0xFF
963                && jpeg[pos + 1] == 0xE2
964                && jpeg.len() > pos + 16
965                && &jpeg[pos + 4..pos + 16] == b"ICC_PROFILE\0"
966            {
967                chunk_count += 1;
968                let chunk_num = jpeg[pos + 16];
969                let total_chunks = jpeg[pos + 17];
970                assert_eq!(chunk_num as usize, chunk_count);
971                assert_eq!(total_chunks, 2); // 100000 / 65519 = 2 chunks
972            }
973            if jpeg[pos] == 0xFF && jpeg[pos + 1] != 0x00 && jpeg[pos + 1] != 0xFF {
974                let len = ((jpeg[pos + 2] as usize) << 8) | (jpeg[pos + 3] as usize);
975                pos += 2 + len;
976            } else {
977                pos += 1;
978            }
979        }
980        assert_eq!(chunk_count, 2, "Expected 2 ICC chunks for 100KB profile");
981    }
982
983    #[test]
984    fn test_finish_to_vec() {
985        let config = EncoderConfig::new(85, ChromaSubsampling::Quarter);
986        let mut enc = config.encode_from_rgb::<RGB<u8>>(8, 8).unwrap();
987
988        let pixels: Vec<RGB<u8>> = vec![RGB::new(100, 150, 200); 64];
989        enc.push_packed(&pixels, Unstoppable).unwrap();
990
991        // Finish to existing vec
992        let mut output = Vec::new();
993        enc.finish_to_vec(&mut output).unwrap();
994
995        assert!(!output.is_empty());
996        assert_eq!(&output[0..2], &[0xFF, 0xD8]); // JPEG SOI marker
997    }
998
999    #[test]
1000    fn test_finish_to_vec_append() {
1001        let config = EncoderConfig::new(85, ChromaSubsampling::Quarter);
1002        let mut enc = config.encode_from_rgb::<RGB<u8>>(8, 8).unwrap();
1003
1004        let pixels: Vec<RGB<u8>> = vec![RGB::new(100, 150, 200); 64];
1005        enc.push_packed(&pixels, Unstoppable).unwrap();
1006
1007        // Finish to vec with existing content
1008        let mut output = vec![0xDE, 0xAD, 0xBE, 0xEF];
1009        let prefix_len = output.len();
1010        enc.finish_to_vec(&mut output).unwrap();
1011
1012        // Verify prefix preserved
1013        assert_eq!(&output[0..4], &[0xDE, 0xAD, 0xBE, 0xEF]);
1014        // Verify JPEG appended
1015        assert_eq!(&output[prefix_len..prefix_len + 2], &[0xFF, 0xD8]);
1016    }
1017
1018    #[test]
1019    fn test_icc_roundtrip_extraction() {
1020        // Test that we can extract the same ICC profile we injected
1021        let original_icc: Vec<u8> = (0..=255).cycle().take(3000).collect();
1022
1023        let config =
1024            EncoderConfig::new(85, ChromaSubsampling::Quarter).icc_profile(original_icc.clone());
1025        let mut enc = config
1026            .encode_from_bytes(8, 8, PixelLayout::Rgb8Srgb)
1027            .unwrap();
1028
1029        let pixels = vec![100u8; 8 * 8 * 3];
1030        enc.push_packed(&pixels, Unstoppable).unwrap();
1031
1032        let jpeg = enc.finish().unwrap();
1033
1034        // Extract ICC profile using the existing extraction function
1035        let extracted = crate::color::icc::extract_icc_profile(&jpeg);
1036        assert!(extracted.is_some(), "Failed to extract ICC profile");
1037        assert_eq!(
1038            extracted.unwrap(),
1039            original_icc,
1040            "Extracted ICC doesn't match original"
1041        );
1042    }
1043}