imagesize/formats/
jxl.rs

1use crate::util::*;
2use crate::{ImageError, ImageResult, ImageSize};
3
4use std::io::{BufRead, Read, Seek, SeekFrom};
5
6pub fn size<R: BufRead + Seek>(reader: &mut R) -> ImageResult<ImageSize> {
7    let mut file_header = [0; 16]; // The size is variable, but doesn't exceed 16 bytes
8    let mut header_size = 0;
9
10    reader.seek(SeekFrom::Start(0))?;
11    reader.read_exact(&mut file_header[..2])?;
12
13    if &file_header[..2] == b"\xFF\x0A" {
14        // Raw data: Read header directly
15        header_size = reader.read(&mut file_header[2..])? + 2;
16    } else {
17        // Container format: Read from a single jxlc box or multiple jxlp boxes
18        reader.seek(SeekFrom::Start(12))?;
19
20        loop {
21            let (box_type, box_size) = read_tag(reader)?;
22            let box_start = reader.stream_position()? - 8;
23
24            // If box_size is 1, the real size is stored in the first 8 bytes of content.
25            // If box_size is 0, the box ends at EOF.
26
27            let box_size = match box_size {
28                1 => {
29                    let mut box_size = [0; 8];
30                    reader.read_exact(&mut box_size)?;
31                    u64::from_be_bytes(box_size)
32                }
33                _ => box_size as u64,
34            };
35
36            let box_end = box_start
37                .checked_add(box_size)
38                .ok_or(ImageError::CorruptedImage)?;
39            let box_header_size = reader.stream_position()? - box_start;
40
41            if box_size != 0 && box_size < box_header_size {
42                return Err(std::io::Error::new(
43                    std::io::ErrorKind::InvalidData,
44                    format!("Invalid size for {} box: {}", box_type, box_size),
45                )
46                .into());
47            }
48
49            let mut box_reader = match box_size {
50                0 => reader.take(file_header.len() as u64),
51                _ => reader.take(box_size - box_header_size),
52            };
53
54            // The jxlc box must contain the complete codestream
55
56            if box_type == "jxlc" {
57                header_size = box_reader.read(&mut file_header)?;
58                break;
59            }
60
61            // Or it could be stored as part of multiple jxlp boxes
62
63            if box_type == "jxlp" {
64                let mut jxlp_index = [0; 4];
65                box_reader.read_exact(&mut jxlp_index)?;
66
67                header_size += box_reader.read(&mut file_header[header_size..])?;
68
69                // If jxlp_index has the high bit set to 1, this is the final jxlp box
70                if header_size == file_header.len() || (jxlp_index[0] & 0x80) != 0 {
71                    break;
72                }
73            }
74
75            if box_size == 0 {
76                break;
77            }
78
79            reader.seek(SeekFrom::Start(box_end))?;
80        }
81    }
82
83    if header_size < 2 {
84        return Err(ImageError::CorruptedImage);
85    }
86
87    if &file_header[0..2] != b"\xFF\x0A" {
88        return Err(
89            std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid JXL signature").into(),
90        );
91    }
92
93    // Parse the header data
94
95    let file_header = u128::from_le_bytes(file_header);
96    let header_size = 8 * header_size;
97
98    let is_small = read_bits(file_header, 1, 16, header_size)? != 0;
99
100    // Extract image height:
101    //     For small images, the height is stored in the next 5 bits
102    //     For non-small images, the next two bits are used to determine the number of bits to read
103
104    let height_selector = read_bits(file_header, 2, 17, header_size)?;
105
106    let (height_bits, height_offset, height_shift) = match (is_small, height_selector) {
107        (true, _) => (5, 17, 3),
108        (false, 0) => (9, 19, 0),
109        (false, 1) => (13, 19, 0),
110        (false, 2) => (18, 19, 0),
111        (false, 3) => (30, 19, 0),
112        (false, _) => (0, 0, 0),
113    };
114
115    let height =
116        (read_bits(file_header, height_bits, height_offset, header_size)? + 1) << height_shift;
117
118    // Extract image width:
119    //     If ratio is 0, use the same logic as before
120    //     Otherwise, the width is calculated using a predefined aspect ratio
121
122    let ratio = read_bits(file_header, 3, height_bits + height_offset, header_size)?;
123    let width_selector = read_bits(file_header, 2, height_bits + height_offset + 3, 128)?;
124
125    let (width_bits, width_offset, width_shift) = match (is_small, width_selector) {
126        (true, _) => (5, 25, 3),
127        (false, 0) => (9, height_bits + height_offset + 5, 0),
128        (false, 1) => (13, height_bits + height_offset + 5, 0),
129        (false, 2) => (18, height_bits + height_offset + 5, 0),
130        (false, 3) => (30, height_bits + height_offset + 5, 0),
131        (false, _) => (0, 0, 0),
132    };
133
134    let width = match ratio {
135        1 => height,             // 1:1
136        2 => (height / 10) * 12, // 12:10
137        3 => (height / 3) * 4,   // 4:3
138        4 => (height / 2) * 3,   // 3:2
139        5 => (height / 9) * 16,  // 16:9
140        6 => (height / 4) * 5,   // 5:4
141        7 => height * 2,         // 2:1
142        _ => (read_bits(file_header, width_bits, width_offset, header_size)? + 1) << width_shift,
143    };
144
145    // Extract orientation:
146    //     This value overrides the orientation in EXIF metadata
147
148    let metadata_offset = match ratio {
149        0 => width_bits + width_offset,
150        _ => height_bits + height_offset + 3,
151    };
152
153    let all_default = read_bits(file_header, 1, metadata_offset, header_size)? != 0;
154
155    let orientation = match all_default {
156        true => 0,
157        false => {
158            let extra_fields = read_bits(file_header, 1, metadata_offset + 1, header_size)? != 0;
159
160            match extra_fields {
161                false => 0,
162                true => read_bits(file_header, 3, metadata_offset + 2, header_size)?,
163            }
164        }
165    };
166
167    if orientation < 4 {
168        Ok(ImageSize { width, height })
169    } else {
170        Ok(ImageSize {
171            width: height,
172            height: width,
173        })
174    }
175}
176
177pub fn matches(header: &[u8]) -> bool {
178    header.starts_with(b"\xFF\x0A") || header.starts_with(b"\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A")
179}