/src/serenity/Userland/Libraries/LibGfx/ImageFormats/JPEGXLICC.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2024, Lucas Chollet <lucas.chollet@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/ConstrainedStream.h> |
8 | | #include <AK/MemoryStream.h> |
9 | | #include <LibGfx/ImageFormats/JPEGXLCommon.h> |
10 | | #include <LibGfx/ImageFormats/JPEGXLEntropyDecoder.h> |
11 | | |
12 | | namespace Gfx { |
13 | | |
14 | | namespace { |
15 | | /// E.4.1 - Data stream |
16 | | u8 icc_context(u64 i, u8 b1, u8 b2) |
17 | 0 | { |
18 | 0 | u8 p1 = 0; |
19 | 0 | u8 p2 = 0; |
20 | |
|
21 | 0 | if (i <= 128) |
22 | 0 | return 0; |
23 | | |
24 | 0 | if (b1 >= 'a' && b1 <= 'z') |
25 | 0 | p1 = 0; |
26 | 0 | else if (b1 >= 'A' && b1 <= 'Z') |
27 | 0 | p1 = 0; |
28 | 0 | else if (b1 >= '0' && b1 <= '9') |
29 | 0 | p1 = 1; |
30 | 0 | else if (b1 == '.' || b1 == ',') |
31 | 0 | p1 = 1; |
32 | 0 | else if (b1 <= 1) |
33 | 0 | p1 = 2 + b1; |
34 | 0 | else if (b1 > 1 && b1 < 16) |
35 | 0 | p1 = 4; |
36 | 0 | else if (b1 > 240 && b1 < 255) |
37 | 0 | p1 = 5; |
38 | 0 | else if (b1 == 255) |
39 | 0 | p1 = 6; |
40 | 0 | else |
41 | 0 | p1 = 7; |
42 | |
|
43 | 0 | if (b2 >= 'a' && b2 <= 'z') |
44 | 0 | p2 = 0; |
45 | 0 | else if (b2 >= 'A' && b2 <= 'Z') |
46 | 0 | p2 = 0; |
47 | 0 | else if (b2 >= '0' && b2 <= '9') |
48 | 0 | p2 = 1; |
49 | 0 | else if (b2 == '.' || b2 == ',') |
50 | 0 | p2 = 1; |
51 | 0 | else if (b2 < 16) |
52 | 0 | p2 = 2; |
53 | 0 | else if (b2 > 240) |
54 | 0 | p2 = 3; |
55 | 0 | else |
56 | 0 | p2 = 4; |
57 | |
|
58 | 0 | return 1 + p1 + p2 * 8; |
59 | 0 | } |
60 | | |
61 | | ErrorOr<ByteBuffer> read_encoded_icc_stream(LittleEndianInputBitStream& stream) |
62 | 0 | { |
63 | 0 | auto const enc_size = TRY(U64(stream)); |
64 | | |
65 | 0 | auto decoder = TRY(EntropyDecoder::create(stream, 41)); |
66 | | |
67 | 0 | ByteBuffer uncompressed_icc_stream {}; |
68 | 0 | TRY(uncompressed_icc_stream.try_resize(enc_size)); |
69 | 0 | for (u64 index = 0; index < enc_size; ++index) { |
70 | 0 | auto const prev_byte = index > 0 ? uncompressed_icc_stream[index - 1] : 0u; |
71 | 0 | auto const prev_prev_byte = index > 1 ? uncompressed_icc_stream[index - 2] : 0u; |
72 | 0 | auto const context = icc_context(index, prev_byte, prev_prev_byte); |
73 | 0 | uncompressed_icc_stream[index] = TRY(decoder.decode_hybrid_uint(stream, context)); |
74 | 0 | } |
75 | | |
76 | 0 | TRY(decoder.ensure_end_state()); |
77 | 0 | return uncompressed_icc_stream; |
78 | 0 | } |
79 | | /// |
80 | | |
81 | | /// E.4.2 - Encoded ICC stream |
82 | | ErrorOr<u64> read_varint(Stream& stream) |
83 | 0 | { |
84 | 0 | u64 value = 0; |
85 | 0 | u8 shift = 0; |
86 | 0 | while (1) { |
87 | 0 | if (shift >= 56) |
88 | 0 | return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalint shift value in varint"); |
89 | 0 | auto const b = TRY(stream.read_value<u8>()); |
90 | 0 | value += (b & 127) << shift; |
91 | 0 | if (b <= 127) |
92 | 0 | break; |
93 | 0 | shift += 7; |
94 | 0 | } |
95 | 0 | return value; |
96 | 0 | } |
97 | | /// |
98 | | |
99 | | /// E.4.3 - ICC header |
100 | | ErrorOr<void> read_icc_header(Stream& data_stream, u32 output_size, ByteBuffer& out) |
101 | 0 | { |
102 | 0 | u8 const header_size = min(128u, output_size); |
103 | |
|
104 | 0 | for (u8 i = 0; i < header_size; ++i) { |
105 | 0 | auto const e = TRY(data_stream.read_value<u8>()); |
106 | 0 | u8 p = 0; |
107 | |
|
108 | 0 | if (i == 0 || i == 1 || i == 2 || i == 3) { |
109 | | // 'output_size[i]' means byte i of output_size encoded as an |
110 | | // unsigned 32-bit integer in big endian order |
111 | 0 | BigEndian const output_size_as_be { static_cast<u32>(output_size) }; |
112 | 0 | p = bit_cast<u8 const*>(&output_size_as_be)[i]; |
113 | 0 | } else if (i == 8) { |
114 | 0 | p = 4; |
115 | 0 | } else if (i >= 12 && i <= 23) { |
116 | 0 | auto s = "mntrRGB XYZ "sv; |
117 | 0 | p = s[i - 12]; |
118 | 0 | } else if (i >= 36 && i <= 39) { |
119 | 0 | auto s = "acsp"sv; |
120 | 0 | p = s[i - 36]; |
121 | 0 | } else if ((i == 41 || i == 42) && out[40] == 'A') { |
122 | 0 | p = 'P'; |
123 | 0 | } else if (i == 43 && out[40] == 'A') { |
124 | 0 | p = 'L'; |
125 | 0 | } else if (i == 41 && out[40] == 'M') { |
126 | 0 | p = 'S'; |
127 | 0 | } else if (i == 42 && out[40] == 'M') { |
128 | 0 | p = 'F'; |
129 | 0 | } else if (i == 43 && out[40] == 'M') { |
130 | 0 | p = 'T'; |
131 | 0 | } else if (i == 42 && out[40] == 'S' && out[41] == 'G') { |
132 | 0 | p = 'I'; |
133 | 0 | } else if (i == 43 && out[40] == 'S' && out[41] == 'G') { |
134 | 0 | p = 32; |
135 | 0 | } else if (i == 42 && out[40] == 'S' && out[41] == 'U') { |
136 | 0 | p = 'N'; |
137 | 0 | } else if (i == 43 && out[40] == 'S' && out[41] == 'U') { |
138 | 0 | p = 'W'; |
139 | 0 | } else if (i == 70) { |
140 | 0 | p = 246; |
141 | 0 | } else if (i == 71) { |
142 | 0 | p = 214; |
143 | 0 | } else if (i == 73) { |
144 | 0 | p = 1; |
145 | 0 | } else if (i == 78) { |
146 | 0 | p = 211; |
147 | 0 | } else if (i == 79) { |
148 | 0 | p = 45; |
149 | 0 | } else if (i >= 80 && i < 84) { |
150 | 0 | p = out[4 + i - 80]; |
151 | 0 | } |
152 | |
|
153 | 0 | out.append((p + e) & 255); |
154 | 0 | } |
155 | | |
156 | 0 | return {}; |
157 | 0 | } |
158 | | /// |
159 | | |
160 | | /// E.4.4 - ICC tag list |
161 | | |
162 | | ErrorOr<void> append_as_u32_be(ByteBuffer& buffer, u32 value) |
163 | 0 | { |
164 | 0 | BigEndian be_value { value }; |
165 | 0 | return buffer.try_append({ &be_value, sizeof(be_value) }); |
166 | 0 | } |
167 | | ErrorOr<void> read_tag_list(ConstrainedStream& command_stream, Stream& data_stream, ByteBuffer& out) |
168 | 0 | { |
169 | 0 | auto const v = TRY(read_varint(command_stream)); |
170 | 0 | if (v == 0) |
171 | 0 | return {}; |
172 | 0 | auto const num_tags = v - 1; |
173 | |
|
174 | 0 | TRY(append_as_u32_be(out, num_tags)); |
175 | 0 | u32 previous_tagstart = num_tags * 12 + 128; |
176 | 0 | u32 previous_tagsize = 0; |
177 | | |
178 | | // Then, the decoder repeatedly reads a tag as specified by the following code until a tag with tagcode |
179 | | // equal to 0 is read or until the end of the command stream is reached. |
180 | 0 | while (command_stream.remaining() > 0) { |
181 | 0 | auto const command = TRY(command_stream.read_value<u8>()); |
182 | 0 | auto const tagcode = command & 63; |
183 | 0 | if (tagcode == 0) |
184 | 0 | return {}; |
185 | | |
186 | 0 | Array<u8, 4> tag; |
187 | 0 | if (tagcode == 1) { |
188 | 0 | TRY(data_stream.read_until_filled(tag)); |
189 | 0 | } else if (tagcode == 2) { |
190 | 0 | tag = Array<u8, 4>::from_span("rTRC"sv.bytes()); |
191 | 0 | } else if (tagcode == 3) { |
192 | 0 | tag = Array<u8, 4>::from_span("rXYZ"sv.bytes()); |
193 | 0 | } else if (tagcode >= 4 && tagcode < 21) { |
194 | 0 | static constexpr auto strings = to_array( |
195 | 0 | { "cprt"sv, "wtpt"sv, "bkpt"sv, "rXYZ"sv, "gXYZ"sv, "bXYZ"sv, "kXYZ"sv, "rTRC"sv, "gTRC"sv, |
196 | 0 | "bTRC"sv, "kTRC"sv, "chad"sv, "desc"sv, "chrm"sv, "dmnd"sv, "dmdd"sv, "lumi"sv }); |
197 | 0 | tag = Array<u8, 4>::from_span(strings[tagcode - 4].bytes()); |
198 | 0 | } else { |
199 | 0 | return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid tagcode in ICC profile"); |
200 | 0 | } |
201 | | |
202 | 0 | auto tagstart = previous_tagstart + previous_tagsize; |
203 | 0 | if ((command & 64) != 0) |
204 | 0 | tagstart = TRY(read_varint(command_stream)); |
205 | | |
206 | 0 | u32 tagsize = previous_tagsize; |
207 | 0 | if (tag == "rXYZ"sv.bytes() || tag == "gXYZ"sv.bytes() || tag == "bXYZ"sv.bytes() |
208 | 0 | || tag == "kXYZ"sv.bytes() || tag == "wtpt"sv.bytes() || tag == "bkpt"sv.bytes() |
209 | 0 | || tag == "lumi"sv.bytes()) |
210 | 0 | tagsize = 20; |
211 | |
|
212 | 0 | if ((command & 128) != 0) |
213 | 0 | tagsize = TRY(read_varint(command_stream)); |
214 | 0 | previous_tagstart = tagstart; |
215 | 0 | previous_tagsize = tagsize; |
216 | | |
217 | | // Write tag to output |
218 | |
|
219 | 0 | TRY(out.try_append(tag)); |
220 | 0 | TRY(append_as_u32_be(out, tagstart)); |
221 | 0 | TRY(append_as_u32_be(out, tagsize)); |
222 | 0 | if (tagcode == 2) { |
223 | 0 | out.append("gTRC"sv.bytes()); |
224 | 0 | TRY(append_as_u32_be(out, tagstart)); |
225 | 0 | TRY(append_as_u32_be(out, tagsize)); |
226 | | |
227 | 0 | out.append("bTRC"sv.bytes()); |
228 | 0 | TRY(append_as_u32_be(out, tagstart)); |
229 | 0 | TRY(append_as_u32_be(out, tagsize)); |
230 | 0 | } else if (tagcode == 3) { |
231 | 0 | out.append("gXYZ"sv.bytes()); |
232 | 0 | TRY(append_as_u32_be(out, tagstart + tagsize)); |
233 | 0 | TRY(append_as_u32_be(out, tagsize)); |
234 | | |
235 | 0 | out.append("bXYZ"sv.bytes()); |
236 | 0 | TRY(append_as_u32_be(out, tagstart + 2 * tagsize)); |
237 | 0 | TRY(append_as_u32_be(out, tagsize)); |
238 | 0 | } |
239 | 0 | } |
240 | | |
241 | 0 | return {}; |
242 | 0 | } |
243 | | /// |
244 | | |
245 | | /// E.4.5 - Main content |
246 | | ErrorOr<void> shuffle(Bytes bytes, u8 width) |
247 | 0 | { |
248 | 0 | auto temp = TRY(ByteBuffer::create_uninitialized(bytes.size())); |
249 | | |
250 | 0 | u64 const height = (bytes.size() + width - 1) / width; |
251 | |
|
252 | 0 | u64 row_start = 0; |
253 | 0 | u64 j = 0; |
254 | 0 | for (size_t i = 0; i < bytes.size(); i++) { |
255 | 0 | temp[i] = bytes[j]; |
256 | 0 | j += height; |
257 | 0 | if (j >= bytes.size()) { |
258 | 0 | ++row_start; |
259 | 0 | j = row_start; |
260 | 0 | } |
261 | 0 | } |
262 | |
|
263 | 0 | temp.bytes().copy_to(bytes); |
264 | 0 | return {}; |
265 | 0 | } |
266 | | |
267 | | ErrorOr<void> read_icc_main_content(ConstrainedStream& command_stream, Stream& data_stream, ByteBuffer& out) |
268 | 0 | { |
269 | 0 | while (command_stream.remaining() > 0) { |
270 | 0 | auto const command = TRY(command_stream.read_value<u8>()); |
271 | 0 | if (command == 1) { |
272 | 0 | auto const num = TRY(read_varint(command_stream)); |
273 | 0 | auto bytes = TRY(out.get_bytes_for_writing(num)); |
274 | 0 | TRY(data_stream.read_until_filled(bytes)); |
275 | 0 | } else if (command == 2 or command == 3) { |
276 | 0 | auto const num = TRY(read_varint(command_stream)); |
277 | 0 | auto bytes = TRY(out.get_bytes_for_writing(num)); |
278 | 0 | TRY(data_stream.read_until_filled(bytes)); |
279 | 0 | u8 width = (command == 2) ? 2 : 4; |
280 | 0 | TRY(shuffle(bytes, width)); |
281 | 0 | } else if (command == 4) { |
282 | 0 | u8 const flags = TRY(command_stream.read_value<u8>()); |
283 | 0 | u8 const width = (flags & 3) + 1; |
284 | 0 | u8 const order = (flags & 12) >> 2; |
285 | 0 | if (width == 3 || order == 3) |
286 | 0 | return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid width or order value"); |
287 | | |
288 | 0 | u64 stride = width; |
289 | 0 | if ((flags & 16) != 0) |
290 | 0 | stride = TRY(read_varint(command_stream)); |
291 | | |
292 | 0 | if (stride * 4 >= out.size() || stride < width) |
293 | 0 | return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid stride value"); |
294 | | |
295 | 0 | auto const num = TRY(read_varint(command_stream)); |
296 | 0 | ByteBuffer bytes; |
297 | 0 | TRY(bytes.try_resize(num)); |
298 | 0 | TRY(data_stream.read_until_filled(bytes)); |
299 | 0 | if (width == 2 || width == 4) |
300 | 0 | TRY(shuffle(bytes, width)); |
301 | | |
302 | 0 | for (u64 i = 0; i < num; i += width) { |
303 | | // NOTE: 0 <= order <= 2 |
304 | 0 | u8 const N = order + 1; |
305 | 0 | Array<u32, 3> prev {}; |
306 | 0 | for (u8 j = 0; j < N; ++j) { |
307 | | // "read u(width * 8) from the output ICC profile starting from |
308 | | // (stride * (j + 1)) bytes before the current output size, |
309 | | // interpreted as a big-endian unsigned integer of width bytes" |
310 | 0 | Array<u8, 4> bytes {}; |
311 | 0 | for (u8 k = 0; k < width; ++k) |
312 | 0 | bytes[4 - width + k] = out[out.size() - stride * (j + 1) + k]; |
313 | 0 | prev[j] = *bit_cast<BigEndian<u32> const*>(bytes.data()); |
314 | 0 | } |
315 | |
|
316 | 0 | u32 p; |
317 | 0 | if (order == 0) |
318 | 0 | p = prev[0]; |
319 | 0 | else if (order == 1) |
320 | 0 | p = 2 * prev[0] - prev[1]; |
321 | 0 | else if (order == 2) |
322 | 0 | p = 3 * prev[0] - 3 * prev[1] + prev[2]; |
323 | |
|
324 | 0 | for (u8 j = 0; j < width && i + j < num; ++j) { |
325 | 0 | u8 const val = (bytes[i + j] + (p >> (8 * (width - 1 - j)))) & 255; |
326 | 0 | TRY(out.try_append(val)); |
327 | 0 | } |
328 | 0 | } |
329 | 0 | } else if (command == 10) { |
330 | 0 | TRY(out.try_append("XYZ "sv.bytes())); |
331 | 0 | Array<u8, 4> constexpr zeros {}; |
332 | 0 | TRY(out.try_append(zeros)); |
333 | 0 | auto bytes = TRY(out.get_bytes_for_writing(12)); |
334 | 0 | TRY(data_stream.read_until_filled(bytes)); |
335 | 0 | } else if (command >= 16 and command < 24) { |
336 | 0 | Array constexpr strings = { "XYZ "sv, "desc"sv, "text"sv, "mluc"sv, "para"sv, "curv"sv, "sf32"sv, "gbd "sv }; |
337 | 0 | TRY(out.try_append(strings[command - 16].bytes())); |
338 | 0 | Array<u8, 4> constexpr zeros {}; |
339 | 0 | TRY(out.try_append(zeros)); |
340 | 0 | } else { |
341 | 0 | return Error::from_string_literal("JPEGXLImageDecoderPlugin: Invalid command in ICC main context"); |
342 | 0 | } |
343 | 0 | } |
344 | | |
345 | 0 | return {}; |
346 | 0 | } |
347 | | /// |
348 | | } |
349 | | |
350 | | /// E.4 - ICC profile |
351 | | ErrorOr<ByteBuffer> read_icc(LittleEndianInputBitStream& stream) |
352 | 0 | { |
353 | 0 | auto const encoded_icc = TRY(read_encoded_icc_stream(stream)); |
354 | | |
355 | 0 | FixedMemoryStream buffer(encoded_icc); |
356 | 0 | auto const output_size = TRY(read_varint(buffer)); |
357 | 0 | auto const commands_size = TRY(read_varint(buffer)); |
358 | | |
359 | 0 | ConstrainedStream command_stream { MaybeOwned<Stream>(buffer), commands_size }; |
360 | |
|
361 | 0 | auto const data_offset = buffer.offset() + commands_size; |
362 | 0 | FixedMemoryStream data_stream(encoded_icc); |
363 | 0 | TRY(data_stream.discard(data_offset)); |
364 | | |
365 | 0 | ByteBuffer out; |
366 | 0 | TRY(out.try_ensure_capacity(output_size)); |
367 | | |
368 | 0 | TRY(read_icc_header(data_stream, output_size, out)); |
369 | | |
370 | 0 | if (output_size <= 128) |
371 | 0 | return out; |
372 | | |
373 | 0 | TRY(read_tag_list(command_stream, data_stream, out)); |
374 | | |
375 | 0 | TRY(read_icc_main_content(command_stream, data_stream, out)); |
376 | | |
377 | 0 | return out; |
378 | 0 | } |
379 | | /// |
380 | | |
381 | | } |