1use crate::error::{CodecError, CodecResult};
6
7pub const JXL_CODESTREAM_SIGNATURE: [u8; 2] = [0xFF, 0x0A];
9
10pub const JXL_CONTAINER_SIGNATURE: [u8; 12] = [
12 0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A,
13];
14
15#[derive(Clone, Debug)]
19pub struct JxlHeader {
20 pub width: u32,
22 pub height: u32,
24 pub bits_per_sample: u8,
26 pub num_channels: u8,
28 pub is_float: bool,
30 pub has_alpha: bool,
32 pub color_space: JxlColorSpace,
34 pub orientation: u8,
36}
37
38impl JxlHeader {
39 pub fn srgb(width: u32, height: u32, channels: u8) -> CodecResult<Self> {
41 if channels == 0 || channels > 4 {
42 return Err(CodecError::InvalidParameter(format!(
43 "Invalid channel count: {channels}, must be 1-4"
44 )));
45 }
46 if width == 0 || height == 0 {
47 return Err(CodecError::InvalidParameter(
48 "Width and height must be non-zero".into(),
49 ));
50 }
51 let has_alpha = channels == 2 || channels == 4;
52 let color_space = if channels <= 2 {
53 JxlColorSpace::Gray
54 } else {
55 JxlColorSpace::Srgb
56 };
57 Ok(Self {
58 width,
59 height,
60 bits_per_sample: 8,
61 num_channels: channels,
62 is_float: false,
63 has_alpha,
64 color_space,
65 orientation: 1,
66 })
67 }
68
69 pub fn total_channels(&self) -> u8 {
71 self.num_channels
72 }
73
74 pub fn color_channels(&self) -> u8 {
76 if self.has_alpha {
77 self.num_channels.saturating_sub(1)
78 } else {
79 self.num_channels
80 }
81 }
82
83 pub fn bytes_per_sample(&self) -> usize {
85 match self.bits_per_sample {
86 1..=8 => 1,
87 9..=16 => 2,
88 _ => 4,
89 }
90 }
91
92 pub fn data_size(&self) -> usize {
94 self.width as usize
95 * self.height as usize
96 * self.num_channels as usize
97 * self.bytes_per_sample()
98 }
99
100 pub fn validate(&self) -> CodecResult<()> {
102 if self.width == 0 || self.height == 0 {
103 return Err(CodecError::InvalidParameter(
104 "Width and height must be non-zero".into(),
105 ));
106 }
107 if self.width > 1_073_741_823 || self.height > 1_073_741_823 {
108 return Err(CodecError::InvalidParameter(
109 "Dimensions exceed JPEG-XL maximum (2^30 - 1)".into(),
110 ));
111 }
112 if self.num_channels == 0 || self.num_channels > 4 {
113 return Err(CodecError::InvalidParameter(format!(
114 "Invalid channel count: {}",
115 self.num_channels
116 )));
117 }
118 match self.bits_per_sample {
119 8 | 16 | 32 => {}
120 other => {
121 return Err(CodecError::InvalidParameter(format!(
122 "Unsupported bit depth: {other}, must be 8, 16, or 32"
123 )));
124 }
125 }
126 Ok(())
127 }
128}
129
130impl Default for JxlHeader {
131 fn default() -> Self {
132 Self {
133 width: 0,
134 height: 0,
135 bits_per_sample: 8,
136 num_channels: 3,
137 is_float: false,
138 has_alpha: false,
139 color_space: JxlColorSpace::Srgb,
140 orientation: 1,
141 }
142 }
143}
144
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum JxlColorSpace {
151 Srgb,
153 LinearSrgb,
155 Gray,
157 Xyb,
159}
160
161impl Default for JxlColorSpace {
162 fn default() -> Self {
163 Self::Srgb
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub enum JxlFrameEncoding {
172 VarDct,
174 Modular,
176}
177
178#[derive(Clone, Debug)]
182pub struct JxlConfig {
183 pub quality: f32,
186 pub effort: u8,
191 pub lossless: bool,
193 pub use_container: bool,
195}
196
197impl JxlConfig {
198 pub fn new_lossless() -> Self {
200 Self {
201 quality: 0.0,
202 effort: 7,
203 lossless: true,
204 use_container: false,
205 }
206 }
207
208 pub fn new_lossy(quality: f32) -> Self {
210 Self {
211 quality: quality.clamp(0.0, 100.0),
212 effort: 7,
213 lossless: false,
214 use_container: false,
215 }
216 }
217
218 pub fn with_effort(mut self, effort: u8) -> Self {
220 self.effort = effort.clamp(1, 9);
221 self
222 }
223
224 pub fn frame_encoding(&self) -> JxlFrameEncoding {
226 if self.lossless {
227 JxlFrameEncoding::Modular
228 } else {
229 JxlFrameEncoding::VarDct
230 }
231 }
232
233 pub fn validate(&self) -> CodecResult<()> {
235 if self.effort < 1 || self.effort > 9 {
236 return Err(CodecError::InvalidParameter(format!(
237 "Effort must be 1-9, got {}",
238 self.effort
239 )));
240 }
241 if self.quality < 0.0 || self.quality > 100.0 {
242 return Err(CodecError::InvalidParameter(format!(
243 "Quality must be 0.0-100.0, got {}",
244 self.quality
245 )));
246 }
247 Ok(())
248 }
249}
250
251impl Default for JxlConfig {
252 fn default() -> Self {
253 Self::new_lossless()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 #[ignore]
263 fn test_header_srgb() {
264 let header = JxlHeader::srgb(1920, 1080, 3).expect("valid header");
265 assert_eq!(header.width, 1920);
266 assert_eq!(header.height, 1080);
267 assert_eq!(header.num_channels, 3);
268 assert!(!header.has_alpha);
269 assert_eq!(header.color_space, JxlColorSpace::Srgb);
270 }
271
272 #[test]
273 #[ignore]
274 fn test_header_srgb_rgba() {
275 let header = JxlHeader::srgb(100, 100, 4).expect("valid header");
276 assert!(header.has_alpha);
277 assert_eq!(header.color_channels(), 3);
278 assert_eq!(header.total_channels(), 4);
279 }
280
281 #[test]
282 #[ignore]
283 fn test_header_gray() {
284 let header = JxlHeader::srgb(64, 64, 1).expect("valid header");
285 assert_eq!(header.color_space, JxlColorSpace::Gray);
286 assert!(!header.has_alpha);
287 }
288
289 #[test]
290 #[ignore]
291 fn test_header_invalid_channels() {
292 assert!(JxlHeader::srgb(100, 100, 0).is_err());
293 assert!(JxlHeader::srgb(100, 100, 5).is_err());
294 }
295
296 #[test]
297 #[ignore]
298 fn test_header_zero_dimensions() {
299 assert!(JxlHeader::srgb(0, 100, 3).is_err());
300 assert!(JxlHeader::srgb(100, 0, 3).is_err());
301 }
302
303 #[test]
304 #[ignore]
305 fn test_header_data_size() {
306 let header = JxlHeader::srgb(10, 10, 3).expect("valid");
307 assert_eq!(header.data_size(), 10 * 10 * 3);
308 }
309
310 #[test]
311 #[ignore]
312 fn test_config_lossless() {
313 let config = JxlConfig::new_lossless();
314 assert!(config.lossless);
315 assert_eq!(config.frame_encoding(), JxlFrameEncoding::Modular);
316 }
317
318 #[test]
319 #[ignore]
320 fn test_config_lossy() {
321 let config = JxlConfig::new_lossy(50.0);
322 assert!(!config.lossless);
323 assert_eq!(config.frame_encoding(), JxlFrameEncoding::VarDct);
324 }
325
326 #[test]
327 #[ignore]
328 fn test_config_effort() {
329 let config = JxlConfig::new_lossless().with_effort(3);
330 assert_eq!(config.effort, 3);
331 }
332
333 #[test]
334 #[ignore]
335 fn test_config_validate() {
336 assert!(JxlConfig::new_lossless().validate().is_ok());
337 let mut bad = JxlConfig::new_lossless();
338 bad.effort = 0;
339 assert!(bad.validate().is_err());
340 }
341
342 #[test]
343 #[ignore]
344 fn test_codestream_signature() {
345 assert_eq!(JXL_CODESTREAM_SIGNATURE, [0xFF, 0x0A]);
346 }
347
348 #[test]
349 #[ignore]
350 fn test_container_signature() {
351 assert_eq!(JXL_CONTAINER_SIGNATURE.len(), 12);
352 assert_eq!(&JXL_CONTAINER_SIGNATURE[4..8], b"JXL ");
354 }
355}