Coverage Report

Created: 2025-12-18 07:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibGfx/ImageFormats/TGALoader.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2022, Tom Needham <06needhamt@gmail.com>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/MemoryStream.h>
8
#include <AK/Span.h>
9
#include <AK/StdLibExtraDetails.h>
10
#include <AK/String.h>
11
#include <AK/Traits.h>
12
#include <LibGfx/ImageFormats/TGALoader.h>
13
14
namespace Gfx {
15
16
enum TGADataType : u8 {
17
    None = 0,
18
    UncompressedColorMapped = 1,
19
    UncompressedRGB = 2,
20
    UncompressedBlackAndWhite = 3,
21
    RunLengthEncodedColorMapped = 9,
22
    RunLengthEncodedRGB = 10,
23
    CompressedBlackAndWhite = 11,
24
    CompressedColorMapped = 32,
25
    CompressedColorMappedFourPass = 33
26
};
27
28
struct [[gnu::packed]] TGAHeader {
29
    u8 id_length;
30
    u8 color_map_type;
31
    TGADataType data_type_code;
32
    i16 color_map_origin;
33
    i16 color_map_length;
34
    u8 color_map_depth;
35
    i16 x_origin;
36
    i16 y_origin;
37
    u16 width;
38
    u16 height;
39
    u8 bits_per_pixel;
40
    u8 image_descriptor;
41
};
42
static_assert(sizeof(TGAHeader) == 18);
43
44
}
45
46
class TGAImageIdentifier : public Gfx::Metadata {
47
public:
48
    TGAImageIdentifier(String identifier)
49
88
        : m_identifier(move(identifier))
50
88
    {
51
88
    }
52
53
protected:
54
    virtual void fill_main_tags() const override
55
0
    {
56
0
        if (!m_identifier.is_empty())
57
0
            m_main_tags.set("Identifier"sv, m_identifier);
58
0
    }
59
60
    String m_identifier;
61
};
62
63
template<>
64
struct AK::Traits<Gfx::TGAHeader> : public DefaultTraits<Gfx::TGAHeader> {
65
0
    static constexpr bool is_trivially_serializable() { return true; }
66
};
67
68
namespace Gfx {
69
70
struct TGALoadingContext {
71
    TGALoadingContext(ReadonlyBytes bytes, FixedMemoryStream stream)
72
504
        : bytes(bytes)
73
504
        , stream(move(stream))
74
504
    {
75
504
    }
76
    ReadonlyBytes bytes;
77
    FixedMemoryStream stream;
78
    TGAHeader header {};
79
    RefPtr<Gfx::Bitmap> bitmap;
80
    OwnPtr<TGAImageIdentifier> identifier_metadata;
81
};
82
83
TGAImageDecoderPlugin::TGAImageDecoderPlugin(NonnullOwnPtr<TGALoadingContext> context)
84
504
    : m_context(move(context))
85
504
{
86
504
}
87
88
504
TGAImageDecoderPlugin::~TGAImageDecoderPlugin() = default;
89
90
IntSize TGAImageDecoderPlugin::size()
91
0
{
92
0
    return IntSize { m_context->header.width, m_context->header.height };
93
0
}
94
95
static ErrorOr<void> ensure_header_validity(TGAHeader const& header, size_t whole_image_stream_size)
96
499
{
97
499
    if ((header.bits_per_pixel % 8) != 0 || header.bits_per_pixel < 8 || header.bits_per_pixel > 32)
98
128
        return Error::from_string_literal("Invalid bit depth");
99
371
    auto bytes_remaining = whole_image_stream_size - sizeof(TGAHeader) - header.id_length;
100
371
    if (header.data_type_code == TGADataType::UncompressedRGB && bytes_remaining < static_cast<u64>(header.width) * header.height * (header.bits_per_pixel / 8))
101
36
        return Error::from_string_literal("Not enough data to read an image with the expected size");
102
335
    return {};
103
371
}
104
105
ErrorOr<void> TGAImageDecoderPlugin::decode_tga_header()
106
504
{
107
504
    m_context->header = TRY(m_context->stream.read_value<TGAHeader>());
108
499
    if (m_context->header.id_length > 0) {
109
421
        auto identifier = String::from_stream(m_context->stream, m_context->header.id_length);
110
421
        if (!identifier.is_error())
111
88
            m_context->identifier_metadata = make<TGAImageIdentifier>(identifier.release_value());
112
333
        else
113
333
            dbgln("FIXME: Handle non-utf8 identifiers in TGALoader");
114
421
    }
115
499
    TRY(ensure_header_validity(m_context->header, m_context->bytes.size()));
116
335
    return {};
117
499
}
118
119
bool TGAImageDecoderPlugin::validate_before_create(ReadonlyBytes data)
120
0
{
121
0
    FixedMemoryStream stream { data };
122
0
    auto header_or_err = stream.read_value<Gfx::TGAHeader>();
123
0
    if (header_or_err.is_error())
124
0
        return false;
125
0
    return !ensure_header_validity(header_or_err.release_value(), data.size()).is_error();
126
0
}
127
128
ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> TGAImageDecoderPlugin::create(ReadonlyBytes data)
129
504
{
130
504
    FixedMemoryStream stream { data };
131
504
    auto context = TRY(adopt_nonnull_own_or_enomem(new (nothrow) TGALoadingContext(data, move(stream))));
132
504
    auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) TGAImageDecoderPlugin(move(context))));
