Coverage Report

Created: 2026-02-16 07:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibGfx/ImageFormats/PNGWriter.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2021, Pierre Hoffmeister
3
 * Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
4
 * Copyright (c) 2021, Aziz Berkay Yesilyurt <abyesilyurt@gmail.com>
5
 * Copyright (c) 2024, Torben Jonas Virtmann
6
 *
7
 * SPDX-License-Identifier: BSD-2-Clause
8
 */
9
10
#include <AK/Concepts.h>
11
#include <AK/FixedArray.h>
12
#include <AK/MemoryStream.h>
13
#include <AK/SIMDExtras.h>
14
#include <AK/String.h>
15
#include <LibCompress/Zlib.h>
16
#include <LibCrypto/Checksum/CRC32.h>
17
#include <LibGfx/Bitmap.h>
18
#include <LibGfx/ImageFormats/PNGWriter.h>
19
20
namespace Gfx {
21
22
class PNGChunk {
23
    using data_length_type = u32;
24
25
public:
26
    explicit PNGChunk(String);
27
0
    auto const& data() const { return m_data; }
28
0
    String const& type() const { return m_type; }
29
0
    ErrorOr<void> reserve(size_t bytes) { return m_data.try_ensure_capacity(bytes); }
30
31
    template<typename T>
32
    ErrorOr<void> add_as_big_endian(T);
33
34
    ErrorOr<void> add_u8(u8);
35
36
    ErrorOr<void> compress_and_add(ReadonlyBytes, Compress::ZlibCompressionLevel);
37
    ErrorOr<void> add(ReadonlyBytes);
38
39
    ErrorOr<void> store_type();
40
    void store_data_length();
41
    u32 crc();
42
43
private:
44
    ByteBuffer m_data;
45
    String m_type;
46
};
47
48
PNGChunk::PNGChunk(String type)
49
0
    : m_type(move(type))
50
0
{
51
0
    VERIFY(m_type.bytes().size() == 4);
52
53
    // NOTE: These are MUST() because they should always be able to fit in m_data's inline capacity.
54
0
    MUST(add_as_big_endian<data_length_type>(0));
55
0
    MUST(store_type());
56
0
}
57
58
ErrorOr<void> PNGChunk::store_type()
59
0
{
60
0
    TRY(add(type().bytes()));
61
0
    return {};
62
0
}
63
64
void PNGChunk::store_data_length()
65
0
{
66
0
    auto data_length = BigEndian<u32>(m_data.size() - sizeof(data_length_type) - m_type.bytes().size());
67
0
    __builtin_memcpy(m_data.offset_pointer(0), &data_length, sizeof(u32));
68
0
}
69
70
u32 PNGChunk::crc()
71
0
{
72
0
    u32 crc = Crypto::Checksum::CRC32({ m_data.offset_pointer(sizeof(data_length_type)), m_data.size() - sizeof(data_length_type) }).digest();
73
0
    return crc;
74
0
}
75
76
ErrorOr<void> PNGChunk::compress_and_add(ReadonlyBytes uncompressed_bytes, Compress::ZlibCompressionLevel compression_level)
77
0
{
78
0
    return add(TRY(Compress::ZlibCompressor::compress_all(uncompressed_bytes, compression_level)));
79
0
}
80
81
ErrorOr<void> PNGChunk::add(ReadonlyBytes bytes)
82
0
{
83
0
    TRY(m_data.try_append(bytes));
84
0
    return {};
85
0
}
86
87
template<typename T>
88
ErrorOr<void> PNGChunk::add_as_big_endian(T data)
89
0
{
90
0
    auto data_out = AK::convert_between_host_and_big_endian(data);
91
0
    TRY(m_data.try_append(&data_out, sizeof(T)));
92
0
    return {};
93
0
}
Unexecuted instantiation: AK::ErrorOr<void, AK::Error> Gfx::PNGChunk::add_as_big_endian<unsigned int>(unsigned int)
Unexecuted instantiation: AK::ErrorOr<void, AK::Error> Gfx::PNGChunk::add_as_big_endian<unsigned short>(unsigned short)
94
95
ErrorOr<void> PNGChunk::add_u8(u8 data)
96
0
{
97
0
    TRY(m_data.try_append(data));
98
0
    return {};
99
0
}
100
101
PNGWriter::PNGWriter(Stream& stream)
102
0
    : m_stream(stream)
103
0
{
104
0
}
105
106
ErrorOr<void> PNGWriter::add_chunk(PNGChunk& png_chunk)
107
0
{
108
0
    png_chunk.store_data_length();
109
0
    u32 crc = png_chunk.crc();
110
0
    TRY(png_chunk.add_as_big_endian(crc));
111
0
    TRY(m_stream.write_until_depleted(png_chunk.data()));
112
0
    return {};
113
0
}
114
115
ErrorOr<void> PNGWriter::add_png_header()
116
0
{
117
0
    TRY(m_stream.write_until_depleted(PNG::header));
118
0
    return {};
119
0
}
120
121
ErrorOr<void> PNGWriter::add_acTL_chunk(u32 num_frames, u32 loop_count)
122
0
{
123
    // https://www.w3.org/TR/png/#acTL-chunk
124
0
    PNGChunk png_chunk { "acTL"_string };
125
0
    TRY(png_chunk.add_as_big_endian(num_frames));
126
0
    TRY(png_chunk.add_as_big_endian(loop_count));
127
0
    TRY(add_chunk(png_chunk));
128
0
    return {};
129
0
}
130
131
struct fcTLData {
132
    u32 sequence_number { 0 };
133
    u32 width { 0 };
134
    u32 height { 0 };
135
    u32 x_offset { 0 };
136
    u32 y_offset { 0 };
137
    u16 delay_numerator { 0 };
138
    u16 delay_denominator { 1 };
139
    // dispose_op values
140
    // 0           APNG_DISPOSE_OP_NONE
141
    // 1           APNG_DISPOSE_OP_BACKGROUND
142
    // 2           APNG_DISPOSE_OP_PREVIOUS
143
    u8 dispose_operation { 0 };
144
    // blend_op values
145
    // value
146
    // 0       APNG_BLEND_OP_SOURCE
147
    // 1       APNG_BLEND_OP_OVER
148
    u8 blend_operation { 0 };
149
};
150
151
ErrorOr<void> PNGWriter::add_fcTL_chunk(fcTLData const& data)
152
0
{
153
    // https://www.w3.org/TR/png/#fcTL-chunk
154
155
    // TODO: Constraints on frame regions
156
0
    PNGChunk png_chunk { "fcTL"_string };
157
0
    TRY(png_chunk.add_as_big_endian(data.sequence_number));
158
0
    TRY(png_chunk.add_as_big_endian(data.width));
159
0
    TRY(png_chunk.add_as_big_endian(data.height));
160
0
    TRY(png_chunk.add_as_big_endian(data.x_offset));
161
0
    TRY(png_chunk.add_as_big_endian(data.y_offset));
162
0
    TRY(png_chunk.add_as_big_endian(data.delay_numerator));
163
0
    TRY(png_chunk.add_as_big_endian(data.delay_denominator));
164
0
    TRY(png_chunk.add_u8(data.dispose_operation));
165
0
    TRY(png_chunk.add_u8(data.blend_operation));
166
0
    TRY(add_chunk(png_chunk));
167
0
    return {};
168
0
}
169
170
ErrorOr<void> PNGWriter::add_IHDR_chunk(u32 width, u32 height, u8 bit_depth, PNG::ColorType color_type, u8 compression_method, u8 filter_method, u8 interlace_method)
171
0
{
172
0
    PNGChunk png_chunk { "IHDR"_string };
173
0
    TRY(png_chunk.add_as_big_endian(width));
174
0
    TRY(png_chunk.add_as_big_endian(height));
175
0
    TRY(png_chunk.add_u8(bit_depth));
176
0
    TRY(png_chunk.add_u8(to_underlying(color_type)));
177
0
    TRY(png_chunk.add_u8(compression_method));
178
0
    TRY(png_chunk.add_u8(filter_method));
179
0
    TRY(png_chunk.add_u8(interlace_method));
180
0
    TRY(add_chunk(png_chunk));
181
0
    return {};
182
0
}
183
184
ErrorOr<void> PNGWriter::add_iCCP_chunk(ReadonlyBytes icc_data, Compress::ZlibCompressionLevel compression_level)
185
0
{
186
    // https://www.w3.org/TR/png/#11iCCP
187
0
    PNGChunk chunk { "iCCP"_string };
188
189
0
    TRY(chunk.add("embedded profile"sv.bytes()));
190
0
    TRY(chunk.add_u8(0)); // \0-terminate profile name
191
192
0
    TRY(chunk.add_u8(0)); // compression method deflate
193
0
    TRY(chunk.compress_and_add(icc_data, compression_level));
194
195
0
    TRY(add_chunk(chunk));
196
0
    return {};
197
0
}
198
199
ErrorOr<void> PNGWriter::add_IEND_chunk()
200
0
{
201
0
    PNGChunk png_chunk { "IEND"_string };
202
0
    TRY(add_chunk(png_chunk));
203
0
    return {};
204
0
}
205
206
union [[gnu::packed]] Pixel {
207
    ARGB32 rgba { 0 };
208
    AK::SIMD::u8x4 simd;
209
210
    ALWAYS_INLINE static AK::SIMD::u8x4 argb32_to_simd(Pixel pixel)
211
0
    {
212
0
        return pixel.simd;
213
0
    }
214
};
215
static_assert(AssertSize<Pixel, 4>());
216
217
template<bool include_alpha, bool include_colors>
218
static ErrorOr<void> add_image_data_to_chunk_impl(Gfx::Bitmap const& bitmap, PNGChunk& png_chunk, Compress::ZlibCompressionLevel compression_level)
219
0
{
220
0
    ByteBuffer uncompressed_block_data;
221
0
    TRY(uncompressed_block_data.try_ensure_capacity(bitmap.size_in_bytes() + bitmap.height()));
222
223
0
    auto dummy_scanline = TRY(FixedArray<Pixel>::create(bitmap.width()));
224
0
    auto const* scanline_minus_1 = dummy_scanline.data();
225
226
0
    for (int y = 0; y < bitmap.height(); ++y) {
227
0
        auto* scanline = reinterpret_cast<Pixel const*>(bitmap.scanline(y));
228
229
0
        struct Filter {
230
0
            PNG::FilterType type;
231
0
            AK::SIMD::u32x4 sum { 0, 0, 0, 0 };
232
233
0
            AK::SIMD::u8x4 predict(AK::SIMD::u8x4 pixel, AK::SIMD::u8x4 pixel_x_minus_1, AK::SIMD::u8x4 pixel_y_minus_1, AK::SIMD::u8x4 pixel_xy_minus_1)
234
0
            {
235
0
                switch (type) {
236
0
                case PNG::FilterType::None:
237
0
                    return pixel;
238
0
                case PNG::FilterType::Sub:
239
0
                    return pixel - pixel_x_minus_1;
240
0
                case PNG::FilterType::Up:
241
0
                    return pixel - pixel_y_minus_1;
242
0
                case PNG::FilterType::Average: {
243
                    // The sum Orig(a) + Orig(b) shall be performed without overflow (using at least nine-bit arithmetic).
244
0
                    auto sum = AK::SIMD::simd_cast<AK::SIMD::u16x4>(pixel_x_minus_1) + AK::SIMD::simd_cast<AK::SIMD::u16x4>(pixel_y_minus_1);
245
0
                    auto average = AK::SIMD::simd_cast<AK::SIMD::u8x4>(sum / 2);
246
0
                    return pixel - average;
247
0
                }
248
0
                case PNG::FilterType::Paeth:
249
0
                    return pixel - PNG::paeth_predictor(pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1);
250
0
                }
251
0
                VERIFY_NOT_REACHED();
252
0
            }
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<false, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::predict(unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4))
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<false, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::predict(unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4))
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<true, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::predict(unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4))
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<true, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::predict(unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4), unsigned char __vector(4))
253
254
0
            void append(AK::SIMD::u8x4 simd)
