Coverage Report

Created: 2026-07-25 07:46

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
87
        : m_identifier(move(identifier))
50
87
    {
51
87
    }
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
551
        : bytes(bytes)
73
551
        , stream(move(stream))
74
551
    {
75
551
    }
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
551
    : m_context(move(context))
85
551
{
86
551
}
87
88
551
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
546
{
97
546
    if ((header.bits_per_pixel % 8) != 0 || header.bits_per_pixel < 8 || header.bits_per_pixel > 32)
98
135
        return Error::from_string_literal("Invalid bit depth");
99
411
    auto bytes_remaining = whole_image_stream_size - sizeof(TGAHeader) - header.id_length;
100
411
    if (header.data_type_code == TGADataType::UncompressedRGB && bytes_remaining < static_cast<u64>(header.width) * header.height * (header.bits_per_pixel / 8))
101
31
        return Error::from_string_literal("Not enough data to read an image with the expected size");
102
380
    return {};
103
411
}
104
105
ErrorOr<void> TGAImageDecoderPlugin::decode_tga_header()
106
551
{
107
551
    m_context->header = TRY(m_context->stream.read_value<TGAHeader>());
108
546
    if (m_context->header.id_length > 0) {
109
467
        auto identifier = String::from_stream(m_context->stream, m_context->header.id_length);
110
467
        if (!identifier.is_error())
111
87
            m_context->identifier_metadata = make<TGAImageIdentifier>(identifier.release_value());
112
380
        else
113
380
            dbgln("FIXME: Handle non-utf8 identifiers in TGALoader");
114
467
    }
115
546
    TRY(ensure_header_validity(m_context->header, m_context->bytes.size()));
116
380
    return {};
117
546
}
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
551
{
130
551
    FixedMemoryStream stream { data };
131
551
    auto context = TRY(adopt_nonnull_own_or_enomem(new (nothrow) TGALoadingContext(data, move(stream))));
132
551
    auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) TGAImageDecoderPlugin(move(context))));
133
551
    TRY(plugin->decode_tga_header());
134
380
    return plugin;
135
551
}
136
137
static ErrorOr<ARGB32> read_pixel_from_stream(Stream& stream, size_t bytes_size)
138
3.88M
{
139
    // NOTE: We support 8-bit, 24-bit and 32-bit color pixels
140
3.88M
    VERIFY(bytes_size == 1 || bytes_size == 3 || bytes_size == 4);
141
142
3.88M
    if (bytes_size == 1) {
143
3.20M
        auto raw = TRY(stream.read_value<u8>());
144
3.20M
        return Color(raw, raw, raw).value();
145
3.20M
    }
146
678k
    if (bytes_size == 3) {
147
678k
        Array<u8, 3> raw;
148
678k
        TRY(stream.read_until_filled(raw.span()));
149
678k
        return Color(raw[2], raw[1], raw[0]).value();
150
678k
    }
151
216
    return stream.read_value<ARGB32>();
152
678k
}
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.81M
{
161
1.81M
    auto const pixel_packet_header = TRY(stream.read_value<u8>());
162
1.81M
    bool pixels_raw_in_packet = !(pixel_packet_header & 0x80);
163
1.81M
    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.81M
    pixels_count_in_packet++;
167
1.81M
    VERIFY(pixels_count_in_packet > 0);
168
1.81M
    return TGAPixelPacketHeader { pixels_raw_in_packet, pixels_count_in_packet };
169
1.81M
}
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
380
{
180
380
    auto bits_per_pixel = m_context->header.bits_per_pixel;
181
380
    auto color_map = m_context->header.color_map_type;
182
380
    auto data_type = m_context->header.data_type_code;
183
380
    auto width = m_context->header.width;
184
380
    auto height = m_context->header.height;
185
380
    auto image_descriptior = m_context->header.image_descriptor;
186
187
380
    if (index != 0)
188
0
        return Error::from_string_literal("TGAImageDecoderPlugin: frame index must be 0");
189
190
380
    if (color_map > 1)
191
42
        return Error::from_string_literal("TGAImageDecoderPlugin: Invalid color map type");
192
193
338
    if (m_context->bitmap)
194
0
        return ImageFrameDescriptor { m_context->bitmap, 0 };
195
196
338
    RefPtr<Gfx::Bitmap> bitmap;
197
338
    switch (bits_per_pixel) {
198
241
    case 8:
199
288
    case 24:
200
288
        bitmap = TRY(Bitmap::create(BitmapFormat::BGRx8888, { m_context->header.width, m_context->header.height }));
201
265
        break;
202
203
46
    case 32:
204
46
        bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, { m_context->header.width, m_context->header.height }));
