Skip to main content

otf_pixels_codec_jxl/
encoder.rs

1//! The JPEG XL encoder: lossy VarDCT by default, lossless Modular on
2//! request (and, until VarDCT carries it, for images with alpha).
3//!
4//! Lossless:
5//!
6//! The image becomes integer channels (grey or YCoCg colour, then alpha),
7//! each predicted by the clamped gradient and coded per channel with prefix
8//! codes fitted to its residuals. An image of one 256x256 group is a single
9//! section; a larger one is split into groups, each its own section, behind
10//! a table of contents, all sharing the codes of the global section.
11
12use otf_pixels_core::{
13    ChannelLayout, EncodeOptions, Encoder, ImageDescriptor, PixelsError, Result, SampleKind, Sink,
14};
15
16use crate::bits::BitWriter;
17use crate::entropy::{EntropyCode, HybridUint, Token, with_runs};
18use crate::headers::{ImageInfo, write_image_header, write_modular_frame_header, write_toc};
19use crate::modular::{Channel, Tree, forward_ycocg, residuals, write_header};
20use crate::predict::{Predictor, Region};
21
22/// libjxl's cutoffs for the weighted predictor's error (`kWPFixedDC`).
23const WP_CUTOFFS: [i32; 33] = [
24    -500, -392, -255, -191, -127, -95, -63, -47, -31, -23, -15, -11, -7, -4, -3, -1, 0, 1, 3, 5, 7,
25    11, 15, 23, 31, 47, 63, 95, 127, 191, 255, 392, 500,
26];
27
28/// The side of a group, the unit a large image is split into.
29const GROUP: usize = 256;
30/// The side of an LF group, which a Modular frame without squeeze leaves
31/// empty.
32const LF_GROUP: usize = GROUP * 8;
33/// The largest dimension the size header can declare.
34const MAX_DIMENSION: u32 = 1 << 30;
35
36/// Encodes a JPEG XL codestream.
37#[derive(Debug)]
38pub struct JxlEncoder {
39    state: Option<State>,
40    /// `None` for lossless; otherwise the Butteraugli-style distance.
41    distance: Option<f32>,
42}
43
44impl Default for JxlEncoder {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50#[derive(Debug)]
51struct State {
52    descriptor: ImageDescriptor,
53    pixels: Vec<u8>,
54    rows_written: u32,
55}
56
57impl JxlEncoder {
58    /// A lossy encoder at distance 1, visually lossless.
59    #[must_use]
60    pub const fn new() -> Self {
61        Self {
62            state: None,
63            distance: Some(1.0),
64        }
65    }
66
67    /// A lossless encoder.
68    #[must_use]
69    pub const fn lossless() -> Self {
70        Self {
71            state: None,
72            distance: None,
73        }
74    }
75
76    /// A lossy encoder at `distance`: 1 is visually lossless, 0.5 finer,
77    /// up to 25 for the coarsest.
78    #[must_use]
79    pub const fn with_distance(distance: f32) -> Self {
80        Self {
81            state: None,
82            distance: Some(distance),
83        }
84    }
85
86    /// An encoder configured from generic encode options: lossless if
87    /// `lossless`, otherwise lossy at the distance libjxl's `cjxl` gives
88    /// `quality` (90 is distance 1).
89    #[must_use]
90    pub fn from_options(options: &EncodeOptions) -> Self {
91        if options.lossless {
92            return Self::lossless();
93        }
94        Self::with_distance(distance_for_quality(options.quality))
95    }
96}
97
98/// libjxl's quality-to-distance mapping.
99fn distance_for_quality(quality: u8) -> f32 {
100    let q = f32::from(quality.clamp(1, 100));
101    if q >= 30.0 {
102        0.1 + (100.0 - q) * 0.09
103    } else {
104        53.0 / 3000.0 * q * q - 23.0 / 20.0 * q + 25.0
105    }
106}
107
108impl JxlEncoder {}
109
110/// The header's view of `format`.
111fn info_of(descriptor: &ImageDescriptor) -> Result<ImageInfo> {
112    let format = descriptor.pixel;
113    let bit_depth = match format.sample_kind() {
114        SampleKind::U8 => 8,
115        SampleKind::U16 => 16,
116        _ => {
117            return Err(PixelsError::unsupported(format!(
118                "JPEG XL encoding takes 8- or 16-bit samples; got {format}. Convert first."
119            )));
120        }
121    };
122    Ok(ImageInfo {
123        width: descriptor.width,
124        height: descriptor.height,
125        bit_depth,
126        grey: matches!(
127            format.layout(),
128            ChannelLayout::Gray | ChannelLayout::GrayAlpha
129        ),
130        alpha: format.layout().has_alpha(),
131        xyb: false,
132    })
133}
134
135/// The image's channels: grey or R, G, B, then alpha.
136fn channels_of(descriptor: &ImageDescriptor, pixels: &[u8]) -> Vec<Channel> {
137    let format = descriptor.pixel;
138    let (width, height) = (descriptor.width as usize, descriptor.height as usize);
139    let count = format.channels();
140    let wide = format.sample_kind() == SampleKind::U16;
141    let mut channels = vec![Channel::new(width, height); count];
142    let bytes = if wide { 2 } else { 1 };
143    for (i, pixel) in pixels.chunks_exact(count * bytes).enumerate() {
144        for (c, channel) in channels.iter_mut().enumerate() {
145            let at = c * bytes;
146            let value = if wide {
147                i32::from(u16::from_ne_bytes([
148                    pixel.get(at).copied().unwrap_or(0),
149                    pixel.get(at + 1).copied().unwrap_or(0),
150                ]))
151            } else {
152                i32::from(pixel.get(at).copied().unwrap_or(0))
153            };
154            if let Some(slot) = channel.samples.get_mut(i) {
155                *slot = value;
156            }
157        }
158    }
159    channels
160}
161
162/// The whole lossy codestream for `pixels`, which have no alpha.
163fn encode_lossy(descriptor: &ImageDescriptor, pixels: &[u8], distance: f32) -> Result<Vec<u8>> {
164    let mut info = info_of(descriptor)?;
165    let channels = channels_of(descriptor, pixels);
166    let max = if info.bit_depth == 16 {
167        65_535.0
168    } else {
169        255.0
170    };
171    let n = descriptor.width as usize * descriptor.height as usize;
172    let rgb: Vec<[f32; 3]> = (0..n)
173        .map(|i| {
174            let at = |c: usize| {
175                channels
176                    .get(c)
177                    .and_then(|ch| ch.samples.get(i))
178                    .copied()
179                    .unwrap_or(0) as f32
180                    / max
181            };
182            if info.grey {
183                [at(0); 3]
184            } else {
185                [at(0), at(1), at(2)]
186            }
187        })
188        .collect();
189    // XYB carries colour; the header's sRGB says what to decode it to, and
190    // eight bits is what the samples were.
191    info.grey = false;
192    info.xyb = true;
193    info.bit_depth = 8;
194    Ok(crate::vardct::encode(&rgb, &info, distance))
195}
196
197/// The whole lossless codestream for `pixels`.
198fn encode(descriptor: &ImageDescriptor, pixels: &[u8]) -> Result<Vec<u8>> {
199    let info = info_of(descriptor)?;
200    let mut channels = channels_of(descriptor, pixels);
201    let rct = !info.grey;
202    if rct {
203        forward_ycocg(&mut channels);
204    }
205    let (width, height) = (info.width as usize, info.height as usize);
206    // libjxl's `-e3` tree: the weighted predictor everywhere, contexts by
207    // its error estimate (property 15).
208    let pixels_total = width * height * channels.len();
209    let tree = Tree::fixed(
210        15,
211        &WP_CUTOFFS,
212        Predictor::Weighted,
213        pixels_total,
214        info.bit_depth,
215    );
216    let tree_tokens = tree.tokens();
217    let tree_code = EntropyCode::build(6, &tree_tokens, HybridUint::BUCKETS);
218
219    // Every group's residuals, in raster order of groups; a single group is
220    // the whole image, coded in the global section.
221    let groups_x = width.div_ceil(GROUP);
222    let groups_y = height.div_ceil(GROUP);
223    let single = groups_x * groups_y == 1;
224    let mut group_tokens: Vec<Vec<Token>> = Vec::with_capacity(groups_x * groups_y);
225    for gy in 0..groups_y {
226        for gx in 0..groups_x {
227            let (x0, y0) = (gx * GROUP, gy * GROUP);
228            let region = (x0, y0, GROUP.min(width - x0), GROUP.min(height - y0));
229            let mut tokens = Vec::with_capacity(region.2 * region.3 * channels.len());
230            for (c, channel) in channels.iter().enumerate() {
231                let region = Region {
232                    channel,
233                    x0: region.0,
234                    y0: region.1,
235                    width: region.2,
236                    height: region.3,
237                };
238                residuals(&region, c as u32, &tree, &mut tokens);
239            }
240            // Runs of exact predictions (residual 0) become copies.
241            group_tokens.push(with_runs(&tokens, 0));
242        }
243    }
244    let all: Vec<Token> = group_tokens.iter().flatten().copied().collect();
245    let code = EntropyCode::build(tree.leaves(), &all, HybridUint::DEFAULT);
246
247    // The global section: dequantization defaults, the tree and codes, the
248    // image's header and, for a single group, its residuals.
249    let mut global = BitWriter::new();
250    global.bool(true); // default LF dequantization
251    global.bool(true); // a global tree follows
252    tree_code.write_header(&mut global);
253    for &token in &tree_tokens {
254        tree_code.write(&mut global, token);
255    }
256    code.write_header(&mut global);
257    write_header(&mut global, rct);
258    let mut sections = Vec::new();
259    if single {
260        for &token in &all {
261            code.write(&mut global, token);
262        }
263        sections.push(global.finish());
264    } else {
265        sections.push(global.finish());
266        let lf_groups = width.div_ceil(LF_GROUP) * height.div_ceil(LF_GROUP);
267        // LF groups and the HF global section hold nothing in a lossless
268        // Modular frame.
269        sections.extend(std::iter::repeat_n(Vec::new(), lf_groups + 1));
270        for tokens in &group_tokens {
271            let mut group = BitWriter::new();
272            write_header(&mut group, false);
273            for &token in tokens {
274                code.write(&mut group, token);
275            }
276            sections.push(group.finish());
277        }
278    }
279
280    let mut out = BitWriter::new();
281    write_image_header(&mut out, &info);
282    write_modular_frame_header(&mut out, &info);
283    let sizes: Vec<usize> = sections.iter().map(Vec::len).collect();
284    write_toc(&mut out, &sizes);
285    let mut bytes = out.finish();
286    for section in &sections {
287        bytes.extend_from_slice(section);
288    }
289    Ok(bytes)
290}
291
292impl Encoder for JxlEncoder {
293    fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
294        if self.state.is_some() {
295            return Err(PixelsError::invalid_argument(
296                "descriptor",
297                "write_header called more than once",
298            ));
299        }
300        info_of(desc)?;
301        if desc.width == 0
302            || desc.height == 0
303            || desc.width > MAX_DIMENSION
304            || desc.height > MAX_DIMENSION
305        {
306            return Err(PixelsError::unsupported(format!(
307                "JPEG XL dimensions must be 1 to {MAX_DIMENSION}; {}x{} is not",
308                desc.width, desc.height
309            )));
310        }
311        let capacity = desc
312            .byte_len()
313            .ok_or_else(|| PixelsError::malformed("jxl", "image size overflows"))?;
314        self.state = Some(State {
315            descriptor: *desc,
316            pixels: Vec::with_capacity(capacity),
317            rows_written: 0,
318        });
319        Ok(())
320    }
321
322    fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
323        let Some(state) = self.state.as_mut() else {
324            return Err(PixelsError::invalid_argument(
325                "row",
326                "write_row called before write_header",
327            ));
328        };
329        let expected = state.descriptor.row_bytes();
330        if row.len() != expected {
331            return Err(PixelsError::invalid_argument(
332                "row",
333                format!("row is {} bytes, expected {expected}", row.len()),
334            ));
335        }
336        if state.rows_written >= state.descriptor.height {
337            return Err(PixelsError::invalid_argument(
338                "row",
339                format!("more than {} rows written", state.descriptor.height),
340            ));
341        }
342        state.pixels.extend_from_slice(row);
343        state.rows_written += 1;
344        Ok(())
345    }
346
347    fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
348        let Some(state) = self.state.as_ref() else {
349            return Err(PixelsError::invalid_argument(
350                "sink",
351                "finish called before write_header",
352            ));
353        };
354        if state.rows_written < state.descriptor.height {
355            return Err(PixelsError::malformed(
356                "jxl",
357                format!(
358                    "{} of {} rows were written",
359                    state.rows_written, state.descriptor.height
360                ),
361            ));
362        }
363        let bytes = match self.distance {
364            Some(distance) if !state.descriptor.pixel.layout().has_alpha() => {
365                encode_lossy(&state.descriptor, &state.pixels, distance)?
366            }
367            _ => encode(&state.descriptor, &state.pixels)?,
368        };
369        sink.write_all(&bytes)?;
370        sink.flush()
371    }
372}