255
0
            {
256
0
                using namespace AK::SIMD;
257
0
                sum += simd_cast<u32x4>(abs(simd_cast<i32x4>(simd_cast<i8x4>(simd))));
258
0
            }
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<false, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::append(unsigned char __vector(4))
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<false, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::append(unsigned char __vector(4))
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<true, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::append(unsigned char __vector(4))
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<true, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::append(unsigned char __vector(4))
259
260
0
            u32 sum_of_abs_values() const
261
0
            {
262
0
                u32 result = sum[0];
263
                if constexpr (include_colors)
264
0
                    result += sum[1] + sum[2];
265
                if constexpr (include_alpha)
266
0
                    result += sum[3];
267
0
                return result;
268
0
            }
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<false, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::sum_of_abs_values() const
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<false, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::sum_of_abs_values() const
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<true, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::sum_of_abs_values() const
Unexecuted instantiation: PNGWriter.cpp:Gfx::add_image_data_to_chunk_impl<true, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)::Filter::sum_of_abs_values() const
269
0
        };
270
271
0
        Filter none_filter { .type = PNG::FilterType::None };
272
0
        Filter sub_filter { .type = PNG::FilterType::Sub };
273
0
        Filter up_filter { .type = PNG::FilterType::Up };
274
0
        Filter average_filter { .type = PNG::FilterType::Average };