205
37
        break;
206
207
4
    default:
208
        // FIXME: Implement other TGA bit depths
209
4
        return Error::from_string_literal("TGAImageDecoderPlugin: Can only handle 8, 24 and 32 bits per pixel");
210
338
    }
211
212
302
    auto is_top_to_bottom = (image_descriptior & 1 << 5) == 0;
213
302
    auto is_left_to_right = (image_descriptior & 1 << 4) == 0;
214
215
302
    VERIFY((bits_per_pixel % 8) == 0);
216
302
    auto bytes_per_pixel = bits_per_pixel / 8;
217
218
302
    switch (data_type) {
219
35
    case TGADataType::UncompressedBlackAndWhite:
220
119
    case TGADataType::UncompressedRGB: {
221
645
        for (int row = 0; row < height; ++row) {
222
7.85k
            for (int col = 0; col < width; ++col) {
223
7.32k
                auto actual_row = row;
224
7.32k
                if (is_top_to_bottom)
225
1.21k
                    actual_row = height - 1 - row;
226
7.32k
                auto actual_col = col;
227
7.32k
                if (!is_left_to_right)
228
6.07k
                    actual_col = width - 1 - col;
229
7.32k
                bitmap->scanline(actual_row)[actual_col] = TRY(read_pixel_from_stream(m_context->stream, bytes_per_pixel));
230
7.20k
            }
231
642
        }
232
119
        break;
233
119
    }
234
235
166
    case TGADataType::RunLengthEncodedRGB: {
236
166
        size_t pixel_index = 0;
237
166
        size_t pixel_count = height * width;
238
1.81M
        while (pixel_index < pixel_count) {
239
1.81M
            auto pixel_packet_header = TRY(read_pixel_packet_header(m_context->stream));
240
1.81M
            VERIFY(pixel_packet_header.pixels_count > 0);
241
242
1.81M
            auto pixel = TRY(read_pixel_from_stream(m_context->stream, bytes_per_pixel));
243
1.81M
            auto max_pixel_index = min(pixel_index + pixel_packet_header.pixels_count, pixel_count);
244
205M
            for (size_t current_pixel_index = pixel_index; current_pixel_index < max_pixel_index; ++current_pixel_index) {
245
203M
                int row = current_pixel_index / width;
246
203M
                int col = current_pixel_index % width;
247
203M
                auto actual_row = row;
248
203M
                if (is_top_to_bottom)
249
13.0M
                    actual_row = height - 1 - row;
250
203M
                auto actual_col = col;
251
203M
                if (!is_left_to_right)
252
190M
                    actual_col = width - 1 - col;
253
203M
                bitmap->scanline(actual_row)[actual_col] = pixel;
254
203M
                if (pixel_packet_header.raw && (current_pixel_index + 1) < max_pixel_index)
255
2.06M
                    pixel = TRY(read_pixel_from_stream(m_context->stream, bytes_per_pixel));
256
203M
            }
257
1.81M
            pixel_index += pixel_packet_header.pixels_count;
258
1.81M
        }
259
7
        break;
260
166
    }
261
17
    default:
262
        // FIXME: Implement other TGA data types
263
17
        return Error::from_string_literal("TGAImageDecoderPlugin: Can currently only handle the UncompressedRGB, CompressedRGB or UncompressedBlackAndWhite data type");
264
302
    }
265
266
10
    m_context->bitmap = bitmap;
267
10
    return ImageFrameDescriptor { m_context->bitmap, 0 };
268
302
}
269
270
}