1use crate::fdct;
33use crate::format::{ICC_CHUNK, ZIGZAG, icc_segments, marker};
34use crate::huffman::HuffmanEncoder;
35use crate::progressive_encode::Coefficients;
36use crate::tables;
37use otf_pixels_core::{
38 ChromaSubsampling, EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result,
39 Sink,
40};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49#[non_exhaustive]
50pub enum Subsampling {
51 None,
53 Horizontal,
55 #[default]
57 Both,
58}
59
60impl Subsampling {
61 const fn factors(self) -> (u32, u32) {
63 match self {
64 Self::None => (1, 1),
65 Self::Horizontal => (2, 1),
66 Self::Both => (2, 2),
67 }
68 }
69}
70
71#[derive(Debug)]
73pub struct JpegEncoder {
74 quality: u8,
75 subsampling: Subsampling,
76 progressive: bool,
78 state: Option<State>,
80 icc: Option<Vec<u8>>,
82 exif: Option<Vec<u8>>,
84}
85
86#[derive(Debug)]
88struct State {
89 descriptor: ImageDescriptor,
90 grayscale: bool,
92 factors: (u32, u32),
94 luma_quant: [u16; 64],
95 chroma_quant: [u16; 64],
96 luma_dc: HuffmanEncoder,
97 luma_ac: HuffmanEncoder,
98 chroma_dc: HuffmanEncoder,
99 chroma_ac: HuffmanEncoder,
100 band: Vec<u8>,
102 band_rows: u32,
104 band_height: u32,
106 mcus_per_line: u32,
107 luma: Plane,
109 chroma_blue: Plane,
110 chroma_red: Plane,
111 chroma_full: Vec<u8>,
113 predictors: [i32; 3],
115 writer: BitWriter,
116 rows_written: u32,
117 coefficients: Option<Coefficients>,
119 mcu_row: u32,
121}
122
123#[derive(Debug, Default)]
125struct Plane {
126 stride: usize,
127 height: usize,
128 samples: Vec<u8>,
129}
130
131impl Plane {
132 fn new(stride: usize, height: usize) -> Self {
133 Self {
134 stride,
135 height,
136 samples: vec![128; stride * height],
137 }
138 }
139
140 fn block(&self, x: usize, y: usize, out: &mut [u8; 64]) {
142 for row in 0..8 {
143 let start = (y + row) * self.stride + x;
144 let source = self
145 .samples
146 .get(start..)
147 .and_then(|rest| rest.get(..8))
148 .unwrap_or(&[128; 8]);
149 if let Some(target) = out.get_mut(row * 8..row * 8 + 8) {
150 target.copy_from_slice(source);
151 }
152 }
153 }
154}
155
156fn chroma_plane(grayscale: bool, stride: usize, height: u32) -> Plane {
158 if grayscale {
159 Plane::default()
160 } else {
161 Plane::new(stride, height as usize)
162 }
163}
164
165#[derive(Debug, Default)]
167pub(crate) struct BitWriter {
168 pub(crate) bytes: Vec<u8>,
170 accumulator: u32,
171 bits: u32,
172}
173
174impl BitWriter {
175 pub(crate) fn write(&mut self, code: u32, length: u32) {
177 if length == 0 || length > 32 {
178 return;
179 }
180 let mask = if length >= 32 {
181 u32::MAX
182 } else {
183 (1_u32 << length) - 1
184 };
185 self.accumulator = (self.accumulator << length.min(31)) | (code & mask);
186 self.bits += length;
187
188 while self.bits >= 8 {
189 let byte = ((self.accumulator >> (self.bits - 8)) & 0xFF) as u8;
190 self.bytes.push(byte);
191 if byte == 0xFF {
196 self.bytes.push(0x00);
197 }
198 self.bits -= 8;
199 }
200 self.accumulator &= (1_u32 << self.bits) - 1;
201 }
202
203 pub(crate) fn flush(&mut self) {
209 if self.bits > 0 {
210 let padding = 8 - self.bits;
211 self.write((1 << padding) - 1, padding);
212 }
213 }
214}
215
216pub(crate) fn magnitude(value: i32) -> (u32, u32) {
222 if value == 0 {
223 return (0, 0);
224 }
225 let size = 32 - value.unsigned_abs().leading_zeros();
226 let bits = if value < 0 {
227 (value - 1) as u32 & ((1_u32 << size) - 1)
229 } else {
230 value as u32
231 };
232 (size, bits)
233}
234
235impl JpegEncoder {
236 #[must_use]
238 pub const fn new() -> Self {
239 Self {
240 quality: EncodeOptions::DEFAULT_QUALITY,
241 subsampling: Subsampling::Both,
242 progressive: false,
243 state: None,
244 icc: None,
245 exif: None,
246 }
247 }
248
249 pub fn with_quality(quality: u8) -> Result<Self> {
260 if !(1..=100).contains(&quality) {
261 return Err(PixelsError::invalid_argument(
262 "quality",
263 format!("must be in 1..=100, got {quality}"),
264 ));
265 }
266 Ok(Self {
267 quality,
268 subsampling: if quality >= 90 {
269 Subsampling::None
270 } else {
271 Subsampling::Both
272 },
273 progressive: false,
274 state: None,
275 icc: None,
276 exif: None,
277 })
278 }
279
280 #[must_use]
282 pub const fn with_subsampling(mut self, subsampling: Subsampling) -> Self {
283 self.subsampling = subsampling;
284 self
285 }
286
287 #[must_use]
292 pub const fn with_progressive(mut self, progressive: bool) -> Self {
293 self.progressive = progressive;
294 self
295 }
296
297 #[must_use]
299 pub const fn progressive(&self) -> bool {
300 self.progressive
301 }
302
303 #[must_use]
307 pub fn from_options(options: &EncodeOptions) -> Self {
308 let encoder = Self::with_quality(options.quality)
309 .unwrap_or_else(|_| Self::new())
310 .with_progressive(options.progressive);
311 match options.chroma_subsampling {
312 Some(ChromaSubsampling::Yuv444) => encoder.with_subsampling(Subsampling::None),
313 Some(ChromaSubsampling::Yuv420) => encoder.with_subsampling(Subsampling::Both),
314 _ => encoder,
315 }
316 }
317
318 #[must_use]
320 pub const fn subsampling(&self) -> Subsampling {
321 self.subsampling
322 }
323}
324
325impl Default for JpegEncoder {
326 fn default() -> Self {
327 Self::new()
328 }
329}
330
331fn channels_of(format: PixelFormat) -> Result<usize> {
333 match format {
334 PixelFormat::Gray8 => Ok(1),
335 PixelFormat::GrayA8 => Ok(2),
336 PixelFormat::Rgb8 => Ok(3),
337 PixelFormat::Rgba8 => Ok(4),
338 other => Err(PixelsError::unsupported(format!(
340 "JPEG encoding needs an 8-bit format; got {other}. Convert first."
341 ))),
342 }
343}
344
345fn pixel_rgb(row: &[u8], index: usize, format: PixelFormat) -> [u8; 3] {
352 let channels = format.channels();
353 let at = index * channels;
354 let get = |offset: usize| u32::from(row.get(at + offset).copied().unwrap_or(0));
355 let blend = |value: u32, alpha: u32| ((value * alpha + 127) / 255) as u8;
356
357 match format {
358 PixelFormat::Gray8 => {
359 let value = get(0) as u8;
360 [value, value, value]
361 }
362 PixelFormat::GrayA8 => {
363 let value = blend(get(0), get(1));
364 [value, value, value]
365 }
366 PixelFormat::Rgba8 => {
367 let alpha = get(3);
368 [
369 blend(get(0), alpha),
370 blend(get(1), alpha),
371 blend(get(2), alpha),
372 ]
373 }
374 _ => [get(0) as u8, get(1) as u8, get(2) as u8],
376 }
377}
378
379fn rgb_to_ycbcr([r, g, b]: [u8; 3]) -> [u8; 3] {
384 const HALF: i32 = 1 << 15;
385 let (r, g, b) = (i32::from(r), i32::from(g), i32::from(b));
386
387 let y = (19_595 * r + 38_470 * g + 7_471 * b + HALF) >> 16;
388 let cb = ((-11_056 * r - 21_712 * g + 32_768 * b + HALF) >> 16) + 128;
389 let cr = ((32_768 * r - 27_440 * g - 5_328 * b + HALF) >> 16) + 128;
390 [
391 y.clamp(0, 255) as u8,
392 cb.clamp(0, 255) as u8,
393 cr.clamp(0, 255) as u8,
394 ]
395}
396
397impl State {
398 fn fill_planes(&mut self) {
407 let width = self.descriptor.width as usize;
408 let format = self.descriptor.pixel;
409 let row_bytes = self.descriptor.row_bytes();
410 let (h, v) = (self.factors.0 as usize, self.factors.1 as usize);
411 let stride = self.luma.stride;
412 let last = (self.band_rows.max(1) - 1) as usize;
414
415 for y in 0..self.luma.height {
416 let row = self
417 .band
418 .get(y.min(last) * row_bytes..)
419 .and_then(|rest| rest.get(..row_bytes))
420 .unwrap_or(&[]);
421
422 for x in 0..stride {
423 let rgb = pixel_rgb(row, x.min(width - 1), format);
424 let ycbcr = if self.grayscale {
425 [rgb[0], 128, 128]
426 } else {
427 rgb_to_ycbcr(rgb)
428 };
429 if let Some(slot) = self.luma.samples.get_mut(y * stride + x) {
430 *slot = ycbcr[0];
431 }
432 if self.grayscale {
433 continue;
434 }
435 if let Some(slot) = self.chroma_full.get_mut((y * stride + x) * 2..) {
438 if let Some(pair) = slot.get_mut(..2) {
439 pair.copy_from_slice(&[ycbcr[1], ycbcr[2]]);
440 }
441 }
442 }
443 }
444 if self.grayscale {
445 return;
446 }
447
448 let count = (h * v) as u32;
452 for cy in 0..self.chroma_blue.height {
453 for cx in 0..self.chroma_blue.stride {
454 let (mut blue, mut red) = (0_u32, 0_u32);
455 for dy in 0..v {
456 for dx in 0..h {
457 let y = (cy * v + dy).min(self.luma.height - 1);
458 let x = (cx * h + dx).min(stride - 1);
459 let at = (y * stride + x) * 2;
460 blue += u32::from(self.chroma_full.get(at).copied().unwrap_or(128));
461 red += u32::from(self.chroma_full.get(at + 1).copied().unwrap_or(128));
462 }
463 }
464 let at = cy * self.chroma_blue.stride + cx;
465 if let Some(slot) = self.chroma_blue.samples.get_mut(at) {
466 *slot = ((blue + count / 2) / count) as u8;
467 }
468 if let Some(slot) = self.chroma_red.samples.get_mut(at) {
469 *slot = ((red + count / 2) / count) as u8;
470 }
471 }
472 }
473 }
474
475 fn encode_band(&mut self) {
477 let (h, v) = self.factors;
478 let mut samples = [0_u8; 64];
479 let mut coefficients = [0_i64; 64];
480 let mut quantized = [0_i32; 64];
481
482 let [luma_predictor, blue_predictor, red_predictor] = &mut self.predictors;
483
484 for mcu in 0..self.mcus_per_line {
485 for block_y in 0..v {
488 for block_x in 0..h {
489 let x = ((mcu * h + block_x) * 8) as usize;
490 let y = (block_y * 8) as usize;
491 self.luma.block(x, y, &mut samples);
492 fdct::block(&samples, &mut coefficients);
493 fdct::quantize(&coefficients, &self.luma_quant, &mut quantized);
494 if let Some(kept) = self.coefficients.as_mut() {
495 let row = (self.mcu_row * v + block_y) as usize;
496 kept.store(0, (mcu * h + block_x) as usize, row, &quantized);
497 continue;
498 }
499 encode_block(
500 &mut self.writer,
501 &quantized,
502 &self.luma_dc,
503 &self.luma_ac,
504 luma_predictor,
505 );
506 }
507 }
508 if self.grayscale {
509 continue;
510 }
511
512 let x = (mcu * 8) as usize;
513 for (component, (plane, predictor)) in [
514 (&self.chroma_blue, &mut *blue_predictor),
515 (&self.chroma_red, &mut *red_predictor),
516 ]
517 .into_iter()
518 .enumerate()
519 {
520 plane.block(x, 0, &mut samples);
521 fdct::block(&samples, &mut coefficients);
522 fdct::quantize(&coefficients, &self.chroma_quant, &mut quantized);
523 if let Some(kept) = self.coefficients.as_mut() {
524 kept.store(
525 component + 1,
526 mcu as usize,
527 self.mcu_row as usize,
528 &quantized,
529 );
530 continue;
531 }
532 encode_block(
533 &mut self.writer,
534 &quantized,
535 &self.chroma_dc,
536 &self.chroma_ac,
537 predictor,
538 );
539 }
540 }
541 self.mcu_row += 1;
542 }
543}
544
545fn encode_block(
547 writer: &mut BitWriter,
548 block: &[i32; 64],
549 dc: &HuffmanEncoder,
550 ac: &HuffmanEncoder,
551 predictor: &mut i32,
552) {
553 let value = block.first().copied().unwrap_or(0);
554 let difference = value.wrapping_sub(*predictor);
558 *predictor = value;
559
560 let (size, bits) = magnitude(difference);
561 if let Some((code, length)) = dc.code(size as u8) {
562 writer.write(code, length);
563 }
564 writer.write(bits, size);
565
566 let mut run = 0_u32;
567 for index in 1..64 {
568 let value = ZIGZAG
569 .get(index)
570 .and_then(|&at| block.get(at))
571 .copied()
572 .unwrap_or(0);
573 if value == 0 {
574 run += 1;
575 continue;
576 }
577 while run >= 16 {
580 if let Some((code, length)) = ac.code(0xF0) {
581 writer.write(code, length);
582 }
583 run -= 16;
584 }
585 let (size, bits) = magnitude(value);
586 if let Some((code, length)) = ac.code(((run as u8) << 4) | size as u8) {
587 writer.write(code, length);
588 }
589 writer.write(bits, size);
590 run = 0;
591 }
592 if run > 0 {
595 if let Some((code, length)) = ac.code(0x00) {
596 writer.write(code, length);
597 }
598 }
599}
600
601fn write_marker(code: u8, sink: &mut dyn Sink) -> Result<()> {
603 sink.write_all(&[0xFF, code])
604}
605
606pub(crate) fn write_segment(code: u8, payload: &[u8], sink: &mut dyn Sink) -> Result<()> {
608 let Ok(length) = u16::try_from(payload.len() + 2) else {
609 return Err(PixelsError::unsupported(format!(
610 "a {code:#04x} segment of {} bytes does not fit a 16-bit length",
611 payload.len()
612 )));
613 };
614 sink.write_all(&[0xFF, code])?;
615 sink.write_all(&length.to_be_bytes())?;
616 sink.write_all(payload)
617}
618
619fn write_quant_table(slot: u8, steps: &[u16; 64], payload: &mut Vec<u8>) {
621 payload.push(slot & 0x0F);
623 for &position in &ZIGZAG {
624 payload.push(steps.get(position).copied().unwrap_or(1).clamp(1, 255) as u8);
625 }
626}
627
628fn write_baseline_scan_header(grayscale: bool, sink: &mut dyn Sink) -> Result<()> {
631 let mut payload = Vec::new();
632 write_huffman_table(
633 0,
634 0,
635 &tables::LUMA_DC_COUNTS,
636 &tables::LUMA_DC_VALUES,
637 &mut payload,
638 );
639 write_huffman_table(
640 1,
641 0,
642 &tables::LUMA_AC_COUNTS,
643 &tables::LUMA_AC_VALUES,
644 &mut payload,
645 );
646 if !grayscale {
647 write_huffman_table(
648 0,
649 1,
650 &tables::CHROMA_DC_COUNTS,
651 &tables::CHROMA_DC_VALUES,
652 &mut payload,
653 );
654 write_huffman_table(
655 1,
656 1,
657 &tables::CHROMA_AC_COUNTS,
658 &tables::CHROMA_AC_VALUES,
659 &mut payload,
660 );
661 }
662 write_segment(marker::DHT, &payload, sink)?;
663
664 let mut payload = Vec::new();
665 if grayscale {
666 payload.push(1);
667 payload.extend_from_slice(&[1, 0x00]);
668 } else {
669 payload.push(3);
670 payload.extend_from_slice(&[1, 0x00, 2, 0x11, 3, 0x11]);
671 }
672 payload.extend_from_slice(&[0, 63, 0]);
675 write_segment(marker::SOS, &payload, sink)
676}
677
678fn write_huffman_table(
680 class: u8,
681 slot: u8,
682 counts: &[u8; 16],
683 values: &[u8],
684 payload: &mut Vec<u8>,
685) {
686 payload.push(((class & 0x0F) << 4) | (slot & 0x0F));
687 payload.extend_from_slice(counts);
688 payload.extend_from_slice(values);
689}
690
691const MAX_EXIF: usize = 65_535 - 2 - 6;
694
695impl Encoder for JpegEncoder {
696 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
697 if self.state.is_some() {
698 return Err(PixelsError::invalid_argument(
699 "profile",
700 "the ICC profile must be set before write_header",
701 ));
702 }
703 if let Some(profile) = profile.filter(|p| p.len() > 255 * ICC_CHUNK) {
705 return Err(PixelsError::unsupported(format!(
706 "a {}-byte ICC profile needs more than 255 JPEG segments",
707 profile.len()
708 )));
709 }
710 self.icc = profile.map(<[u8]>::to_vec);
711 Ok(())
712 }
713
714 fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
718 if self.state.is_some() {
719 return Err(PixelsError::invalid_argument(
720 "exif",
721 "the EXIF block must be set before write_header",
722 ));
723 }
724 self.exif = exif.filter(|e| e.len() <= MAX_EXIF).map(<[u8]>::to_vec);
725 Ok(())
726 }
727
728 fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()> {
729 if self.state.is_some() {
730 return Err(PixelsError::invalid_argument(
731 "descriptor",
732 "write_header called more than once",
733 ));
734 }
735 let channels = channels_of(desc.pixel)?;
736 if desc.width > u32::from(u16::MAX) || desc.height > u32::from(u16::MAX) {
739 return Err(PixelsError::unsupported(format!(
740 "JPEG dimensions are 16-bit; {}x{} does not fit",
741 desc.width, desc.height
742 )));
743 }
744
745 let grayscale = channels <= 2;
746 let factors = if grayscale {
748 (1, 1)
749 } else {
750 self.subsampling.factors()
751 };
752 let luma_quant = tables::scale_quant(&tables::LUMA_QUANT, self.quality);
753 let chroma_quant = tables::scale_quant(&tables::CHROMA_QUANT, self.quality);
754
755 let (h, v) = factors;
756 let band_height = v * 8;
757 let mcus_per_line = desc.width.div_ceil(h * 8);
758 let luma_stride = (mcus_per_line * h * 8) as usize;
759 let chroma_stride = (mcus_per_line * 8) as usize;
760
761 sink.write_all(&[0xFF, marker::SOI])?;
762
763 write_segment(
767 marker::APP0,
768 &[
769 b'J', b'F', b'I', b'F', 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, ],
775 sink,
776 )?;
777 if let Some(exif) = &self.exif {
778 let mut payload = b"Exif\0\0".to_vec();
779 payload.extend_from_slice(exif);
780 write_segment(marker::APP1, &payload, sink)?;
781 }
782 if let Some(profile) = &self.icc {
784 for segment in icc_segments(profile) {
785 write_segment(marker::APP2, &segment, sink)?;
786 }
787 }
788
789 let mut payload = Vec::new();
790 write_quant_table(0, &luma_quant, &mut payload);
791 if !grayscale {
792 write_quant_table(1, &chroma_quant, &mut payload);
793 }
794 write_segment(marker::DQT, &payload, sink)?;
795
796 let mut payload = vec![8];
797 payload.extend_from_slice(&(desc.height as u16).to_be_bytes());
798 payload.extend_from_slice(&(desc.width as u16).to_be_bytes());
799 if grayscale {
800 payload.push(1);
801 payload.extend_from_slice(&[1, 0x11, 0]);
802 } else {
803 payload.push(3);
804 payload.extend_from_slice(&[1, ((h as u8) << 4) | v as u8, 0]);
805 payload.extend_from_slice(&[2, 0x11, 1]);
806 payload.extend_from_slice(&[3, 0x11, 1]);
807 }
808 let frame = if self.progressive {
809 marker::SOF2
810 } else {
811 marker::SOF0
812 };
813 write_segment(frame, &payload, sink)?;
814
815 if !self.progressive {
818 write_baseline_scan_header(grayscale, sink)?;
819 }
820
821 self.state = Some(State {
822 descriptor: *desc,
823 grayscale,
824 factors,
825 luma_quant,
826 chroma_quant,
827 luma_dc: HuffmanEncoder::new(&tables::LUMA_DC_COUNTS, &tables::LUMA_DC_VALUES)?,
828 luma_ac: HuffmanEncoder::new(&tables::LUMA_AC_COUNTS, &tables::LUMA_AC_VALUES)?,
829 chroma_dc: HuffmanEncoder::new(&tables::CHROMA_DC_COUNTS, &tables::CHROMA_DC_VALUES)?,
830 chroma_ac: HuffmanEncoder::new(&tables::CHROMA_AC_COUNTS, &tables::CHROMA_AC_VALUES)?,
831 band: vec![0; desc.row_bytes() * band_height as usize],
832 band_rows: 0,
833 band_height,
834 mcus_per_line,
835 luma: Plane::new(luma_stride, band_height as usize),
836 chroma_full: if grayscale {
838 Vec::new()
839 } else {
840 vec![128; luma_stride * band_height as usize * 2]
841 },
842 chroma_blue: chroma_plane(grayscale, chroma_stride, band_height / v),
843 chroma_red: chroma_plane(grayscale, chroma_stride, band_height / v),
844 predictors: [0; 3],
845 writer: BitWriter::default(),
846 rows_written: 0,
847 coefficients: self
848 .progressive
849 .then(|| Coefficients::new(desc.width, desc.height, factors, grayscale)),
850 mcu_row: 0,
851 });
852 Ok(())
853 }
854
855 fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()> {
856 let Some(state) = self.state.as_mut() else {
857 return Err(PixelsError::invalid_argument(
858 "row",
859 "write_row called before write_header",
860 ));
861 };
862 let expected = state.descriptor.row_bytes();
863 if row.len() != expected {
864 return Err(PixelsError::invalid_argument(
865 "row",
866 format!("row is {} bytes, expected {expected}", row.len()),
867 ));
868 }
869 if state.rows_written >= state.descriptor.height {
870 return Err(PixelsError::invalid_argument(
871 "row",
872 format!("more than {} rows written", state.descriptor.height),
873 ));
874 }
875
876 let at = state.band_rows as usize * expected;
877 if let Some(slot) = state
878 .band
879 .get_mut(at..)
880 .and_then(|rest| rest.get_mut(..expected))
881 {
882 slot.copy_from_slice(row);
883 }
884 state.band_rows += 1;
885 state.rows_written += 1;
886
887 if state.band_rows == state.band_height {
888 state.fill_planes();
889 state.encode_band();
890 state.band_rows = 0;
891 sink.write_all(&state.writer.bytes)?;
894 state.writer.bytes.clear();
895 }
896 Ok(())
897 }
898
899 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
900 let Some(state) = self.state.as_mut() else {
901 return Err(PixelsError::invalid_argument(
902 "sink",
903 "finish called before write_header",
904 ));
905 };
906 if state.rows_written < state.descriptor.height {
907 return Err(PixelsError::malformed(
908 "jpeg",
909 format!(
910 "{} of {} rows were written",
911 state.rows_written, state.descriptor.height
912 ),
913 ));
914 }
915
916 if state.band_rows > 0 {
920 state.fill_planes();
921 state.encode_band();
922 state.band_rows = 0;
923 }
924 if let Some(kept) = state.coefficients.take() {
925 kept.write_scans(sink)?;
926 } else {
927 state.writer.flush();
928 sink.write_all(&state.writer.bytes)?;
929 state.writer.bytes.clear();
930 }
931
932 write_marker(marker::EOI, sink)?;
933 sink.flush()
934 }
935}
936
937#[cfg(test)]
938#[allow(
939 clippy::unwrap_used,
940 clippy::indexing_slicing,
941 reason = "tests operate on known-good values and assert shapes directly"
942)]
943mod tests {
944 use super::*;
945
946 fn encode_with(desc: &ImageDescriptor, raster: &[u8], mut encoder: JpegEncoder) -> Vec<u8> {
948 let mut out = Vec::new();
949 encoder.write_header(desc, &mut out).unwrap();
950 for row in raster.chunks_exact(desc.row_bytes()) {
951 encoder.write_row(row, &mut out).unwrap();
952 }
953 encoder.finish(&mut out).unwrap();
954 out
955 }
956
957 fn decode_all(bytes: &[u8]) -> (bool, Vec<u8>) {
958 use otf_pixels_core::{Decoder, Limits};
959 let mut decoder = crate::JpegDecoder::new(bytes, Limits::default()).unwrap();
960 let mut row = vec![0_u8; decoder.descriptor().row_bytes()];
961 let mut out = Vec::new();
962 for _ in 0..decoder.descriptor().height {
963 decoder.read_row(&mut row).unwrap();
964 out.extend_from_slice(&row);
965 }
966 (decoder.is_progressive(), out)
967 }
968
969 #[test]
970 fn progressive_output_decodes_to_exactly_the_baseline_pixels() {
971 for (width, height, format) in [
973 (1, 1, PixelFormat::Rgb8),
974 (17, 9, PixelFormat::Rgb8),
975 (33, 47, PixelFormat::Rgba8),
976 (64, 64, PixelFormat::Gray8),
977 (23, 31, PixelFormat::GrayA8),
978 (130, 66, PixelFormat::Rgb8),
979 ] {
980 let desc = ImageDescriptor::new(width, height, format).unwrap();
981 let raster: Vec<u8> = (0..desc.byte_len().unwrap())
982 .map(|i| ((i * 37 + i / 101 * 11) % 256) as u8)
983 .collect();
984 for subsampling in [
985 Subsampling::Both,
986 Subsampling::Horizontal,
987 Subsampling::None,
988 ] {
989 for quality in [20, 75, 100] {
990 let base = || {
991 JpegEncoder::with_quality(quality)
992 .unwrap()
993 .with_subsampling(subsampling)
994 };
995 let baseline = encode_with(&desc, &raster, base());
996 let progressive = encode_with(&desc, &raster, base().with_progressive(true));
997 let (was, pixels) = decode_all(&progressive);
998 assert!(was, "the file is progressive");
999 assert_eq!(
1000 pixels,
1001 decode_all(&baseline).1,
1002 "{width}x{height} {format} {subsampling:?} q{quality}"
1003 );
1004 }
1005 }
1006 }
1007 }
1008
1009 #[test]
1010 fn progressive_output_is_smaller_on_a_photograph() {
1011 let desc = ImageDescriptor::new(256, 192, PixelFormat::Rgb8).unwrap();
1012 let mut seed = 5_u32;
1013 let raster: Vec<u8> = (0..256 * 192)
1014 .flat_map(|i| {
1015 seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
1016 let n = (seed >> 28) as usize;
1017 let (x, y) = (i % 256, i / 256);
1018 [(x + n) as u8, (y + n) as u8, ((x + y) / 2) as u8]
1019 })
1020 .collect();
1021 let base = || JpegEncoder::with_quality(80).unwrap();
1022 let baseline = encode_with(&desc, &raster, base());
1023 let progressive = encode_with(&desc, &raster, base().with_progressive(true));
1024 assert!(
1025 progressive.len() < baseline.len(),
1026 "{} against {}",
1027 progressive.len(),
1028 baseline.len()
1029 );
1030 }
1031
1032 #[test]
1033 fn progressive_follows_the_options() {
1034 let options = EncodeOptions::default().with_progressive(true);
1035 assert!(JpegEncoder::from_options(&options).progressive());
1036 assert!(!JpegEncoder::from_options(&EncodeOptions::default()).progressive());
1037 }
1038
1039 #[test]
1040 fn options_choose_the_subsampling_over_the_one_quality_implies() {
1041 let low = EncodeOptions::with_quality(50).unwrap();
1042 let high = EncodeOptions::with_quality(95).unwrap();
1043 assert_eq!(
1044 JpegEncoder::from_options(&low).subsampling(),
1045 Subsampling::Both
1046 );
1047 assert_eq!(
1048 JpegEncoder::from_options(&high).subsampling(),
1049 Subsampling::None
1050 );
1051 let full = low.with_chroma_subsampling(ChromaSubsampling::Yuv444);
1052 assert_eq!(
1053 JpegEncoder::from_options(&full).subsampling(),
1054 Subsampling::None
1055 );
1056 let sub = high.with_chroma_subsampling(ChromaSubsampling::Yuv420);
1057 assert_eq!(
1058 JpegEncoder::from_options(&sub).subsampling(),
1059 Subsampling::Both
1060 );
1061 }
1062
1063 #[test]
1064 fn magnitude_categories_mirror_the_decoder() {
1065 for value in [-2047_i32, -255, -8, -3, -2, -1, 1, 2, 3, 8, 255, 2047] {
1068 let (size, bits) = magnitude(value);
1069 assert!(size <= 15, "{value}: size {size}");
1070 let threshold = 1_i32 << (size - 1);
1072 let raw = bits as i32;
1073 let decoded = if raw < threshold {
1074 raw - (1_i32 << size) + 1
1075 } else {
1076 raw
1077 };
1078 assert_eq!(decoded, value, "{value} round-tripped as {decoded}");
1079 }
1080 assert_eq!(magnitude(0), (0, 0));
1081 }
1082
1083 #[test]
1084 fn the_bit_writer_stuffs_ff_bytes() {
1085 let mut writer = BitWriter::default();
1086 writer.write(0xFF, 8);
1087 assert_eq!(
1088 writer.bytes,
1089 vec![0xFF, 0x00],
1090 "a literal FF must be stuffed"
1091 );
1092
1093 let mut writer = BitWriter::default();
1094 writer.write(0b1010, 4);
1095 writer.write(0b0101, 4);
1096 assert_eq!(writer.bytes, vec![0b1010_0101]);
1097 }
1098
1099 #[test]
1100 fn flushing_pads_with_one_bits() {
1101 let mut writer = BitWriter::default();
1102 writer.write(0b101, 3);
1103 writer.flush();
1104 assert_eq!(writer.bytes, vec![0b1011_1111]);
1106
1107 let mut writer = BitWriter::default();
1109 writer.write(0xAB, 8);
1110 writer.flush();
1111 assert_eq!(writer.bytes, vec![0xAB]);
1112 }
1113
1114 #[test]
1115 fn colour_conversion_round_trips_through_the_decoder() {
1116 for rgb in [
1120 [0, 0, 0],
1121 [255, 255, 255],
1122 [128, 128, 128],
1123 [255, 0, 0],
1124 [0, 255, 0],
1125 [0, 0, 255],
1126 [37, 142, 201],
1127 ] {
1128 let ycbcr = rgb_to_ycbcr(rgb);
1129 if rgb[0] == rgb[1] && rgb[1] == rgb[2] {
1130 assert_eq!(ycbcr[0], rgb[0], "grey should map to its own luma");
1131 assert_eq!([ycbcr[1], ycbcr[2]], [128, 128], "grey has no chroma");
1132 }
1133 let back = crate::decoder::ycbcr_to_rgb(ycbcr);
1134 for channel in 0..3 {
1135 assert!(
1136 back[channel].abs_diff(rgb[channel]) <= 2,
1137 "{rgb:?} -> {ycbcr:?} -> {back:?}"
1138 );
1139 }
1140 }
1141 }
1142
1143 #[test]
1144 fn unsupported_pixel_formats_are_refused_at_the_header() {
1145 for format in [
1146 PixelFormat::Gray16,
1147 PixelFormat::Rgb16,
1148 PixelFormat::Rgba16,
1149 PixelFormat::RgbF32,
1150 PixelFormat::RgbaF32,
1151 ] {
1152 let descriptor = ImageDescriptor::new(8, 8, format).unwrap();
1153 let mut sink = Vec::new();
1154 let error = JpegEncoder::new()
1155 .write_header(&descriptor, &mut sink)
1156 .unwrap_err();
1157 assert_eq!(
1158 error.code(),
1159 otf_pixels_core::ErrorCode::Unsupported,
1160 "{format}"
1161 );
1162 assert!(sink.is_empty(), "{format}: bytes were written anyway");
1163 }
1164 }
1165
1166 #[test]
1167 fn quality_selects_subsampling_but_can_be_overridden() {
1168 assert_eq!(
1169 JpegEncoder::with_quality(80).unwrap().subsampling(),
1170 Subsampling::Both
1171 );
1172 assert_eq!(
1173 JpegEncoder::with_quality(95).unwrap().subsampling(),
1174 Subsampling::None
1175 );
1176 assert_eq!(
1177 JpegEncoder::with_quality(95)
1178 .unwrap()
1179 .with_subsampling(Subsampling::Horizontal)
1180 .subsampling(),
1181 Subsampling::Horizontal
1182 );
1183 assert!(JpegEncoder::with_quality(0).is_err());
1184 assert!(JpegEncoder::with_quality(101).is_err());
1185 }
1186}