275
0
        Filter paeth_filter { .type = PNG::FilterType::Paeth };
276
277
0
        auto pixel_x_minus_1 = Pixel::argb32_to_simd(dummy_scanline[0]);
278
0
        auto pixel_xy_minus_1 = Pixel::argb32_to_simd(dummy_scanline[0]);
279
280
0
        for (int x = 0; x < bitmap.width(); ++x) {
281
0
            auto pixel = Pixel::argb32_to_simd(scanline[x]);
282
0
            auto pixel_y_minus_1 = Pixel::argb32_to_simd(scanline_minus_1[x]);
283
284
0
            none_filter.append(none_filter.predict(pixel, pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1));
285
0
            sub_filter.append(sub_filter.predict(pixel, pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1));
286
0
            up_filter.append(up_filter.predict(pixel, pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1));
287
0
            average_filter.append(average_filter.predict(pixel, pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1));
288
0
            paeth_filter.append(paeth_filter.predict(pixel, pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1));
289
290
0
            pixel_x_minus_1 = pixel;
291
0
            pixel_xy_minus_1 = pixel_y_minus_1;
292
0
        }
293
294
        // 12.8 Filter selection: https://www.w3.org/TR/PNG/#12Filter-selection
295
        // For best compression of truecolour and greyscale images, the recommended approach
