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(needle:b"\xFF\x0A") || header.starts_with(needle:b"\x00\x00\x00\x0CJXL \x0D\x0A\x87\x0A")
179}
180