Coverage Report

Created: 2025-03-04 07:22

/src/serenity/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Copyright (c) 2023, Nicolas Ramz <nicolas.ramz@gmail.com>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/ByteReader.h>
8
#include <AK/Debug.h>
9
#include <AK/Endian.h>
10
#include <AK/FixedArray.h>
11
#include <AK/IntegralMath.h>
12
#include <LibCompress/PackBitsDecoder.h>
13
#include <LibGfx/FourCC.h>
14
#include <LibGfx/ImageFormats/ILBMLoader.h>
15
#include <LibRIFF/IFF.h>
16
17
namespace Gfx {
18
19
static constexpr size_t const ilbm_header_size = 12;
20
21
enum class CompressionType : u8 {
22
    None = 0,
23
    ByteRun = 1,
24
    __Count
25
};
26
27
enum class MaskType : u8 {
28
    None = 0,
29
    HasMask = 1,
30
    HasTransparentColor = 2,
31
    HasLasso = 3,
32
    __Count
33
};
34
35
enum class ViewportMode : u32 {
36
    EHB = 0x80,
37
    HAM = 0x800
38
};
39
40
enum class Format : u8 {
41
    // Amiga interleaved format
42
    ILBM = 0,
43
    // PC-DeluxePaint chunky format
44
    PBM = 1
45
};
46
47
AK_ENUM_BITWISE_OPERATORS(ViewportMode);
48
49
struct BMHDHeader {
50
    BigEndian<u16> width;
51
    BigEndian<u16> height;
52
    BigEndian<i16> x;
53
    BigEndian<i16> y;
54
    u8 planes;
55
    MaskType mask;
56
    CompressionType compression;
57
    u8 pad;
58
    BigEndian<u16> transparent_color;
59
    u8 x_aspect;
60
    u8 y_aspect;
61
    BigEndian<u16> page_width;
62
    BigEndian<u16> page_height;
63
};
64
65
static_assert(sizeof(BMHDHeader) == 20);
66
67
struct ILBMLoadingContext {
68
    enum class State {
69
        NotDecoded = 0,
70
        HeaderDecoded,
71
        BitmapDecoded
72
    };
73
    State state { State::NotDecoded };
74
    ReadonlyBytes data;
75
76
    // points to current chunk
77
    ReadonlyBytes chunks_cursor;
78
79
    // max number of bytes per plane row
80
    u16 pitch;
81
82
    ViewportMode viewport_mode;
83
84
    Vector<Color> color_table;
85
86
    // number of bits needed to describe current palette
87
    u8 cmap_bits;
88
89
    RefPtr<Gfx::Bitmap> bitmap;
90
91
    BMHDHeader bm_header;
92
93
    Format format;
94
};
95
96
static ErrorOr<void> decode_iff_ilbm_header(ILBMLoadingContext& context)
97
473
{
98
473
    if (context.state >= ILBMLoadingContext::State::HeaderDecoded)
99
0
        return {};
100
101
473
    if (context.data.size() < ilbm_header_size)
102
0
        return Error::from_string_literal("Missing IFF header");
103
104
473
    auto header_stream = FixedMemoryStream { context.data };
105
473
    auto header = TRY(IFF::FileHeader::read_from_stream(header_stream));
106
473
    if (header.magic() != "FORM"sv || (header.subformat != "ILBM"sv && header.subformat != "PBM "sv))
107
4
        return Error::from_string_literal("Invalid IFF-ILBM header");
108
109
469
    context.format = header.subformat == "ILBM" ? Format::ILBM : Format::PBM;
110
111
469
    return {};
112
473
}
113
114
static ErrorOr<Vector<Color>> decode_cmap_chunk(IFF::Chunk cmap_chunk)
115
2.92k
{
116
2.92k
    size_t const size = cmap_chunk.size() / 3;
117
2.92k
    Vector<Color> color_table;
118
2.92k
    TRY(color_table.try_ensure_capacity(size));
119
120
586k
    for (size_t i = 0; i < size; ++i) {
121
583k
        color_table.unchecked_append(Color(cmap_chunk[i * 3], cmap_chunk[(i * 3) + 1], cmap_chunk[(i * 3) + 2]));
122
583k
    }
123
124
2.92k
    return color_table;
125
2.92k
}
126
127
static ErrorOr<RefPtr<Gfx::Bitmap>> chunky_to_bitmap(ILBMLoadingContext& context, ByteBuffer const& chunky)
128
6.49k
{
129
6.49k
    auto const width = context.bm_header.width;
130
6.49k
    auto const height = context.bm_header.height;
131
132
6.49k
    RefPtr<Gfx::Bitmap> bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, { width, height }));