296
        // is adaptive filtering in which a filter is chosen for each scanline.
297
        // The following simple heuristic has performed well in early tests:
298
        // compute the output scanline using all five filters, and select the filter that gives the smallest sum of absolute values of outputs.
299
        // (Consider the output bytes as signed differences for this test.)
300
0
        Filter& best_filter = none_filter;
301
0
        if (best_filter.sum_of_abs_values() > sub_filter.sum_of_abs_values())
302
0
            best_filter = sub_filter;
303
0
        if (best_filter.sum_of_abs_values() > up_filter.sum_of_abs_values())
304
0
            best_filter = up_filter;
305
0
        if (best_filter.sum_of_abs_values() > average_filter.sum_of_abs_values())
306
0
            best_filter = average_filter;
307
0
        if (best_filter.sum_of_abs_values() > paeth_filter.sum_of_abs_values())
308
0
            best_filter = paeth_filter;
309
310
0
        TRY(uncompressed_block_data.try_append(to_underlying(best_filter.type)));
311
312
0
        pixel_x_minus_1 = Pixel::argb32_to_simd(dummy_scanline[0]);
313
0
        pixel_xy_minus_1 = Pixel::argb32_to_simd(dummy_scanline[0]);
314
315
0
        for (int x = 0; x < bitmap.width(); ++x) {
316
0
            auto pixel = Pixel::argb32_to_simd(scanline[x]);
317
0
            auto pixel_y_minus_1 = Pixel::argb32_to_simd(scanline_minus_1[x]);
318
319
0
            auto predicted_pixel = best_filter.predict(pixel, pixel_x_minus_1, pixel_y_minus_1, pixel_xy_minus_1);
320
0
            if constexpr (include_colors) {
321
0
                TRY(uncompressed_block_data.try_append(predicted_pixel[2]));
322
0
                TRY(uncompressed_block_data.try_append(predicted_pixel[1]));
323
0
            }
324
0
            TRY(uncompressed_block_data.try_append(predicted_pixel[0]));
325
            if constexpr (include_alpha)
326
0
                TRY(uncompressed_block_data.try_append(predicted_pixel[3]));
327
328
0
            pixel_x_minus_1 = pixel;
329
0
            pixel_xy_minus_1 = pixel_y_minus_1;
330
0
        }
