Coverage Report

Created: 2025-08-28 06:26

/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
}