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