133
134
6.49k
    dbgln_if(ILBM_DEBUG, "created Bitmap {}x{}", width, height);
135
136
    // - For 24bit pictures: the chunky buffer contains 3 bytes (R,G,B) per pixel
137
    // - For indexed colored pictures: chunky buffer contains a single byte per pixel
138
6.49k
    u8 pixel_size = AK::max(1, context.bm_header.planes / 8);
139
140
1.05M
    for (int row = 0; row < height; ++row) {
141
        // Keep color: in HAM mode, current color
142
        // may be based on previous color instead of coming from
143
        // the palette.
144
1.04M
        Color color = Color::Black;
145
451M
        for (int col = 0; col < width; col++) {
146
450M
            size_t index = (width * row * pixel_size) + (col * pixel_size);
147
450M
            if (context.bm_header.planes == 24) {
148
17.7k
                color = Color(chunky[index], chunky[index + 1], chunky[index + 2]);
149
450M
            } else if (chunky[index] < context.color_table.size()) {
150
450M
                color = context.color_table[chunky[index]];
151
450M
                if (context.bm_header.mask == MaskType::HasTransparentColor && chunky[index] == context.bm_header.transparent_color)
152
255
                    color = color.with_alpha(0);
153
450M
            } else if (has_flag(context.viewport_mode, ViewportMode::HAM)) {
154
                // Get the control bit which will tell use how current pixel should be calculated
155
19.3k
                u8 control = (chunky[index] >> context.cmap_bits) & 0x3;
156
                // Since we only have (cmap_bits - 2) bits to define the component,
157
                // we need to pad it to 8 bits.
158
19.3k
                u8 component = (chunky[index] % context.color_table.size()) << (8 - context.cmap_bits);
159
160
19.3k
                if (control == 1) {
161
18.5k
                    color.set_blue(component);
162
18.5k
                } else if (control == 2) {
163
371
                    color.set_red(component);
164
420
                } else {
165
420
                    color.set_green(component);
166
420
                }
167
19.3k
            } else {
168
22
                return Error::from_string_literal("Color map index out of bounds but HAM bit not set");
169
22
            }
170
450M
            bitmap->set_pixel(col, row, color);
171
450M
        }
172
1.04M
    }
173
174
6.46k
    dbgln_if(ILBM_DEBUG, "filled Bitmap");
175
176
6.46k
    return bitmap;
