1use 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
22const 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
28const GROUP: usize = 256;
30const LF_GROUP: usize = GROUP * 8;
33const MAX_DIMENSION: u32 = 1 << 30;
35
36#[derive(Debug)]
38pub struct JxlEncoder {
39 state: Option<State>,
40 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 #[must_use]
60 pub const fn new() -> Self {
61 Self {
62 state: None,
63 distance: Some(1.0),
64 }
65 }
66
67 #[must_use]
69 pub const fn lossless() -> Self {
70 Self {
71 state: None,
72 distance: None,
73 }
74 }
75
76 #[must_use]
79 pub const fn with_distance(distance: f32) -> Self {
80 Self {
81 state: None,
82 distance: Some(distance),
83 }
84 }
85
86 #[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
98fn 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
110fn 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
135fn 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
162fn 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 info.grey = false;
192 info.xyb = true;
193 info.bit_depth = 8;
194 Ok(crate::vardct::encode(&rgb, &info, distance))
195}
196
197fn 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 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 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(®ion, c as u32, &tree, &mut tokens);
239 }
240 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 let mut global = BitWriter::new();
250 global.bool(true); global.bool(true); 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 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 §ions {
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}