1use crate::fdct;
26use crate::format::{ICC_CHUNK, ZIGZAG, icc_segments, marker};
27use crate::huffman::HuffmanEncoder;
28use crate::tables;
29use otf_pixels_core::{
30 EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
31};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40#[non_exhaustive]
41pub enum Subsampling {
42 None,
44 Horizontal,
46 #[default]
48 Both,
49}
50
51impl Subsampling {
52 const fn factors(self) -> (u32, u32) {
54 match self {
55 Self::None => (1, 1),
56 Self::Horizontal => (2, 1),
57 Self::Both => (2, 2),
58 }
59 }
60}
61
62#[derive(Debug)]
64pub struct JpegEncoder {
65 quality: u8,
66 subsampling: Subsampling,
67 state: Option<State>,
69 icc: Option<Vec<u8>>,
71}
72
73#[derive(Debug)]
75struct State {
76 descriptor: ImageDescriptor,
77 grayscale: bool,
79 factors: (u32, u32),
81 luma_quant: [u16; 64],
82 chroma_quant: [u16; 64],
83 luma_dc: HuffmanEncoder,
84 luma_ac: HuffmanEncoder,
85 chroma_dc: HuffmanEncoder,
86 chroma_ac: HuffmanEncoder,
87 band: Vec<u8>,
89 band_rows: u32,
91 band_height: u32,
93 mcus_per_line: u32,
94 luma: Plane,
96 chroma_blue: Plane,
97 chroma_red: Plane,
98 chroma_full: Vec<u8>,
100 predictors: [i32; 3],
102 writer: BitWriter,
103 rows_written: u32,
104}
105
106#[derive(Debug, Default)]
108struct Plane {
109 stride: usize,
110 height: usize,
111 samples: Vec<u8>,
112}
113
114impl Plane {
115 fn new(stride: usize, height: usize) -> Self {
116 Self {
117 stride,
118 height,
119 samples: vec![128; stride * height],
120 }
121 }
122
123 fn block(&self, x: usize, y: usize, out: &mut [u8; 64]) {
125 for row in 0..8 {
126 let start = (y + row) * self.stride + x;
127 let source = self
128 .samples
129 .get(start..)
130 .and_then(|rest| rest.get(..8))
131 .unwrap_or(&[128; 8]);
132 if let Some(target) = out.get_mut(row * 8..row * 8 + 8) {
133 target.copy_from_slice(source);
134 }
135 }
136 }
137}
138
139fn chroma_plane(grayscale: bool, stride: usize, height: u32) -> Plane {
141 if grayscale {
142 Plane::default()
143 } else {
144 Plane::new(stride, height as usize)
145 }
146}
147
148#[derive(Debug, Default)]
150struct BitWriter {
151 bytes: Vec<u8>,
153 accumulator: u32,
154 bits: u32,
155}
156
157impl BitWriter {
158 fn write(&mut self, code: u32, length: u32) {
160 if length == 0 || length > 32 {
161 return;
162 }
163 let mask = if length >= 32 {
164 u32::MAX
165 } else {
166 (1_u32 << length) - 1
167 };
168 self.accumulator = (self.accumulator << length.min(31)) | (code & mask);
169 self.bits += length;
170
171 while self.bits >= 8 {
172 let byte = ((self.accumulator >> (self.bits - 8)) & 0xFF) as u8;
173 self.bytes.push(byte);
174 if byte == 0xFF {
179 self.bytes.push(0x00);
180 }
181 self.bits -= 8;
182 }
183 self.accumulator &= (1_u32 << self.bits) - 1;
184 }
185
186 fn flush(&mut self) {
192 if self.bits > 0 {
193 let padding = 8 - self.bits;
194 self.write((1 << padding) - 1, padding);
195 }
196 }
197}
198
199fn magnitude(value: i32) -> (u32, u32) {
205 if value == 0 {
206 return (0, 0);
207 }
208 let size = 32 - value.unsigned_abs().leading_zeros();
209 let bits = if value < 0 {
210 (value - 1) as u32 & ((1_u32 << size) - 1)
212 } else {
213 value as u32
214 };
215 (size, bits)
216}
217
218impl JpegEncoder {
219 #[must_use]
221 pub const fn new() -> Self {
222 Self {
223 quality: EncodeOptions::DEFAULT_QUALITY,
224 subsampling: Subsampling::Both,
225 state: None,
226 icc: None,
227 }
228 }
229
230 pub fn with_quality(quality: u8) -> Result<Self> {
241 if !(1..=100).contains(&quality) {
242 return Err(PixelsError::invalid_argument(
243 "quality",
244 format!("must be in 1..=100, got {quality}"),
245 ));
246 }
247 Ok(Self {
248 quality,
249 subsampling: if quality >= 90 {
250 Subsampling::None
251 } else {
252 Subsampling::Both
253 },
254 state: None,
255 icc: None,
256 })
257 }
258
259 #[must_use]
261 pub const fn with_subsampling(mut self, subsampling: Subsampling) -> Self {
262 self.subsampling = subsampling;
263 self
264 }
265
266 #[must_use]
268 pub fn from_options(options: &EncodeOptions) -> Self {
269 Self::with_quality(options.quality).unwrap_or_else(|_| Self::new())
270 }
271
272 #[must_use]
274 pub const fn subsampling(&self) -> Subsampling {
275 self.subsampling
276 }
277}
278
279impl Default for JpegEncoder {
280 fn default() -> Self {
281 Self::new()
282 }
283}
284
285fn channels_of(format: PixelFormat) -> Result<usize> {
287 match format {
288 PixelFormat::Gray8 => Ok(1),
289 PixelFormat::GrayA8 => Ok(2),
290 PixelFormat::Rgb8 => Ok(3),
291 PixelFormat::Rgba8 => Ok(4),
292 other => Err(PixelsError::unsupported(format!(
294 "JPEG encoding needs an 8-bit format; got {other}. Convert first."
295 ))),
296 }
297}
298
299fn pixel_rgb(row: &[u8], index: usize, format: PixelFormat) -> [u8; 3] {
306 let channels = format.channels();
307 let at = index * channels;
308 let get = |offset: usize| u32::from(row.get(at + offset).copied().unwrap_or(0));
309 let blend = |value: u32, alpha: u32| ((value * alpha + 127) / 255) as u8;
310
311 match format {
312 PixelFormat::Gray8 => {
313 let value = get(0) as u8;
314 [value, value, value]
315 }
316 PixelFormat::GrayA8 => {
317 let value = blend(get(0), get(1));
318 [value, value, value]
319 }
320 PixelFormat::Rgba8 => {
321 let alpha = get(3);
322 [
323 blend(get(0), alpha),
324 blend(get(1), alpha),
325 blend(get(2), alpha),
326 ]
327 }
328 _ => [get(0) as u8, get(1) as u8, get(2) as u8],
330 }
331}
332
333fn rgb_to_ycbcr([r, g, b]: [u8; 3]) -> [u8; 3] {
338 const HALF: i32 = 1 << 15;
339 let (r, g, b) = (i32::from(r), i32::from(g), i32::from(b));
340
341 let y = (19_595 * r + 38_470 * g + 7_471 * b + HALF) >> 16;
342 let cb = ((-11_056 * r - 21_712 * g + 32_768 * b + HALF) >> 16) + 128;
343 let cr = ((32_768 * r - 27_440 * g - 5_328 * b + HALF) >> 16) + 128;
344 [
345 y.clamp(0, 255) as u8,
346 cb.clamp(0, 255) as u8,
347 cr.clamp(0, 255) as u8,
348 ]
349}
350
351impl State {
352 fn fill_planes(&mut self) {
361 let width = self.descriptor.width as usize;
362 let format = self.descriptor.pixel;
363 let row_bytes = self.descriptor.row_bytes();
364 let (h, v) = (self.factors.0 as usize, self.factors.1 as usize);
365 let stride = self.luma.stride;
366 let last = (self.band_rows.max(1) - 1) as usize;
368
369 for y in 0..self.luma.height {
370 let row = self
371 .band
372 .get(y.min(last) * row_bytes..)
373 .and_then(|rest| rest.get(..row_bytes))
374 .unwrap_or(&[]);
375
376 for x in 0..stride {
377 let rgb = pixel_rgb(row, x.min(width - 1), format);
378 let ycbcr = if self.grayscale {
379 [rgb[0], 128, 128]
380 } else {
381 rgb_to_ycbcr(rgb)
382 };
383 if let Some(slot) = self.luma.samples.get_mut(y * stride + x) {
384 *slot = ycbcr[0];
385 }
386 if self.grayscale {
387 continue;
388 }
389 if let Some(slot) = self.chroma_full.get_mut((y * stride + x) * 2..) {
392 if let Some(pair) = slot.get_mut(..2) {
393 pair.copy_from_slice(&[ycbcr[1], ycbcr[2]]);
394 }
395 }
396 }
397 }
398 if self.grayscale {
399 return;
400 }
401
402 let count = (h * v) as u32;
406 for cy in 0..self.chroma_blue.height {
407 for cx in 0..self.chroma_blue.stride {
408 let (mut blue, mut red) = (0_u32, 0_u32);
409 for dy in 0..v {
410 for dx in 0..h {
411 let y = (cy * v + dy).min(self.luma.height - 1);
412 let x = (cx * h + dx).min(stride - 1);
413 let at = (y * stride + x) * 2;
414 blue += u32::from(self.chroma_full.get(at).copied().unwrap_or(128));
415 red += u32::from(self.chroma_full.get(at + 1).copied().unwrap_or(128));
416 }
417 }
418 let at = cy * self.chroma_blue.stride + cx;
419 if let Some(slot) = self.chroma_blue.samples.get_mut(at) {
420 *slot = ((blue + count / 2) / count) as u8;
421 }
422 if let Some(slot) = self.chroma_red.samples.get_mut(at) {
423 *slot = ((red + count / 2) / count) as u8;
424 }
425 }
426 }
427 }
428
429 fn encode_band(&mut self) {
431 let (h, v) = self.factors;
432 let mut samples = [0_u8; 64];
433 let mut coefficients = [0_i64; 64];
434 let mut quantized = [0_i32; 64];
435
436 let [luma_predictor, blue_predictor, red_predictor] = &mut self.predictors;
437
438 for mcu in 0..self.mcus_per_line {
439 for block_y in 0..v {
442 for block_x in 0..h {
443 let x = ((mcu * h + block_x) * 8) as usize;
444 let y = (block_y * 8) as usize;
445 self.luma.block(x, y, &mut samples);
446 fdct::block(&samples, &mut coefficients);
447 fdct::quantize(&coefficients, &self.luma_quant, &mut quantized);
448 encode_block(
449 &mut self.writer,
450 &quantized,
451 &self.luma_dc,
452 &self.luma_ac,
453 luma_predictor,
454 );
455 }
456 }
457 if self.grayscale {
458 continue;
459 }
460
461 let x = (mcu * 8) as usize;
462 for (plane, predictor) in [
463 (&self.chroma_blue, &mut *blue_predictor),
464 (&self.chroma_red, &mut *red_predictor),
465 ] {
466 plane.block(x, 0, &mut samples);
467 fdct::block(&samples, &mut coefficients);
468 fdct::quantize(&coefficients, &self.chroma_quant, &mut quantized);
469 encode_block(
470 &mut self.writer,
471 &quantized,
472 &self.chroma_dc,
473 &self.chroma_ac,
474 predictor,
475 );
476 }
477 }
478 }
479}
480
481fn encode_block(
483 writer: &mut BitWriter,
484 block: &[i32; 64],
485 dc: &HuffmanEncoder,
486 ac: &HuffmanEncoder,
487 predictor: &mut i32,
488) {
489 let value = block.first().copied().unwrap_or(0);
490 let difference = value.wrapping_sub(*predictor);
494 *predictor = value;
495
496 let (size, bits) = magnitude(difference);
497 if let Some((code, length)) = dc.code(size as u8) {
498 writer.write(code, length);
499 }
500 writer.write(bits, size);
501
502 let mut run = 0_u32;
503 for index in 1..64 {
504 let value = ZIGZAG
505 .get(index)
506 .and_then(|&at| block.get(at))
507 .copied()
508 .unwrap_or(0);
509 if value == 0 {
510 run += 1;
511 continue;
512 }
513 while run >= 16 {
516 if let Some((code, length)) = ac.code(0xF0) {
517 writer.write(code, length);
518 }
519 run -= 16;
520 }
521 let (size, bits) = magnitude(value);
522 if let Some((code, length)) = ac.code(((run as u8) << 4) | size as u8) {
523 writer.write(code, length);
524 }
525 writer.write(bits, size);
526 run = 0;
527 }
528 if run > 0 {
531 if let Some((code, length)) = ac.code(0x00) {
532 writer.write(code, length);
533 }
534 }
535}
536
537fn write_marker(code: u8, sink: &mut dyn Sink) -> Result<()> {
539 sink.write_all(&[0xFF, code])
540}
541
542fn write_segment(code: u8, payload: &[u8], sink: &mut dyn Sink) -> Result<()> {
544 let Ok(length) = u16::try_from(payload.len() + 2) else {
545 return Err(PixelsError::unsupported(format!(
546 "a {code:#04x} segment of {} bytes does not fit a 16-bit length",
547 payload.len()
548 )));
549 };
550 sink.write_all(&[0xFF, code])?;
551 sink.write_all(&length.to_be_bytes())?;
552 sink.write_all(payload)
553}
554
555fn write_quant_table(slot: u8, steps: &[u16; 64], payload: &mut Vec<u8>) {
557 payload.push(slot & 0x0F);
559 for &position in &ZIGZAG {
560 payload.push(steps.get(position).copied().unwrap_or(1).clamp(1, 255) as u8);
561 }
562}
563
564fn write_huffman_table(
566 class: u8,
567 slot: u8,
568 counts: &[u8; 16],
569 values: &[u8],
570 payload: &mut Vec<u8>,
571) {
572 payload.push(((class & 0x0F) << 4) | (slot & 0x0F));
573 payload.extend_from_slice(counts);
574 payload.extend_from_slice(values);
575}
576
577impl Encoder for JpegEncoder {
578 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
579 if self.state.is_some() {
580 return Err(PixelsError::invalid_argument(
581 "profile",
582 "the ICC profile must be set before write_header",
583 ));
584 }
585 if let Some(profile) = profile.filter(|p| p.len() > 255 * ICC_CHUNK) {
587 return Err(PixelsError::unsupported(format!(
588 "a {}-byte ICC profile needs more than 255 JPEG segments",
589 profile.len()
590 )));
591 }
592 self.icc = profile.map(<[u8]>::to_vec);
593 Ok(())
594 }
595
596 fn write_header(&mut self, desc: &ImageDescriptor, sink: &mut dyn Sink) -> Result<()> {
597 if self.state.is_some() {
598 return Err(PixelsError::invalid_argument(
599 "descriptor",
600 "write_header called more than once",
601 ));
602 }
603 let channels = channels_of(desc.pixel)?;
604 if desc.width > u32::from(u16::MAX) || desc.height > u32::from(u16::MAX) {
607 return Err(PixelsError::unsupported(format!(
608 "JPEG dimensions are 16-bit; {}x{} does not fit",
609 desc.width, desc.height
610 )));
611 }
612
613 let grayscale = channels <= 2;
614 let factors = if grayscale {
616 (1, 1)
617 } else {
618 self.subsampling.factors()
619 };
620 let luma_quant = tables::scale_quant(&tables::LUMA_QUANT, self.quality);
621 let chroma_quant = tables::scale_quant(&tables::CHROMA_QUANT, self.quality);
622
623 let (h, v) = factors;
624 let band_height = v * 8;
625 let mcus_per_line = desc.width.div_ceil(h * 8);
626 let luma_stride = (mcus_per_line * h * 8) as usize;
627 let chroma_stride = (mcus_per_line * 8) as usize;
628
629 sink.write_all(&[0xFF, marker::SOI])?;
630
631 write_segment(
635 marker::APP0,
636 &[
637 b'J', b'F', b'I', b'F', 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, ],
643 sink,
644 )?;
645 if let Some(profile) = &self.icc {
647 for segment in icc_segments(profile) {
648 write_segment(marker::APP2, &segment, sink)?;
649 }
650 }
651
652 let mut payload = Vec::new();
653 write_quant_table(0, &luma_quant, &mut payload);
654 if !grayscale {
655 write_quant_table(1, &chroma_quant, &mut payload);
656 }
657 write_segment(marker::DQT, &payload, sink)?;
658
659 let mut payload = vec![8];
660 payload.extend_from_slice(&(desc.height as u16).to_be_bytes());
661 payload.extend_from_slice(&(desc.width as u16).to_be_bytes());
662 if grayscale {
663 payload.push(1);
664 payload.extend_from_slice(&[1, 0x11, 0]);
665 } else {
666 payload.push(3);
667 payload.extend_from_slice(&[1, ((h as u8) << 4) | v as u8, 0]);
668 payload.extend_from_slice(&[2, 0x11, 1]);
669 payload.extend_from_slice(&[3, 0x11, 1]);
670 }
671 write_segment(marker::SOF0, &payload, sink)?;
672
673 let mut payload = Vec::new();
674 write_huffman_table(
675 0,
676 0,
677 &tables::LUMA_DC_COUNTS,
678 &tables::LUMA_DC_VALUES,
679 &mut payload,
680 );
681 write_huffman_table(
682 1,
683 0,
684 &tables::LUMA_AC_COUNTS,
685 &tables::LUMA_AC_VALUES,
686 &mut payload,
687 );
688 if !grayscale {
689 write_huffman_table(
690 0,
691 1,
692 &tables::CHROMA_DC_COUNTS,
693 &tables::CHROMA_DC_VALUES,
694 &mut payload,
695 );
696 write_huffman_table(
697 1,
698 1,
699 &tables::CHROMA_AC_COUNTS,
700 &tables::CHROMA_AC_VALUES,
701 &mut payload,
702 );
703 }
704 write_segment(marker::DHT, &payload, sink)?;
705
706 let mut payload = Vec::new();
707 if grayscale {
708 payload.push(1);
709 payload.extend_from_slice(&[1, 0x00]);
710 } else {
711 payload.push(3);
712 payload.extend_from_slice(&[1, 0x00, 2, 0x11, 3, 0x11]);
713 }
714 payload.extend_from_slice(&[0, 63, 0]);
717 write_segment(marker::SOS, &payload, sink)?;
718
719 self.state = Some(State {
720 descriptor: *desc,
721 grayscale,
722 factors,
723 luma_quant,
724 chroma_quant,
725 luma_dc: HuffmanEncoder::new(&tables::LUMA_DC_COUNTS, &tables::LUMA_DC_VALUES)?,
726 luma_ac: HuffmanEncoder::new(&tables::LUMA_AC_COUNTS, &tables::LUMA_AC_VALUES)?,
727 chroma_dc: HuffmanEncoder::new(&tables::CHROMA_DC_COUNTS, &tables::CHROMA_DC_VALUES)?,
728 chroma_ac: HuffmanEncoder::new(&tables::CHROMA_AC_COUNTS, &tables::CHROMA_AC_VALUES)?,
729 band: vec![0; desc.row_bytes() * band_height as usize],
730 band_rows: 0,
731 band_height,
732 mcus_per_line,
733 luma: Plane::new(luma_stride, band_height as usize),
734 chroma_full: if grayscale {
736 Vec::new()
737 } else {
738 vec![128; luma_stride * band_height as usize * 2]
739 },
740 chroma_blue: chroma_plane(grayscale, chroma_stride, band_height / v),
741 chroma_red: chroma_plane(grayscale, chroma_stride, band_height / v),
742 predictors: [0; 3],
743 writer: BitWriter::default(),
744 rows_written: 0,
745 });
746 Ok(())
747 }
748
749 fn write_row(&mut self, row: &[u8], sink: &mut dyn Sink) -> Result<()> {
750 let Some(state) = self.state.as_mut() else {
751 return Err(PixelsError::invalid_argument(
752 "row",
753 "write_row called before write_header",
754 ));
755 };
756 let expected = state.descriptor.row_bytes();
757 if row.len() != expected {
758 return Err(PixelsError::invalid_argument(
759 "row",
760 format!("row is {} bytes, expected {expected}", row.len()),
761 ));
762 }
763 if state.rows_written >= state.descriptor.height {
764 return Err(PixelsError::invalid_argument(
765 "row",
766 format!("more than {} rows written", state.descriptor.height),
767 ));
768 }
769
770 let at = state.band_rows as usize * expected;
771 if let Some(slot) = state
772 .band
773 .get_mut(at..)
774 .and_then(|rest| rest.get_mut(..expected))
775 {
776 slot.copy_from_slice(row);
777 }
778 state.band_rows += 1;
779 state.rows_written += 1;
780
781 if state.band_rows == state.band_height {
782 state.fill_planes();
783 state.encode_band();
784 state.band_rows = 0;
785 sink.write_all(&state.writer.bytes)?;
788 state.writer.bytes.clear();
789 }
790 Ok(())
791 }
792
793 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
794 let Some(state) = self.state.as_mut() else {
795 return Err(PixelsError::invalid_argument(
796 "sink",
797 "finish called before write_header",
798 ));
799 };
800 if state.rows_written < state.descriptor.height {
801 return Err(PixelsError::malformed(
802 "jpeg",
803 format!(
804 "{} of {} rows were written",
805 state.rows_written, state.descriptor.height
806 ),
807 ));
808 }
809
810 if state.band_rows > 0 {
814 state.fill_planes();
815 state.encode_band();
816 state.band_rows = 0;
817 }
818 state.writer.flush();
819 sink.write_all(&state.writer.bytes)?;
820 state.writer.bytes.clear();
821
822 write_marker(marker::EOI, sink)?;
823 sink.flush()
824 }
825}
826
827#[cfg(test)]
828#[allow(
829 clippy::unwrap_used,
830 clippy::indexing_slicing,
831 reason = "tests operate on known-good values and assert shapes directly"
832)]
833mod tests {
834 use super::*;
835
836 #[test]
837 fn magnitude_categories_mirror_the_decoder() {
838 for value in [-2047_i32, -255, -8, -3, -2, -1, 1, 2, 3, 8, 255, 2047] {
841 let (size, bits) = magnitude(value);
842 assert!(size <= 15, "{value}: size {size}");
843 let threshold = 1_i32 << (size - 1);
845 let raw = bits as i32;
846 let decoded = if raw < threshold {
847 raw - (1_i32 << size) + 1
848 } else {
849 raw
850 };
851 assert_eq!(decoded, value, "{value} round-tripped as {decoded}");
852 }
853 assert_eq!(magnitude(0), (0, 0));
854 }
855
856 #[test]
857 fn the_bit_writer_stuffs_ff_bytes() {
858 let mut writer = BitWriter::default();
859 writer.write(0xFF, 8);
860 assert_eq!(
861 writer.bytes,
862 vec![0xFF, 0x00],
863 "a literal FF must be stuffed"
864 );
865
866 let mut writer = BitWriter::default();
867 writer.write(0b1010, 4);
868 writer.write(0b0101, 4);
869 assert_eq!(writer.bytes, vec![0b1010_0101]);
870 }
871
872 #[test]
873 fn flushing_pads_with_one_bits() {
874 let mut writer = BitWriter::default();
875 writer.write(0b101, 3);
876 writer.flush();
877 assert_eq!(writer.bytes, vec![0b1011_1111]);
879
880 let mut writer = BitWriter::default();
882 writer.write(0xAB, 8);
883 writer.flush();
884 assert_eq!(writer.bytes, vec![0xAB]);
885 }
886
887 #[test]
888 fn colour_conversion_round_trips_through_the_decoder() {
889 for rgb in [
893 [0, 0, 0],
894 [255, 255, 255],
895 [128, 128, 128],
896 [255, 0, 0],
897 [0, 255, 0],
898 [0, 0, 255],
899 [37, 142, 201],
900 ] {
901 let ycbcr = rgb_to_ycbcr(rgb);
902 if rgb[0] == rgb[1] && rgb[1] == rgb[2] {
903 assert_eq!(ycbcr[0], rgb[0], "grey should map to its own luma");
904 assert_eq!([ycbcr[1], ycbcr[2]], [128, 128], "grey has no chroma");
905 }
906 let back = crate::decoder::ycbcr_to_rgb(ycbcr);
907 for channel in 0..3 {
908 assert!(
909 back[channel].abs_diff(rgb[channel]) <= 2,
910 "{rgb:?} -> {ycbcr:?} -> {back:?}"
911 );
912 }
913 }
914 }
915
916 #[test]
917 fn unsupported_pixel_formats_are_refused_at_the_header() {
918 for format in [
919 PixelFormat::Gray16,
920 PixelFormat::Rgb16,
921 PixelFormat::Rgba16,
922 PixelFormat::RgbF32,
923 PixelFormat::RgbaF32,
924 ] {
925 let descriptor = ImageDescriptor::new(8, 8, format).unwrap();
926 let mut sink = Vec::new();
927 let error = JpegEncoder::new()
928 .write_header(&descriptor, &mut sink)
929 .unwrap_err();
930 assert_eq!(
931 error.code(),
932 otf_pixels_core::ErrorCode::Unsupported,
933 "{format}"
934 );
935 assert!(sink.is_empty(), "{format}: bytes were written anyway");
936 }
937 }
938
939 #[test]
940 fn quality_selects_subsampling_but_can_be_overridden() {
941 assert_eq!(
942 JpegEncoder::with_quality(80).unwrap().subsampling(),
943 Subsampling::Both
944 );
945 assert_eq!(
946 JpegEncoder::with_quality(95).unwrap().subsampling(),
947 Subsampling::None
948 );
949 assert_eq!(
950 JpegEncoder::with_quality(95)
951 .unwrap()
952 .with_subsampling(Subsampling::Horizontal)
953 .subsampling(),
954 Subsampling::Horizontal
955 );
956 assert!(JpegEncoder::with_quality(0).is_err());
957 assert!(JpegEncoder::with_quality(101).is_err());
958 }
959}