177
6.49k
}
178
179
static ErrorOr<ByteBuffer> planar_to_chunky(ReadonlyBytes bitplanes, ILBMLoadingContext& context)
180
4.80k
{
181
4.80k
    dbgln_if(ILBM_DEBUG, "planar_to_chunky");
182
4.80k
    u16 pitch = context.pitch;
183
4.80k
    u16 width = context.bm_header.width;
184
4.80k
    u16 height = context.bm_header.height;
185
    // mask is added as an extra plane
186
4.80k
    u8 planes = context.bm_header.mask == MaskType::HasMask ? context.bm_header.planes + 1 : context.bm_header.planes;
187
4.80k
    size_t buffer_size = static_cast<size_t>(width) * height;
188
    // If planes number is 24 we'll store R,G,B components so buffer needs to be 3 times width*height
189
    // otherwise we'll store a single 8bit index to the CMAP.
190
4.80k
    if (planes == 24)
191
280
        buffer_size *= 3;
192
193
4.80k
    auto chunky = TRY(ByteBuffer::create_zeroed(buffer_size));
194
195
0
    u8 const pixel_size = AK::max(1, planes / 8);
196
197
1.16M
    for (u16 y = 0; y < height; y++) {
198
1.16M
        size_t scanline = static_cast<size_t>(y) * width;
199
1.28M
        for (u8 p = 0; p < planes; p++) {
200
122k
            u8 const plane_mask = 1 << (p % 8);
201
122k
            size_t offset_base = (pitch * planes * y) + (p * pitch);
202
122k
            if (offset_base + pitch > bitplanes.size())
203
22
                return Error::from_string_literal("Malformed bitplane data");
204
205
2.31M
            for (u16 i = 0; i < pitch; i++) {
206
2.18M
                u8 bit = bitplanes[offset_base + i];
207
2.18M
                u8 rgb_shift = p / 8;
208
209
                // Some encoders don't pad bytes rows with 0: make sure we stop
210
                // when enough data for current bitplane row has been read
211
18.4M
                for (u8 b = 0; b < 8 && (i * 8) + b < width; b++) {
212
16.2M
                    u8 mask = 1 << (7 - b);
213
                    // get current plane: simply skip mask plane for now
214
16.2M
                    if (bit & mask && p < context.bm_header.planes) {
215
2.99M
                        u16 x = (i * 8) + b;
216
2.99M
                        size_t offset = (scanline * pixel_size) + (x * pixel_size) + rgb_shift;
217
                        // Only throw an error if we would actually attempt to write
218
                        // outside of the chunky buffer. Some apps like PPaint produce
219
                        // malformed bitplane data but files are still accepted by most readers
220
                        // since they do not cause writing past the chunky buffer.
221
2.99M
                        if (offset >= chunky.size())
222
0
                            return Error::from_string_literal("Malformed bitplane data");
223
224
2.99M
                        chunky[offset] |= plane_mask;
225
2.99M
                    }
226
16.2M
                }
227
2.18M
            }
228
122k
        }
229
1.16M
    }
230
231
4.78k
    dbgln_if(ILBM_DEBUG, "planar_to_chunky: end");
232
233
4.78k
    return chunky;
234
4.80k
}
235
236
static ErrorOr<ByteBuffer> uncompress_byte_run(ReadonlyBytes data, ILBMLoadingContext& context)
237
4.01k
{
238
4.01k
    auto length = data.size();
239
4.01k
    dbgln_if(ILBM_DEBUG, "uncompress_byte_run pitch={} size={}", context.pitch, data.size());
240
241
4.01k
    size_t plane_data_size = context.pitch * context.bm_header.height * context.bm_header.planes;
242
243
    // The mask is encoded as an extra bitplane but is not counted in the bm_header planes
244
4.01k
    if (context.bm_header.mask == MaskType::HasMask)
245
820
        plane_data_size += context.pitch * context.bm_header.height;
246
247
    // The maximum run length of this compression method is 127 bytes, so the uncompressed size
248
    // cannot be more than 127 times the size of the chunk we are decompressing.
249
4.01k
    if (plane_data_size > NumericLimits<u32>::max() || ceil_div(plane_data_size, 127ul) > length)
250
3
        return Error::from_string_literal("Uncompressed data size too large");
251
252
4.00k
    auto plane_data = TRY(Compress::PackBits::decode_all(data, plane_data_size));
253
254
0
    return plane_data;
255
4.00k
}
256
257
static ErrorOr<void> extend_ehb_palette(ILBMLoadingContext& context)
258
2.40k
{
259
2.40k
    dbgln_if(ILBM_DEBUG, "need to extend palette");
260
79.3k
    for (size_t i = 0; i < 32; ++i) {
261
76.9k
        auto const color = context.color_table[i];
262
76.9k
        TRY(context.color_table.try_append(color.darkened()));
263
76.9k
    }
264
265
2.40k
    return {};
266
2.40k
}
267
268
static ErrorOr<void> reduce_ham_palette(ILBMLoadingContext& context)
269
553
{
270
553
    u8 bits = context.cmap_bits;
271
272
553
    dbgln_if(ILBM_DEBUG, "reduce palette planes={} bits={}", context.bm_header.planes, context.cmap_bits);
273
274
553
    if (bits > context.bm_header.planes) {
275
6
        dbgln_if(ILBM_DEBUG, "need to reduce palette");
276
6
        bits -= (bits - context.bm_header.planes) + 2;
277
        // bits shouldn't theorically be less than 4 bits in HAM mode.
278
6
        if (bits < 4)
279
2
            return Error::from_string_literal("Error while reducing CMAP for HAM: bits too small");
280
281
4
        context.color_table.resize((context.color_table.size() >> bits));
282
4
        context.cmap_bits = bits;
283
4
    }
284
285
551
    return {};
286
553
}
287
288
static ErrorOr<void> decode_body_chunk(IFF::Chunk body_chunk, ILBMLoadingContext& context)
289
6.56k
{
290
6.56k
    dbgln_if(ILBM_DEBUG, "decode_body_chunk {}", body_chunk.size());
291
292
6.56k
    ByteBuffer pixel_data;
293
294
6.56k
    if (context.bm_header.compression == CompressionType::ByteRun) {
295
4.01k
        auto plane_data = TRY(uncompress_byte_run(body_chunk.data(), context));
296
3.96k
        if (context.format == Format::ILBM)
297
3.18k
            pixel_data = TRY(planar_to_chunky(plane_data, context));
298
785
        else
299
785
            pixel_data = plane_data;
300
3.96k
    } else {
301
2.55k
        if (context.format == Format::ILBM)
302
1.62k
            pixel_data = TRY(planar_to_chunky(body_chunk.data(), context));
303
934
        else
304
934
            pixel_data = TRY(ByteBuffer::copy(body_chunk.data().data(), body_chunk.size()));
305
2.55k
    }
306
307
    // Some files already have 64 colors defined in the palette,
308
    // maybe for upward compatibility with 256 colors software/hardware.
309
    // DPaint 4 & previous files only have 32 colors so the
310
    // palette needs to be extended only for these files.
311
6.50k
    if (has_flag(context.viewport_mode, ViewportMode::EHB) && context.color_table.size() < 64) {
312
2.40k
        TRY(extend_ehb_palette(context));
313
4.09k
    } else if (has_flag(context.viewport_mode, ViewportMode::HAM)) {
314
553
        TRY(reduce_ham_palette(context));
315
551
    }
316
317
6.49k
    context.bitmap = TRY(chunky_to_bitmap(context, pixel_data));
318
319
0
    return {};
320
6.49k
}
321
322
static ErrorOr<void> decode_iff_chunks(ILBMLoadingContext& context)
323
435
{
324
435
    auto& chunks = context.chunks_cursor;
325
326
435
    dbgln_if(ILBM_DEBUG, "decode_iff_chunks");
327
328
21.8k
    while (!chunks.is_empty()) {
329
21.8k
        auto chunk = TRY(IFF::Chunk::decode_and_advance(chunks));
330
21.5k
        if (chunk.id() == "CMAP"sv) {
331
            // Some files (HAM mainly) have CMAP chunks larger than the planes they advertise: I'm not sure
332
            // why but we should not return an error in this case.
333
334
2.92k
            context.color_table = TRY(decode_cmap_chunk(chunk));
335
0
            context.cmap_bits = AK::ceil_log2(context.color_table.size());
336
18.6k
        } else if (chunk.id() == "BODY"sv) {
337
6.57k
            if (context.color_table.is_empty() && context.bm_header.planes != 24)
338
1
                return Error::from_string_literal("Decoding indexed BODY chunk without a color map is not currently supported");
339
340
            // Apparently 32bit ilbm files exist: but I wasn't able to find any,
341
            // nor is it documented anywhere, so let's make it clear it's not supported.
342
6.56k
            if (context.bm_header.planes != 24 && context.bm_header.planes > 8)
343
0
                return Error::from_string_literal("Invalid number of bitplanes");
344
345
13.0k
            TRY(decode_body_chunk(chunk, context));
346
0
            context.state = ILBMLoadingContext::State::BitmapDecoded;
347
13.0k
        } else if (chunk.id() == "CRNG"sv) {
348
392
            dbgln_if(ILBM_DEBUG, "Chunk:CRNG");
349
11.6k
        } else if (chunk.id() == "CAMG"sv) {
350
617
            context.viewport_mode = static_cast<ViewportMode>(AK::convert_between_host_and_big_endian(ByteReader::load32(chunk.data().data())));
351
617
            dbgln_if(ILBM_DEBUG, "Chunk:CAMG, Viewport={}, EHB={}, HAM={}", (u32)context.viewport_mode, has_flag(context.viewport_mode, ViewportMode::EHB), has_flag(context.viewport_mode, ViewportMode::HAM));
352
617
        }
353
21.5k
    }
354
355
63
    if (context.state != ILBMLoadingContext::State::BitmapDecoded)
356
26
        return Error::from_string_literal("Missing body chunk");
357
358
37
    return {};
359
63
}
360
361
static ErrorOr<void> decode_bmhd_chunk(ILBMLoadingContext& context)
362
469
{
363
469
    context.chunks_cursor = context.data.slice(sizeof(IFF::FileHeader));
364
469
    auto first_chunk = TRY(IFF::Chunk::decode_and_advance(context.chunks_cursor));
365
366
459
    if (first_chunk.id() != "BMHD"sv)
367
11
        return Error::from_string_literal("IFFImageDecoderPlugin: Invalid chunk type, expected BMHD");
368
369
448
    if (first_chunk.size() < sizeof(BMHDHeader))
370
3
        return Error::from_string_literal("IFFImageDecoderPlugin: Not enough data for header chunk");
371
372
445
    context.bm_header = *bit_cast<BMHDHeader const*>(first_chunk.data().data());
373
374
445
    if (context.bm_header.mask >= MaskType::__Count)
375
9
        return Error::from_string_literal("IFFImageDecoderPlugin: Unsupported mask type");
376
377
436
    if (context.bm_header.compression >= CompressionType::__Count)
378
1
        return Error::from_string_literal("IFFImageDecoderPlugin: Unsupported compression type");
379
380
435
    context.pitch = ceil_div((u16)context.bm_header.width, (u16)16) * 2;
381
382
435
    context.state = ILBMLoadingContext::State::HeaderDecoded;
383
384
435
    dbgln_if(ILBM_DEBUG, "IFFImageDecoderPlugin: BMHD: {}x{} ({},{}), p={}, m={}, c={}",
385
435
        context.bm_header.width,
386
435
        context.bm_header.height,
387
435
        context.bm_header.x,
388
435
        context.bm_header.y,
389
435
        context.bm_header.planes,
390
435
        to_underlying(context.bm_header.mask),
391
435
        to_underlying(context.bm_header.compression));
392
393
435
    return {};
394
436
}
395
396
ILBMImageDecoderPlugin::ILBMImageDecoderPlugin(ReadonlyBytes data, NonnullOwnPtr<ILBMLoadingContext> context)
397
473
    : m_context(move(context))
