1#![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
18pub struct BytesEncoder {
23 config: EncoderConfig,
25 layout: PixelLayout,
27 width: u32,
29 height: u32,
30 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 if width == 0 || height == 0 {
43 return Err(Error::invalid_dimensions(
44 width,
45 height,
46 "dimensions cannot be zero",
47 ));
48 }
49
50 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 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 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 if let Some(custom_matrices) = config.quant_tables.to_custom_matrices() {
100 builder = builder.custom_quant_matrices(custom_matrices);
101 }
102
103 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 builder = builder.parallel(true);
133 }
134
135 builder.start()
136 }
137
138 pub fn push(
145 &mut self,
146 data: &[u8],
147 rows: usize,
148 stride_bytes: usize,
149 stop: impl Stop,
150 ) -> Result<()> {
151 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 if stride_bytes < min_stride {
161 return Err(Error::stride_too_small(self.width, stride_bytes));
162 }
163
164 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 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 if stride_bytes == min_stride {
179 self.inner
181 .push_rows_with_stop(&data[..rows * min_stride], rows, &stop)?;
182 } else {
183 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 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 #[must_use]
227 pub fn width(&self) -> u32 {
228 self.width
229 }
230
231 #[must_use]
233 pub fn height(&self) -> u32 {
234 self.height
235 }
236
237 #[must_use]
239 pub fn rows_pushed(&self) -> u32 {
240 self.inner.rows_pushed() as u32
241 }
242
243 #[must_use]
245 pub fn rows_remaining(&self) -> u32 {
246 self.height - self.inner.rows_pushed() as u32
247 }
248
249 #[must_use]
251 pub fn layout(&self) -> PixelLayout {
252 self.layout
253 }
254
255 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 let mut jpeg = self.inner.finish()?;
266
267 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 #[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 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
304const ICC_PROFILE_SIGNATURE: &[u8; 12] = b"ICC_PROFILE\0";
306
307const MAX_ICC_BYTES_PER_MARKER: usize = 65519;
310
311fn inject_icc_profile(jpeg: Vec<u8>, icc_data: &[u8]) -> Vec<u8> {
316 if icc_data.is_empty() {
317 return jpeg;
318 }
319
320 let insert_pos = find_icc_insert_position(&jpeg);
322
323 let icc_markers = build_icc_markers(icc_data);
325
326 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
335fn find_icc_insert_position(jpeg: &[u8]) -> usize {
337 let mut pos = 2;
339
340 while pos + 4 <= jpeg.len() {
342 if jpeg[pos] != 0xFF {
343 break;
344 }
345
346 let marker = jpeg[pos + 1];
347 if marker == 0xE0 || marker == 0xE1 {
349 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
360fn 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 markers.push(0xFF);
371 markers.push(0xE2); 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 markers.extend_from_slice(ICC_PROFILE_SIGNATURE);
380
381 markers.push((chunk_num + 1) as u8);
383 markers.push(num_chunks as u8);
384
385 markers.extend_from_slice(&icc_data[offset..offset + chunk_size]);
387
388 offset += chunk_size;
389 }
390
391 markers
392}
393
394const EXIF_SIGNATURE: &[u8; 6] = b"Exif\0\0";
396
397const MAX_EXIF_BYTES: usize = 65527;
400
401const XMP_NAMESPACE: &[u8; 29] = b"http://ns.adobe.com/xap/1.0/\0";
403
404const MAX_XMP_BYTES: usize = 65504;
407
408fn inject_exif(jpeg: Vec<u8>, exif_data: &[u8]) -> Vec<u8> {
410 if exif_data.is_empty() {
411 return jpeg;
412 }
413
414 let exif_len = exif_data.len().min(MAX_EXIF_BYTES);
416
417 let mut marker = Vec::with_capacity(4 + 6 + exif_len);
419 marker.push(0xFF);
420 marker.push(0xE1); let segment_length = 2 + 6 + exif_len;
424 marker.push((segment_length >> 8) as u8);
425 marker.push(segment_length as u8);
426
427 marker.extend_from_slice(EXIF_SIGNATURE);
429
430 marker.extend_from_slice(&exif_data[..exif_len]);
432
433 let mut result = Vec::with_capacity(jpeg.len() + marker.len());
435 result.extend_from_slice(&jpeg[..2]); result.extend_from_slice(&marker);
437 result.extend_from_slice(&jpeg[2..]);
438
439 result
440}
441
442fn inject_xmp(jpeg: Vec<u8>, xmp_data: &[u8]) -> Vec<u8> {
446 if xmp_data.is_empty() {
447 return jpeg;
448 }
449
450 let xmp_len = xmp_data.len().min(MAX_XMP_BYTES);
452
453 let mut marker = Vec::with_capacity(4 + 29 + xmp_len);
455 marker.push(0xFF);
456 marker.push(0xE1); let segment_length = 2 + 29 + xmp_len;
460 marker.push((segment_length >> 8) as u8);
461 marker.push(segment_length as u8);
462
463 marker.extend_from_slice(XMP_NAMESPACE);
465
466 marker.extend_from_slice(&xmp_data[..xmp_len]);
468
469 let insert_pos = find_xmp_insert_position(&jpeg);
471
472 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
481fn find_xmp_insert_position(jpeg: &[u8]) -> usize {
483 let mut pos = 2;
485
486 while pos + 4 <= jpeg.len() {
488 if jpeg[pos] != 0xFF {
489 break;
490 }
491
492 let marker = jpeg[pos + 1];
493 if marker == 0xE1 {
495 if pos + 10 <= jpeg.len() && &jpeg[pos + 4..pos + 10] == b"Exif\0\0" {
497 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
509pub trait Pixel: Copy + 'static + bytemuck::Pod {
511 const LAYOUT: PixelLayout;
513}
514
515impl 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
552pub 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 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 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 #[must_use]
594 pub fn width(&self) -> u32 {
595 self.inner.width()
596 }
597
598 #[must_use]
600 pub fn height(&self) -> u32 {
601 self.inner.height()
602 }
603
604 #[must_use]
606 pub fn rows_pushed(&self) -> u32 {
607 self.inner.rows_pushed()
608 }
609
610 #[must_use]
612 pub fn rows_remaining(&self) -> u32 {
613 self.inner.rows_remaining()
614 }
615
616 pub fn finish(self) -> Result<Vec<u8>> {
620 self.inner.finish()
621 }
622
623 #[cfg(feature = "std")]
625 pub fn finish_to<W: Write>(self, output: W) -> Result<W> {
626 self.inner.finish_to(output)
627 }
628
629 pub fn finish_to_vec(self, output: &mut Vec<u8>) -> Result<()> {
633 self.inner.finish_to_vec(output)
634 }
635}
636
637pub struct YCbCrPlanarEncoder {
644 #[allow(dead_code)] 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 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 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 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 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 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 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 pub fn push_subsampled(
737 &mut self,
738 planes: &YCbCrPlanes<'_>,
739 y_rows: usize,
740 stop: impl Stop,
741 ) -> Result<()> {
742 self.push(planes, y_rows, stop)
745 }
746
747 #[must_use]
751 pub fn width(&self) -> u32 {
752 self.width
753 }
754
755 #[must_use]
757 pub fn height(&self) -> u32 {
758 self.height
759 }
760
761 #[must_use]
763 pub fn rows_pushed(&self) -> u32 {
764 self.rows_pushed
765 }
766
767 #[must_use]
769 pub fn rows_remaining(&self) -> u32 {
770 self.height - self.rows_pushed
771 }
772
773 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 Err(Error::unsupported_feature(
784 "planar YCbCr encoding not yet implemented in v2 API",
785 ))
786 }
787
788 #[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 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 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]); }
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 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]); }
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 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 for _ in 0..4 {
870 enc.push_packed(&row_data, Unstoppable).unwrap();
871 }
872
873 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 let rows_data = vec![0u8; 8 * 3 * 4];
890 enc.push_packed(&rows_data, Unstoppable).unwrap();
891
892 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 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 assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]); 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 if jpeg.len() > pos + 16 && &jpeg[pos + 4..pos + 16] == b"ICC_PROFILE\0" {
926 found_icc = true;
927 assert_eq!(jpeg[pos + 16], 1); assert_eq!(jpeg[pos + 17], 1); 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 let large_icc = vec![0xABu8; 100_000]; 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 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); }
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 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]); }
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 let mut output = vec![0xDE, 0xAD, 0xBE, 0xEF];
1009 let prefix_len = output.len();
1010 enc.finish_to_vec(&mut output).unwrap();
1011
1012 assert_eq!(&output[0..4], &[0xDE, 0xAD, 0xBE, 0xEF]);
1014 assert_eq!(&output[prefix_len..prefix_len + 2], &[0xFF, 0xD8]);
1016 }
1017
1018 #[test]
1019 fn test_icc_roundtrip_extraction() {
1020 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 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}