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