331
332
0
        scanline_minus_1 = scanline;
333
0
    }
334
335
0
    return png_chunk.compress_and_add(uncompressed_block_data, compression_level);
336
0
}
Unexecuted instantiation: PNGWriter.cpp:AK::ErrorOr<void, AK::Error> Gfx::add_image_data_to_chunk_impl<false, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)
Unexecuted instantiation: PNGWriter.cpp:AK::ErrorOr<void, AK::Error> Gfx::add_image_data_to_chunk_impl<false, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)
Unexecuted instantiation: PNGWriter.cpp:AK::ErrorOr<void, AK::Error> Gfx::add_image_data_to_chunk_impl<true, false>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)
Unexecuted instantiation: PNGWriter.cpp:AK::ErrorOr<void, AK::Error> Gfx::add_image_data_to_chunk_impl<true, true>(Gfx::Bitmap const&, Gfx::PNGChunk&, Compress::ZlibCompressionLevel)
337
338
static ErrorOr<void> add_image_data_to_chunk(Gfx::Bitmap const& bitmap, PNG::ColorType color_type, PNGChunk& png_chunk, Compress::ZlibCompressionLevel compression_level)
339
0
{
340
0
    switch (color_type) {
341
0
    case PNG::ColorType::Greyscale:
342
0
        return add_image_data_to_chunk_impl<false, false>(bitmap, png_chunk, compression_level);
343
0
    case PNG::ColorType::Truecolor:
344
0
        return add_image_data_to_chunk_impl<false, true>(bitmap, png_chunk, compression_level);
345
0
    case PNG::ColorType::IndexedColor:
346
0
        VERIFY_NOT_REACHED();
347
0
    case PNG::ColorType::GreyscaleWithAlpha:
348
0
        return add_image_data_to_chunk_impl<true, false>(bitmap, png_chunk, compression_level);
349
0
    case PNG::ColorType::TruecolorWithAlpha:
350
0
        return add_image_data_to_chunk_impl<true, true>(bitmap, png_chunk, compression_level);
351
0
    }
352
0
    VERIFY_NOT_REACHED();
353
0
}
354
355
ErrorOr<void> PNGWriter::add_fdAT_chunk(Gfx::Bitmap const& bitmap, PNG::ColorType color_type, u32 sequence_number, Compress::ZlibCompressionLevel compression_level)
356
0
{
357
    // https://www.w3.org/TR/png/#fdAT-chunk
358
0
    PNGChunk png_chunk { "fdAT"_string };
359
0
    TRY(png_chunk.reserve(bitmap.size_in_bytes() + 4));
360
0
    TRY(png_chunk.add_as_big_endian(sequence_number));
361
0
    TRY(add_image_data_to_chunk(bitmap, color_type, png_chunk, compression_level));
362
0
    return add_chunk(png_chunk);
363
0
}
364
365
ErrorOr<void> PNGWriter::add_IDAT_chunk(Gfx::Bitmap const& bitmap, PNG::ColorType color_type, Compress::ZlibCompressionLevel compression_level)
366
0
{
367
0
    PNGChunk png_chunk { "IDAT"_string };
368
0
    TRY(png_chunk.reserve(bitmap.size_in_bytes()));
369
0
    TRY(add_image_data_to_chunk(bitmap, color_type, png_chunk, compression_level));
370
0
    return add_chunk(png_chunk);
371
0
}
372
373
static bool bitmap_has_transparency(Bitmap const& bitmap)
374
0
{
375
0
    for (auto pixel : bitmap) {
376
0
        if (Color::from_argb(pixel).alpha() != 255)
377
0
            return true;
378
0
    }
379
0
    return false;
380
0
}
381
382
static bool bitmap_has_color(Bitmap const& bitmap)
383
0
{
384
0
    for (auto pixel : bitmap) {
385
0
        auto color = Color::from_argb(pixel);
386
0
        if (color.red() != color.green() || color.green() != color.blue())
387
0
            return true;
388
0
    }
389
0
    return false;
390
0
}
391
392
static PNG::ColorType find_color_type(Bitmap const& bitmap, bool force_alpha)
393
0
{
394
0
    bool has_alpha = force_alpha || bitmap_has_transparency(bitmap);
395
0
    if (bitmap_has_color(bitmap)) {
396
0
        if (has_alpha)
397
0
            return PNG::ColorType::TruecolorWithAlpha;
398
0
        return PNG::ColorType::Truecolor;
399
0
    }
400
0
    if (has_alpha)
401
0
        return PNG::ColorType::GreyscaleWithAlpha;
402
0
    return PNG::ColorType::Greyscale;
403
0
}
404
405
ErrorOr<void> PNGWriter::encode(Stream& stream, Bitmap const& bitmap, Options const& options)
406
0
{
407
0
    PNGWriter writer { stream };
408
0
    TRY(writer.add_png_header());
409
0
    auto color_type = find_color_type(bitmap, options.force_alpha);
410
0
    TRY(writer.add_IHDR_chunk(bitmap.width(), bitmap.height(), 8, color_type, 0, 0, 0));
411
0
    if (options.icc_data.has_value())
412
0
        TRY(writer.add_iCCP_chunk(options.icc_data.value(), options.compression_level));
413
0
    TRY(writer.add_IDAT_chunk(bitmap, color_type, options.compression_level));
414
0
    TRY(writer.add_IEND_chunk());
415
0
    return {};
416
0
}
417
418
ErrorOr<ByteBuffer> PNGWriter::encode(Gfx::Bitmap const& bitmap, Options options)
419
0
{
420
0
    AllocatingMemoryStream stream;
421
0
    TRY(encode(stream, bitmap, options));
422
0
    return stream.read_until_eof();
423
0
}
424
425
class PNGAnimationWriter : public AnimationWriter {
426
public:
427
    PNGAnimationWriter(SeekableStream& stream, IntSize dimensions, int loop_count, PNGWriter::Options const& options)
428
0
        : m_writer(stream)
429
0
        , m_stream(stream)
430
0
        , m_dimensions(dimensions)
431
0
        , m_loop_count(loop_count)
432
0
        , m_options(options)
433
0
    {
434
0
    }
435
436
    virtual ErrorOr<void> add_frame(Bitmap&, int, IntPoint, BlendMode) override;
437
0
    virtual bool can_blend_frames() const override { return true; }
438
439
private:
440
    PNGWriter m_writer;
441
    SeekableStream& m_stream;
442
443
    IntSize const m_dimensions;
444
    int const m_loop_count { 0 };
445
446
    bool m_is_first_frame { true };
447
448
    u32 m_sequence_number { 0 };
449
    u32 m_number_of_frames { 0 };
450
    size_t m_acTL_offset { 0 };
451
    PNGWriter::Options const m_options;
452
};
453
454
ErrorOr<void> PNGAnimationWriter::add_frame(Bitmap& bitmap, int duration_ms, IntPoint at, BlendMode blend_mode)
455
0
{
456
0
    ++m_number_of_frames;
457
0
    bool const is_first_frame = m_number_of_frames == 1;
458
459
0
    if (is_first_frame) {
460
        // "The fcTL chunk corresponding to the default image, if it exists, has these restrictions:
461
        //  * The x_offset and y_offset fields must be 0.
462
        //  * The width and height fields must equal the corresponding fields from the IHDR chunk."
463
        // FIXME: If this ends up happening in practice, we should composite `bitmap` to a temporary bitmap and store that as first frame.
464
0
        if (at != IntPoint {})
465
0
            return Error::from_string_literal("First APNG frame must have x_offset and y_offset set to 0");
466
0
        if (bitmap.size() != m_dimensions)
467
0
            return Error::from_string_literal("First APNG frame must have the same dimensions as the APNG itself");
468
469
        // All frames in an APNG use the same IHDR chunk, which means they all have the same color type.
470
        // To decide if we should write RGB or RGBA, we'd really have to check all frames, but that needs a
471
        // lot of memory and makes streaming impossible.
472
        // Instead, we always include an alpha channel. In practice, inter-frame compression means that
473
        // even for animations without transparency, all but the first frame will have transparent pixels.
474
        // The APNG format doesn't give us super great options here.
475
0
        TRY(m_writer.add_png_header());
476
0
        TRY(m_writer.add_IHDR_chunk(m_dimensions.width(), m_dimensions.height(), 8, PNG::ColorType::TruecolorWithAlpha, 0, 0, 0));
477
0
        if (m_options.icc_data.has_value())
478
0
            TRY(m_writer.add_iCCP_chunk(m_options.icc_data.value(), m_options.compression_level));
479
0
        m_acTL_offset = TRY(m_stream.tell());
480
0
        TRY(m_writer.add_acTL_chunk(m_number_of_frames, m_loop_count));
481
0
    } else {
482
        // Overwrite previous acTL chunk to update its num_frames. Use add_acTL_chunk to make sure the chunk's crc is updated too.
483
0
        auto current_offset = TRY(m_stream.tell());
484
0
        TRY(m_stream.seek(m_acTL_offset, SeekMode::SetPosition));
485
0
        TRY(m_writer.add_acTL_chunk(m_number_of_frames, m_loop_count));
486
0
        TRY(m_stream.seek(current_offset, SeekMode::SetPosition));
487
488
        // Overwrite previous IEND marker.
489
0
        TRY(m_stream.seek(-12, SeekMode::FromCurrentPosition));
490
0
    }
491
492
0
    fcTLData fcTL_data;
493
0
    fcTL_data.sequence_number = m_sequence_number;
494
0
    fcTL_data.width = bitmap.width();
495
0
    fcTL_data.height = bitmap.height();
496
0
    fcTL_data.delay_numerator = duration_ms;
497
0
    fcTL_data.delay_denominator = 1000;
498
0
    fcTL_data.x_offset = at.x();
499
0
    fcTL_data.y_offset = at.y();
500
0
    if (blend_mode == BlendMode::Blend)
501
0
        fcTL_data.blend_operation = 1;
502
0
    TRY(m_writer.add_fcTL_chunk(fcTL_data));
503
0
    m_sequence_number++;
504
505
0
    if (is_first_frame) {
506
0
        TRY(m_writer.add_IDAT_chunk(bitmap, PNG::ColorType::TruecolorWithAlpha, m_options.compression_level));
507
0
    } else {
508
0
        TRY(m_writer.add_fdAT_chunk(bitmap, PNG::ColorType::TruecolorWithAlpha, m_sequence_number, m_options.compression_level));
509
0
        m_sequence_number++;
510
0
    }
511
512
0
    TRY(m_writer.add_IEND_chunk());
513
514
0
    return {};
515
0
}
516
517
ErrorOr<NonnullOwnPtr<AnimationWriter>> PNGWriter::start_encoding_animation(SeekableStream& stream, IntSize dimensions, int loop_count, Options const& options)
518
0
{
519
0
    auto writer = make<PNGAnimationWriter>(stream, dimensions, loop_count, options);
520
0
    return writer;
521
0
}
522
523
}