133
504
    TRY(plugin->decode_tga_header());
134
335
    return plugin;
135
504
}
136
137
static ErrorOr<ARGB32> read_pixel_from_stream(Stream& stream, size_t bytes_size)
138
3.90M
{
139
    // NOTE: We support 8-bit, 24-bit and 32-bit color pixels
140
3.90M
    VERIFY(bytes_size == 1 || bytes_size == 3 || bytes_size == 4);
141
142
3.90M
    if (bytes_size == 1) {
143
3.17M
        auto raw = TRY(stream.read_value<u8>());
144
3.17M
        return Color(raw, raw, raw).value();
145
3.17M
    }
146
729k
    if (bytes_size == 3) {
147
729k
        Array<u8, 3> raw;
148
729k
        TRY(stream.read_until_filled(raw.span()));
149
729k
        return Color(raw[2], raw[1], raw[0]).value();
150
729k
    }
151
218
    return stream.read_value<ARGB32>();
152
729k
}
153
154
struct TGAPixelPacketHeader {
155
    bool raw { false };
156
    u8 pixels_count { 0 };
157
};
158
159
static ErrorOr<TGAPixelPacketHeader> read_pixel_packet_header(Stream& stream)
160
1.82M
{
161
1.82M
    auto const pixel_packet_header = TRY(stream.read_value<u8>());
162
1.82M
    bool pixels_raw_in_packet = !(pixel_packet_header & 0x80);
163
1.82M
    u8 pixels_count_in_packet = (pixel_packet_header & 0x7f);
164
    // NOTE: Run-length-encoded/Raw pixel packets cannot encode zero pixels,
165
    // so value 0 stands for 1 pixel, 1 stands for 2, etc...
166
1.82M
    pixels_count_in_packet++;
167
1.82M
    VERIFY(pixels_count_in_packet > 0);
168
1.82M
    return TGAPixelPacketHeader { pixels_raw_in_packet, pixels_count_in_packet };
169
1.82M
}
170
171
Optional<Metadata const&> TGAImageDecoderPlugin::metadata()
172
0
{
173
0
    if (m_context->identifier_metadata)
174
0
        return *m_context->identifier_metadata;
175
0
    return OptionalNone {};
176
0
}
177
178
ErrorOr<ImageFrameDescriptor> TGAImageDecoderPlugin::frame(size_t index, Optional<IntSize>)
179
335
{
180
335
    auto bits_per_pixel = m_context->header.bits_per_pixel;
181
335
    auto color_map = m_context->header.color_map_type;
182
335
    auto data_type = m_context->header.data_type_code;
183
335
    auto width = m_context->header.width;
184
335
    auto height = m_context->header.height;
185
335
    auto image_descriptior = m_context->header.image_descriptor;
186
187
335
    if (index != 0)
188
0
        return Error::from_string_literal("TGAImageDecoderPlugin: frame index must be 0");
189
190
335
    if (color_map > 1)
191
40
        return Error::from_string_literal("TGAImageDecoderPlugin: Invalid color map type");
192
193
295
    if (m_context->bitmap)
194
0
        return ImageFrameDescriptor { m_context->bitmap, 0 };
195
196
295
    RefPtr<Gfx::Bitmap> bitmap;
197
295
    switch (bits_per_pixel) {
198
200
    case 8:
199
243
    case 24:
200
243
        bitmap = TRY(Bitmap::create(BitmapFormat::BGRx8888, { m_context->header.width, m_context->header.height }));
201
227
        break;
202
203
51
    case 32:
204
51
        bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, { m_context->header.width, m_context->header.height }));
