Skip to main content

otf_pixels_codec_jpeg/
encoder.rs

1//! The baseline JPEG encoder.
2//!
3//! # Memory
4//!
5//! Encoding is streaming, at one MCU row — the same band structure the
6//! decoder uses, run the other way. Rows arrive one at a time, accumulate
7//! until a whole MCU row is available (8 or 16 of them), and are then
8//! colour-converted, transformed, quantized, entropy-coded and written
9//! straight to the sink. Nothing accumulates across bands, so peak memory is
10//! a band and not the image.
11//!
12//! That is why the standard Huffman tables are used rather than tables
13//! derived from the image's own statistics: optimal tables need a counting
14//! pass over every coefficient before the first byte can be written, which
15//! means buffering the whole image. The few percent it would save is not
16//! worth trading ADR-0005's streaming contract for.
17//!
18//! # What is written
19//!
20//! Baseline sequential JPEG: `SOI`, `APP0` (JFIF), `DQT`, `SOF0`, `DHT`,
21//! `SOS`, one interleaved scan, `EOI`. No restart markers — they cost bytes
22//! and buy resynchronization after corruption, which matters for broadcast
23//! and not for a file a pipeline just produced.
24
25use 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/// How much the chroma channels are subsampled relative to luma.
34///
35/// Chroma carries far less perceptible detail than luma, so discarding three
36/// quarters of it is nearly free visually and saves a substantial fraction of
37/// the bytes. It is not free for synthetic images with hard colour edges,
38/// which is why it can be turned off.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40#[non_exhaustive]
41pub enum Subsampling {
42    /// 4:4:4 — full-resolution chroma.
43    None,
44    /// 4:2:2 — chroma at half width.
45    Horizontal,
46    /// 4:2:0 — chroma at half width and half height.
47    #[default]
48    Both,
49}
50
51impl Subsampling {
52    /// The luma sampling factors this implies. Chroma is always 1x1.
53    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/// Encodes a baseline JPEG stream.
63#[derive(Debug)]
64pub struct JpegEncoder {
65    quality: u8,
66    subsampling: Subsampling,
67    /// Set by `write_header`; its presence means the header was written.
68    state: Option<State>,
69    /// The ICC profile to embed after the JFIF header, if any.
70    icc: Option<Vec<u8>>,
71}
72
73/// Everything fixed once the descriptor is known.
74#[derive(Debug)]
75struct State {
76    descriptor: ImageDescriptor,
77    /// Whether the image is coded as one luminance component or three.
78    grayscale: bool,
79    /// Luma sampling factors; chroma is always 1x1.
80    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    /// Input rows accumulated for the MCU row being built.
88    band: Vec<u8>,
89    /// How many rows of `band` hold real pixels.
90    band_rows: u32,
91    /// Pixel height of one MCU row.
92    band_height: u32,
93    mcus_per_line: u32,
94    /// Padded component planes, reused across bands.
95    luma: Plane,
96    chroma_blue: Plane,
97    chroma_red: Plane,
98    /// Full-resolution Cb/Cr for the band, interleaved, before box filtering.
99    chroma_full: Vec<u8>,
100    /// Per-component DC predictor.
101    predictors: [i32; 3],
102    writer: BitWriter,
103    rows_written: u32,
104}
105
106/// One component's padded plane for the MCU row being encoded.
107#[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    /// Copy the 8x8 block whose top-left corner is at `(x, y)`.
124    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
139/// A chroma plane, or an empty one for a greyscale image that has no chroma.
140fn 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/// Accumulates entropy-coded bits, stuffing `0xFF` bytes as it goes.
149#[derive(Debug, Default)]
150struct BitWriter {
151    /// Completed bytes, drained to the sink after each MCU row.
152    bytes: Vec<u8>,
153    accumulator: u32,
154    bits: u32,
155}
156
157impl BitWriter {
158    /// Append `length` low bits of `code`, most significant first.
159    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            // `0xFF` introduces a marker, so entropy data escapes a literal
175            // one as `FF 00`. Omitting this is the classic encoder bug: the
176            // file decodes correctly until the first pixel that happens to
177            // produce an `0xFF`.
178            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    /// Pad to a byte boundary with one bits.
187    ///
188    /// One bits rather than zero: a trailing run of zeros could be read as
189    /// the start of a valid code, where a run of ones cannot be — no standard
190    /// table assigns an all-ones code.
191    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
199/// The number of bits needed to code `value`, and the bits themselves.
200///
201/// JPEG codes a coefficient as a magnitude category plus that many raw bits,
202/// where a negative value is stored as its predecessor in the category's
203/// lower half — the mirror of `receive_extend` on the decoding side.
204fn 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` in `size` bits: -1 becomes 0, -2 and -3 become 1 and 2.
211        (value - 1) as u32 & ((1_u32 << size) - 1)
212    } else {
213        value as u32
214    };
215    (size, bits)
216}
217
218impl JpegEncoder {
219    /// An encoder at the default quality.
220    #[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    /// An encoder at an explicit quality, 1..=100.
231    ///
232    /// Chroma subsampling follows quality, as it does in every encoder that
233    /// exposes one number: 4:4:4 from 90 up, 4:2:0 below. Above 90 the
234    /// subsampling, not the quantization, becomes the dominant loss, so
235    /// keeping it on would make the quality number stop meaning anything.
236    ///
237    /// # Errors
238    ///
239    /// Returns [`PixelsError::InvalidArgument`] unless `quality` is 1..=100.
240    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    /// Override the chroma subsampling chosen by quality.
260    #[must_use]
261    pub const fn with_subsampling(mut self, subsampling: Subsampling) -> Self {
262        self.subsampling = subsampling;
263        self
264    }
265
266    /// An encoder configured from generic encode options.
267    #[must_use]
268    pub fn from_options(options: &EncodeOptions) -> Self {
269        Self::with_quality(options.quality).unwrap_or_else(|_| Self::new())
270    }
271
272    /// The chosen chroma subsampling.
273    #[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
285/// How many channels a row of `format` carries, and whether JPEG can take it.
286fn 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        // JPEG is 8-bit by definition at baseline, and has no alpha at all.
293        other => Err(PixelsError::unsupported(format!(
294            "JPEG encoding needs an 8-bit format; got {other}. Convert first."
295        ))),
296    }
297}
298
299/// Read one pixel as RGB, compositing any alpha against black.
300///
301/// Alpha is composited rather than dropped, for the reason the GIF encoder
302/// gives: JPEG has no transparency, so a translucent pixel has to become
303/// *something*, and blending against black is what a viewer shows for a
304/// flattened image.
305fn 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        // Rgb8 and anything `channels_of` already rejected.
329        _ => [get(0) as u8, get(1) as u8, get(2) as u8],
330    }
331}
332
333/// Convert one RGB triple to YCbCr in fixed point.
334///
335/// The inverse of the decoder's transform, with JFIF's coefficients scaled by
336/// 2^16, and fixed point for the same reason (ADR-0011).
337fn 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    /// Fill the component planes from the rows accumulated in `band`.
353    ///
354    /// Padding is edge replication in both axes: the format codes whole
355    /// blocks, so an image whose width or height is not a multiple of the MCU
356    /// size has samples that must be invented. Replicating the edge invents
357    /// the ones that cost the fewest bits — a constant would put an artificial
358    /// edge inside the last block, and the DCT would spend high-frequency
359    /// coefficients describing it.
360    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        // The last row that holds real pixels; everything below it repeats.
367        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                // Full-resolution chroma is kept only until it is averaged
390                // down, which is why it lives in scratch rather than a plane.
391                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        // Box-filter chroma down to its own resolution. Averaging is what
403        // makes subsampling a resampling rather than a decimation: keeping
404        // every other sample instead would alias hard edges into the chroma.
405        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    /// Transform, quantize and entropy-code one MCU row.
430    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            // Luma blocks first, in raster order within the MCU: that is the
440            // interleave order the scan header declares.
441            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
481/// Entropy-code one quantized block.
482fn 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    // DC is coded as a difference from the previous block of the same
491    // component, because neighbouring blocks of a photograph have nearly the
492    // same average brightness.
493    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        // The run length field holds four bits, so longer runs are broken up
514        // with the zero-run-length code.
515        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    // A trailing run of zeros is collapsed into one end-of-block code, which
529    // is where most of JPEG's compression comes from.
530    if run > 0 {
531        if let Some((code, length)) = ac.code(0x00) {
532            writer.write(code, length);
533        }
534    }
535}
536
537/// Write a marker with no payload.
538fn write_marker(code: u8, sink: &mut dyn Sink) -> Result<()> {
539    sink.write_all(&[0xFF, code])
540}
541
542/// Write a marker segment, prefixing the length its payload implies.
543fn 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
555/// Write one quantization table, in the zigzag order `DQT` uses.
556fn write_quant_table(slot: u8, steps: &[u16; 64], payload: &mut Vec<u8>) {
557    // Precision 0 (8-bit) in the high nibble, slot in the low.
558    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
564/// Write one Huffman table definition.
565fn 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        // 255 segments is the most the sequence numbers can count.
586        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        // JPEG dimensions are 16-bit; a larger image cannot be represented at
605        // all, so this is a format limit rather than a policy.
606        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        // Subsampling chroma that does not exist would only pad the planes.
615        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        // A JFIF header. Nothing here needs it — the density fields are the
632        // only content and they say "no units" — but its absence makes some
633        // consumers guess at the colour model rather than assume YCbCr.
634        write_segment(
635            marker::APP0,
636            &[
637                b'J', b'F', b'I', b'F', 0, // identifier
638                1, 2, // version 1.02
639                0, // density units: none
640                0, 1, 0, 1, // pixel aspect ratio 1:1
641                0, 0, // no thumbnail
642            ],
643            sink,
644        )?;
645        // The profile, in as many APP2 segments as it takes.
646        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        // Spectral selection 0..=63 with no successive approximation: the
715        // only shape a baseline scan may take.
716        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            // A greyscale image has no chroma to hold, so none is allocated.
735            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            // Drained here rather than at the end: this is what makes the
786            // encoder streaming rather than merely incremental.
787            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        // A final partial MCU row still has to be coded: the format has no
811        // way to say "this image ends mid-block". `fill_planes` pads it by
812        // replicating the last real row.
813        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        // The property: `magnitude` and the decoder's `receive_extend` are
839        // inverses over every value a coefficient can take.
840        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            // Reproduce the decoder's extension.
844            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        // Five bits of padding, all ones.
878        assert_eq!(writer.bytes, vec![0b1011_1111]);
879
880        // Already aligned: nothing is added.
881        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        // Grey stays grey and the primaries land where they should; the exact
890        // values are checked against the decoder's inverse rather than
891        // hardcoded, because the pair being inverses is the property.
892        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}