398
473
{
399
473
    m_context->data = data;
400
473
}
401
402
473
ILBMImageDecoderPlugin::~ILBMImageDecoderPlugin() = default;
403
404
IntSize ILBMImageDecoderPlugin::size()
405
0
{
406
0
    return IntSize { m_context->bm_header.width, m_context->bm_header.height };
407
0
}
408
409
bool ILBMImageDecoderPlugin::sniff(ReadonlyBytes data)
410
0
{
411
0
    ILBMLoadingContext context;
412
0
    context.data = data;
413
414
0
    return !decode_iff_ilbm_header(context).is_error();
415
0
}
416
417
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> ILBMImageDecoderPlugin::create(ReadonlyBytes data)
418
473
{
419
473
    auto context = TRY(try_make<ILBMLoadingContext>());
420
473
    auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) ILBMImageDecoderPlugin(data, move(context))));
421
473
    TRY(decode_iff_ilbm_header(*plugin->m_context));
422
469
    TRY(decode_bmhd_chunk(*plugin->m_context));
423
0
    return plugin;
424
469
}
425
426
ErrorOr<ImageFrameDescriptor> ILBMImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
427
435
{
428
435
    if (index > 0)
429
0
        return Error::from_string_literal("ILBMImageDecoderPlugin: frame index must be 0");
430
431
435
    if (m_context->state < ILBMLoadingContext::State::BitmapDecoded)
432
435
        TRY(decode_iff_chunks(*m_context));
433
434
37
    VERIFY(m_context->bitmap);
435
37
    return ImageFrameDescriptor { m_context->bitmap, 0 };
436
37
}
437
438
}