205
40
        break;
206
207
1
    default:
208
        // FIXME: Implement other TGA bit depths
209
1
        return Error::from_string_literal("TGAImageDecoderPlugin: Can only handle 8, 24 and 32 bits per pixel");
210
295
    }
211
212
267
    auto is_top_to_bottom = (image_descriptior & 1 << 5) == 0;
213
267
    auto is_left_to_right = (image_descriptior & 1 << 4) == 0;
214
215
267
    VERIFY((bits_per_pixel % 8) == 0);
216
267
    auto bytes_per_pixel = bits_per_pixel / 8;
217
218
267
    switch (data_type) {
219
41
    case TGADataType::UncompressedBlackAndWhite:
220
100
    case TGADataType::UncompressedRGB: {
221
891
        for (int row = 0; row < height; ++row) {
222
8.10k
            for (int col = 0; col < width; ++col) {
223
7.31k
                auto actual_row = row;
224
7.31k
                if (is_top_to_bottom)
225
972
                    actual_row = height - 1 - row;
226
7.31k
                auto actual_col = col;
227
7.31k
                if (!is_left_to_right)
228
6.30k
                    actual_col = width - 1 - col;
229
7.31k
                bitmap->scanline(actual_row)[actual_col] = TRY(read_pixel_from_stream(m_context->stream, bytes_per_pixel));
230
7.22k
            }
231
888
        }
232
100
        break;
233
100
    }
234
235
148
    case TGADataType::RunLengthEncodedRGB: {
236
148
        size_t pixel_index = 0;
237
148
        size_t pixel_count = height * width;
238
1.82M
        while (pixel_index < pixel_count) {
239
1.82M
            auto pixel_packet_header = TRY(read_pixel_packet_header(m_context->stream));
240
1.82M
            VERIFY(pixel_packet_header.pixels_count > 0);
241
242
1.82M
            auto pixel = TRY(read_pixel_from_stream(m_context->stream, bytes_per_pixel));
243
1.82M
            auto max_pixel_index = min(pixel_index + pixel_packet_header.pixels_count, pixel_count);
244
206M
            for (size_t current_pixel_index = pixel_index; current_pixel_index < max_pixel_index; ++current_pixel_index) {
245
204M
                int row = current_pixel_index / width;
246
204M
                int col = current_pixel_index % width;
247
204M
                auto actual_row = row;
248
204M
                if (is_top_to_bottom)
249
12.6M
                    actual_row = height - 1 - row;
250
204M
                auto actual_col = col;
251
204M
                if (!is_left_to_right)
252
192M
                    actual_col = width - 1 - col;
253
204M
                bitmap->scanline(actual_row)[actual_col] = pixel;
254
204M
                if (pixel_packet_header.raw && (current_pixel_index + 1) < max_pixel_index)
255
2.07M
                    pixel = TRY(read_pixel_from_stream(m_context->stream, bytes_per_pixel));
256
204M
            }
257
1.82M
            pixel_index += pixel_packet_header.pixels_count;
258
1.82M
        }
259
9
        break;
260
148
    }
261
19
    default:
262
        // FIXME: Implement other TGA data types
263
19
        return Error::from_string_literal("TGAImageDecoderPlugin: Can currently only handle the UncompressedRGB, CompressedRGB or UncompressedBlackAndWhite data type");
264
267
    }
265
266
12
    m_context->bitmap = bitmap;
267
12
    return ImageFrameDescriptor { m_context->bitmap, 0 };
268
267
}
269
270
}