/src/serenity/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2023, Lucas Chollet <lucas.chollet@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include "JPEGWriter.h" |
8 | | #include "JPEGShared.h" |
9 | | #include "JPEGWriterTables.h" |
10 | | #include <AK/BitStream.h> |
11 | | #include <AK/Endian.h> |
12 | | #include <AK/Function.h> |
13 | | #include <LibGfx/Bitmap.h> |
14 | | #include <LibGfx/CMYKBitmap.h> |
15 | | |
16 | | namespace Gfx { |
17 | | |
18 | | namespace { |
19 | | |
20 | | enum Mode { |
21 | | RGB, |
22 | | CMYK, |
23 | | }; |
24 | | |
25 | | // This is basically a BigEndianOutputBitStream, the only difference |
26 | | // is that it appends 0x00 after each 0xFF when it writes bits. |
27 | | class JPEGBigEndianOutputBitStream : public Stream { |
28 | | public: |
29 | | explicit JPEGBigEndianOutputBitStream(Stream& stream) |
30 | 0 | : m_stream(stream) |
31 | 0 | { |
32 | 0 | } |
33 | | |
34 | | virtual ErrorOr<Bytes> read_some(Bytes) override |
35 | 0 | { |
36 | 0 | return Error::from_errno(EBADF); |
37 | 0 | } |
38 | | |
39 | | virtual ErrorOr<size_t> write_some(ReadonlyBytes bytes) override |
40 | 0 | { |
41 | 0 | VERIFY(m_bit_offset == 0); |
42 | 0 | return m_stream.write_some(bytes); |
43 | 0 | } |
44 | | |
45 | | template<Unsigned T> |
46 | | ErrorOr<void> write_bits(T value, size_t bit_count) |
47 | 0 | { |
48 | 0 | VERIFY(m_bit_offset <= 7); |
49 | | |
50 | 0 | while (bit_count > 0) { |
51 | 0 | u8 const next_bit = (value >> (bit_count - 1)) & 1; |
52 | 0 | bit_count--; |
53 | |
|
54 | 0 | m_current_byte <<= 1; |
55 | 0 | m_current_byte |= next_bit; |
56 | 0 | m_bit_offset++; |
57 | |
|
58 | 0 | if (m_bit_offset > 7) { |
59 | 0 | TRY(m_stream.write_value(m_current_byte)); |
60 | 0 | if (m_current_byte == 0xFF) |
61 | 0 | TRY(m_stream.write_value<u8>(0)); |
62 | | |
63 | 0 | m_bit_offset = 0; |
64 | 0 | m_current_byte = 0; |
65 | 0 | } |
66 | 0 | } |
67 | | |
68 | 0 | return {}; |
69 | 0 | } Unexecuted instantiation: JPEGWriter.cpp:_ZN3Gfx12_GLOBAL__N_128JPEGBigEndianOutputBitStream10write_bitsITkN2AK8Concepts8UnsignedEtEENS3_7ErrorOrIvNS3_5ErrorEEET_m Unexecuted instantiation: JPEGWriter.cpp:_ZN3Gfx12_GLOBAL__N_128JPEGBigEndianOutputBitStream10write_bitsITkN2AK8Concepts8UnsignedEhEENS3_7ErrorOrIvNS3_5ErrorEEET_m |
70 | | |
71 | | virtual bool is_eof() const override |
72 | 0 | { |
73 | 0 | return true; |
74 | 0 | } |
75 | | |
76 | | virtual bool is_open() const override |
77 | 0 | { |
78 | 0 | return m_stream.is_open(); |
79 | 0 | } |
80 | | |
81 | | virtual void close() override |
82 | 0 | { |
83 | 0 | } |
84 | | |
85 | | ErrorOr<void> align_to_byte_boundary(u8 filler = 0x0) |
86 | 0 | { |
87 | 0 | if (m_bit_offset == 0) |
88 | 0 | return {}; |
89 | | |
90 | 0 | TRY(write_bits(filler, 8 - m_bit_offset)); |
91 | 0 | VERIFY(m_bit_offset == 0); |
92 | 0 | return {}; |
93 | 0 | } |
94 | | |
95 | | private: |
96 | | Stream& m_stream; |
97 | | u8 m_current_byte { 0 }; |
98 | | size_t m_bit_offset { 0 }; |
99 | | }; |
100 | | |
101 | | class JPEGEncodingContext { |
102 | | public: |
103 | | JPEGEncodingContext(JPEGBigEndianOutputBitStream output_stream) |
104 | 0 | : m_bit_stream(move(output_stream)) |
105 | 0 | { |
106 | 0 | } |
107 | | |
108 | | ErrorOr<void> initialize_mcu(Bitmap const& bitmap) |
109 | 0 | { |
110 | 0 | u64 const horizontal_macroblocks = ceil_div(bitmap.width(), 8); |
111 | 0 | u64 const vertical_macroblocks = ceil_div(bitmap.height(), 8); |
112 | 0 | TRY(m_macroblocks.try_resize(horizontal_macroblocks * vertical_macroblocks)); |
113 | | |
114 | 0 | for (u16 y {}; y < bitmap.height(); ++y) { |
115 | 0 | u16 const vertical_macroblock_index = y / 8; |
116 | 0 | u16 const vertical_pixel_offset = y - vertical_macroblock_index * 8; |
117 | |
|
118 | 0 | for (u16 x {}; x < bitmap.width(); ++x) { |
119 | 0 | u16 const horizontal_macroblock_index = x / 8; |
120 | 0 | u16 const horizontal_pixel_offset = x - horizontal_macroblock_index * 8; |
121 | |
|
122 | 0 | auto& macroblock = m_macroblocks[vertical_macroblock_index * horizontal_macroblocks + horizontal_macroblock_index]; |
123 | 0 | auto const pixel_offset = vertical_pixel_offset * 8 + horizontal_pixel_offset; |
124 | |
|
125 | 0 | auto const original_pixel = bitmap.get_pixel(x, y); |
126 | | |
127 | | // Conversion from YCbCr to RGB isn't specified in the first JPEG specification but in the JFIF extension: |
128 | | // See: https://www.itu.int/rec/dologin_pub.asp?lang=f&id=T-REC-T.871-201105-I!!PDF-E&type=items |
129 | | // 7 - Conversion to and from RGB |
130 | 0 | auto const y_ = clamp(0.299 * original_pixel.red() + 0.587 * original_pixel.green() + 0.114 * original_pixel.blue(), 0, 255); |
131 | 0 | auto const cb = clamp(-0.1687 * original_pixel.red() - 0.3313 * original_pixel.green() + 0.5 * original_pixel.blue() + 128, 0, 255); |
132 | 0 | auto const cr = clamp(0.5 * original_pixel.red() - 0.4187 * original_pixel.green() - 0.0813 * original_pixel.blue() + 128, 0, 255); |
133 | | |
134 | | // A.3.1 - Level shift |
135 | 0 | macroblock.r[pixel_offset] = y_ - 128; |
136 | 0 | macroblock.g[pixel_offset] = cb - 128; |
137 | 0 | macroblock.b[pixel_offset] = cr - 128; |
138 | 0 | } |
139 | 0 | } |
140 | |
|
141 | 0 | return {}; |
142 | 0 | } |
143 | | |
144 | | ErrorOr<void> initialize_mcu(CMYKBitmap const& bitmap) |
145 | 0 | { |
146 | 0 | u64 const horizontal_macroblocks = ceil_div(bitmap.size().width(), 8); |
147 | 0 | u64 const vertical_macroblocks = ceil_div(bitmap.size().height(), 8); |
148 | 0 | TRY(m_macroblocks.try_resize(horizontal_macroblocks * vertical_macroblocks)); |
149 | | |
150 | 0 | for (u16 y {}; y < bitmap.size().height(); ++y) { |
151 | 0 | u16 const vertical_macroblock_index = y / 8; |
152 | 0 | u16 const vertical_pixel_offset = y - vertical_macroblock_index * 8; |
153 | |
|
154 | 0 | for (u16 x {}; x < bitmap.size().width(); ++x) { |
155 | 0 | u16 const horizontal_macroblock_index = x / 8; |
156 | 0 | u16 const horizontal_pixel_offset = x - horizontal_macroblock_index * 8; |
157 | |
|
158 | 0 | auto& macroblock = m_macroblocks[vertical_macroblock_index * horizontal_macroblocks + horizontal_macroblock_index]; |
159 | 0 | auto const pixel_offset = vertical_pixel_offset * 8 + horizontal_pixel_offset; |
160 | |
|
161 | 0 | auto const original_pixel = bitmap.scanline(y)[x]; |
162 | | |
163 | | // To get YCCK, the CMY part is converted to RGB (ignoring the K component), and then the RGB is converted to YCbCr. |
164 | | // r is `255 - c` (and similar for g/m b/y), but with the Adobe YCCK color transform marker, the CMY |
165 | | // channels are stored inverted, which cancels out: 255 - (255 - x) == x. |
166 | | // K is stored as-is (meaning it's inverted once for the color transform). |
167 | 0 | u8 r = original_pixel.c; |
168 | 0 | u8 g = original_pixel.m; |
169 | 0 | u8 b = original_pixel.y; |
170 | 0 | u8 k = 255 - original_pixel.k; |
171 | | |
172 | | // See: https://www.itu.int/rec/dologin_pub.asp?lang=f&id=T-REC-T.871-201105-I!!PDF-E&type=items |
173 | | // 7 - Conversion to and from RGB |
174 | 0 | auto const y_ = clamp(0.299 * r + 0.587 * g + 0.114 * b, 0, 255); |
175 | 0 | auto const cb = clamp(-0.1687 * r - 0.3313 * g + 0.5 * b + 128, 0, 255); |
176 | 0 | auto const cr = clamp(0.5 * r - 0.4187 * g - 0.0813 * b + 128, 0, 255); |
177 | | |
178 | | // A.3.1 - Level shift |
179 | 0 | macroblock.r[pixel_offset] = y_ - 128; |
180 | 0 | macroblock.g[pixel_offset] = cb - 128; |
181 | 0 | macroblock.b[pixel_offset] = cr - 128; |
182 | 0 | macroblock.k[pixel_offset] = k - 128; |
183 | 0 | } |
184 | 0 | } |
185 | |
|
186 | 0 | return {}; |
187 | 0 | } |
188 | | |
189 | | static Array<double, 64> create_cosine_lookup_table() |
190 | 0 | { |
191 | 0 | static constexpr double pi_over_16 = AK::Pi<double> / 16; |
192 | |
|
193 | 0 | Array<double, 64> table; |
194 | |
|
195 | 0 | for (u8 u = 0; u < 8; ++u) { |
196 | 0 | for (u8 x = 0; x < 8; ++x) |
197 | 0 | table[u * 8 + x] = cos((2 * x + 1) * u * pi_over_16); |
198 | 0 | } |
199 | |
|
200 | 0 | return table; |
201 | 0 | } |
202 | | |
203 | | void fdct_and_quantization(Mode mode) |
204 | 0 | { |
205 | 0 | static auto cosine_table = create_cosine_lookup_table(); |
206 | |
|
207 | 0 | for (auto& macroblock : m_macroblocks) { |
208 | 0 | constexpr double inverse_sqrt_2 = M_SQRT1_2; |
209 | |
|
210 | 0 | auto const convert_one_component = [&](i16 component[], QuantizationTable const& table) { |
211 | 0 | Array<i16, 64> result {}; |
212 | |
|
213 | 0 | auto const sum_xy = [&](u8 u, u8 v) { |
214 | 0 | double sum {}; |
215 | 0 | for (u8 y {}; y < 8; ++y) { |
216 | 0 | for (u8 x {}; x < 8; ++x) |
217 | 0 | sum += component[y * 8 + x] * cosine_table[u * 8 + x] * cosine_table[v * 8 + y]; |
218 | 0 | } |
219 | 0 | return sum; |
220 | 0 | }; |
221 | |
|
222 | 0 | for (u8 v {}; v < 8; ++v) { |
223 | 0 | double const cv = v == 0 ? inverse_sqrt_2 : 1; |
224 | 0 | for (u8 u {}; u < 8; ++u) { |
225 | 0 | auto const table_index = v * 8 + u; |
226 | |
|
227 | 0 | double const cu = u == 0 ? inverse_sqrt_2 : 1; |
228 | | |
229 | | // A.3.3 - FDCT and IDCT |
230 | 0 | double const fdct = cu * cv * sum_xy(u, v) / 4; |
231 | | |
232 | | // A.3.4 - DCT coefficient quantization |
233 | 0 | i16 const quantized = round(fdct / table.table[table_index]); |
234 | |
|
235 | 0 | result[table_index] = quantized; |
236 | 0 | } |
237 | 0 | } |
238 | |
|
239 | 0 | for (u8 i {}; i < result.size(); ++i) |
240 | 0 | component[i] = result[i]; |
241 | 0 | }; |
242 | |
|
243 | 0 | convert_one_component(macroblock.y, m_luminance_quantization_table); |
244 | 0 | convert_one_component(macroblock.cb, m_chrominance_quantization_table); |
245 | 0 | convert_one_component(macroblock.cr, m_chrominance_quantization_table); |
246 | 0 | if (mode == Mode::CMYK) |
247 | 0 | convert_one_component(macroblock.k, m_luminance_quantization_table); |
248 | 0 | } |
249 | 0 | } |
250 | | |
251 | | ErrorOr<void> write_huffman_stream(Mode mode) |
252 | 0 | { |
253 | 0 | for (auto& macroblock : m_macroblocks) { |
254 | 0 | TRY(encode_dc(dc_luminance_huffman_table, macroblock.y, 0)); |
255 | 0 | TRY(encode_ac(ac_luminance_huffman_table, macroblock.y)); |
256 | | |
257 | 0 | TRY(encode_dc(dc_chrominance_huffman_table, macroblock.cb, 1)); |
258 | 0 | TRY(encode_ac(ac_chrominance_huffman_table, macroblock.cb)); |
259 | | |
260 | 0 | TRY(encode_dc(dc_chrominance_huffman_table, macroblock.cr, 2)); |
261 | 0 | TRY(encode_ac(ac_chrominance_huffman_table, macroblock.cr)); |
262 | | |
263 | 0 | if (mode == Mode::CMYK) { |
264 | 0 | TRY(encode_dc(dc_luminance_huffman_table, macroblock.k, 3)); |
265 | 0 | TRY(encode_ac(ac_luminance_huffman_table, macroblock.k)); |
266 | 0 | } |
267 | 0 | } |
268 | | |
269 | 0 | TRY(m_bit_stream.align_to_byte_boundary(0xFF)); |
270 | | |
271 | 0 | return {}; |
272 | 0 | } |
273 | | |
274 | | void set_luminance_quantization_table(QuantizationTable const& table, int quality) |
275 | 0 | { |
276 | 0 | set_quantization_table(m_luminance_quantization_table, table, quality); |
277 | 0 | } |
278 | | |
279 | | void set_chrominance_quantization_table(QuantizationTable const& table, int quality) |
280 | 0 | { |
281 | 0 | set_quantization_table(m_chrominance_quantization_table, table, quality); |
282 | 0 | } |
283 | | |
284 | | QuantizationTable const& luminance_quantization_table() const |
285 | 0 | { |
286 | 0 | return m_luminance_quantization_table; |
287 | 0 | } |
288 | | |
289 | | QuantizationTable const& chrominance_quantization_table() const |
290 | 0 | { |
291 | 0 | return m_chrominance_quantization_table; |
292 | 0 | } |
293 | | |
294 | | OutputHuffmanTable dc_luminance_huffman_table; |
295 | | OutputHuffmanTable dc_chrominance_huffman_table; |
296 | | |
297 | | OutputHuffmanTable ac_luminance_huffman_table; |
298 | | OutputHuffmanTable ac_chrominance_huffman_table; |
299 | | |
300 | | private: |
301 | | static void set_quantization_table(QuantizationTable& destination, QuantizationTable const& source, int quality) |
302 | 0 | { |
303 | | // In order to be compatible with libjpeg-turbo, we use the same coefficients as them. |
304 | |
|
305 | 0 | quality = clamp(quality, 1, 100); |
306 | |
|
307 | 0 | if (quality < 50) |
308 | 0 | quality = 5000 / quality; |
309 | 0 | else |
310 | 0 | quality = 200 - quality * 2; |
311 | |
|
312 | 0 | destination = source; |
313 | 0 | for (u8 i {}; i < 64; ++i) { |
314 | 0 | auto const shifted_value = (destination.table[i] * quality + 50) / 100; |
315 | 0 | destination.table[i] = clamp(shifted_value, 1, 255); |
316 | 0 | } |
317 | 0 | } |
318 | | |
319 | | ErrorOr<void> write_symbol(OutputHuffmanTable::Symbol symbol) |
320 | 0 | { |
321 | 0 | return m_bit_stream.write_bits(symbol.word, symbol.code_length); |
322 | 0 | } |
323 | | |
324 | | ErrorOr<void> encode_dc(OutputHuffmanTable const& dc_table, i16 const component[], u8 component_id) |
325 | 0 | { |
326 | | // F.1.2.1.3 - Huffman encoding procedures for DC coefficients |
327 | 0 | auto diff = component[0] - m_last_dc_values[component_id]; |
328 | 0 | m_last_dc_values[component_id] = component[0]; |
329 | |
|
330 | 0 | auto const size = csize(diff); |
331 | 0 | TRY(write_symbol(dc_table.from_input_byte(size))); |
332 | | |
333 | 0 | if (diff < 0) |
334 | 0 | diff -= 1; |
335 | |
|
336 | 0 | TRY(m_bit_stream.write_bits<u16>(diff, size)); |
337 | 0 | return {}; |
338 | 0 | } |
339 | | |
340 | | ErrorOr<void> encode_ac(OutputHuffmanTable const& ac_table, i16 const component[]) |
341 | 0 | { |
342 | 0 | { |
343 | | // F.2 - Procedure for sequential encoding of AC coefficients with Huffman coding |
344 | 0 | u32 k {}; |
345 | 0 | u32 r {}; |
346 | |
|
347 | 0 | while (k < 63) { |
348 | 0 | k++; |
349 | |
|
350 | 0 | auto coefficient = component[zigzag_map[k]]; |
351 | 0 | if (coefficient == 0) { |
352 | 0 | if (k == 63) { |
353 | 0 | TRY(write_symbol(ac_table.from_input_byte(0x00))); |
354 | 0 | break; |
355 | 0 | } |
356 | 0 | r += 1; |
357 | 0 | continue; |
358 | 0 | } |
359 | | |
360 | 0 | while (r > 15) { |
361 | 0 | TRY(write_symbol(ac_table.from_input_byte(0xF0))); |
362 | 0 | r -= 16; |
363 | 0 | } |
364 | | |
365 | 0 | { |
366 | | // F.3 - Sequential encoding of a non-zero AC coefficient |
367 | 0 | auto const ssss = csize(coefficient); |
368 | 0 | auto const rs = (r << 4) + ssss; |
369 | 0 | TRY(write_symbol(ac_table.from_input_byte(rs))); |
370 | | |
371 | 0 | if (coefficient < 0) |
372 | 0 | coefficient -= 1; |
373 | |
|
374 | 0 | TRY(m_bit_stream.write_bits<u16>(coefficient, ssss)); |
375 | 0 | } |
376 | | |
377 | 0 | r = 0; |
378 | 0 | } |
379 | 0 | } |
380 | 0 | return {}; |
381 | 0 | } |
382 | | |
383 | | static u8 csize(i16 coefficient) |
384 | 0 | { |
385 | 0 | VERIFY(coefficient >= -2047 && coefficient <= 2047); |
386 | | |
387 | 0 | if (coefficient == 0) |
388 | 0 | return 0; |
389 | | |
390 | 0 | return floor(log2(abs(coefficient))) + 1; |
391 | 0 | } |
392 | | |
393 | | QuantizationTable m_luminance_quantization_table {}; |
394 | | QuantizationTable m_chrominance_quantization_table {}; |
395 | | |
396 | | Vector<Macroblock> m_macroblocks {}; |
397 | | Array<i16, 4> m_last_dc_values {}; |
398 | | |
399 | | JPEGBigEndianOutputBitStream m_bit_stream; |
400 | | }; |
401 | | |
402 | | ErrorOr<void> add_start_of_image(Stream& stream) |
403 | 0 | { |
404 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_SOI)); |
405 | 0 | return {}; |
406 | 0 | } |
407 | | |
408 | | ErrorOr<void> add_end_of_image(Stream& stream) |
409 | 0 | { |
410 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_EOI)); |
411 | 0 | return {}; |
412 | 0 | } |
413 | | |
414 | | ErrorOr<void> add_icc_data(Stream& stream, ReadonlyBytes icc_data) |
415 | 0 | { |
416 | | // https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf, JFIF section |
417 | 0 | constexpr StringView icc_chunk_name = "ICC_PROFILE\0"sv; |
418 | | |
419 | | // One JPEG chunk is at most 65535 bytes long, which includes the size of the 2-byte |
420 | | // "length" field. This leaves 65533 bytes for the actual data. One ICC chunk needs |
421 | | // 12 bytes for the "ICC_PROFILE\0" app id and then one byte each for the current |
422 | | // sequence number and the number of ICC chunks. This leaves 65519 bytes for the |
423 | | // ICC data. |
424 | 0 | constexpr size_t icc_chunk_header_size = 2 + icc_chunk_name.length() + 1 + 1; |
425 | 0 | constexpr size_t max_chunk_size = 65535 - icc_chunk_header_size; |
426 | 0 | static_assert(max_chunk_size == 65519); |
427 | |
|
428 | 0 | constexpr size_t max_number_of_icc_chunks = 255; // Chunk IDs are stored in an u8 and start at 1. |
429 | 0 | constexpr size_t max_icc_data_size = max_chunk_size * max_number_of_icc_chunks; |
430 | | |
431 | | // "The 1-byte chunk count limits the size of embeddable profiles to 16 707 345 bytes."" |
432 | 0 | static_assert(max_icc_data_size == 16'707'345); |
433 | |
|
434 | 0 | if (icc_data.size() > max_icc_data_size) |
435 | 0 | return Error::from_string_view("JPEGWriter: icc data too large for jpeg format"sv); |
436 | | |
437 | 0 | size_t const number_of_icc_chunks = AK::ceil_div(icc_data.size(), max_chunk_size); |
438 | 0 | for (size_t chunk_id = 1; chunk_id <= number_of_icc_chunks; ++chunk_id) { |
439 | 0 | size_t const chunk_size = min(icc_data.size(), max_chunk_size); |
440 | |
|
441 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_APPN2)); |
442 | 0 | TRY(stream.write_value<BigEndian<u16>>(icc_chunk_header_size + chunk_size)); |
443 | 0 | TRY(stream.write_until_depleted(icc_chunk_name.bytes())); |
444 | 0 | TRY(stream.write_value<u8>(chunk_id)); |
445 | 0 | TRY(stream.write_value<u8>(number_of_icc_chunks)); |
446 | 0 | TRY(stream.write_until_depleted(icc_data.slice(0, chunk_size))); |
447 | 0 | icc_data = icc_data.slice(chunk_size); |
448 | 0 | } |
449 | 0 | VERIFY(icc_data.is_empty()); |
450 | 0 | return {}; |
451 | 0 | } |
452 | | |
453 | | ErrorOr<void> add_frame_header(Stream& stream, JPEGEncodingContext const& context, IntSize size, Mode mode) |
454 | 0 | { |
455 | | // B.2.2 - Frame header syntax |
456 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_SOF0)); |
457 | | |
458 | 0 | u16 const Nf = mode == Mode::CMYK ? 4 : 3; |
459 | | |
460 | | // Lf = 8 + 3 × Nf |
461 | 0 | TRY(stream.write_value<BigEndian<u16>>(8 + 3 * Nf)); |
462 | | |
463 | | // P |
464 | 0 | TRY(stream.write_value<u8>(8)); |
465 | | |
466 | | // Y |
467 | 0 | TRY(stream.write_value<BigEndian<u16>>(size.height())); |
468 | | |
469 | | // X |
470 | 0 | TRY(stream.write_value<BigEndian<u16>>(size.width())); |
471 | | |
472 | | // Nf |
473 | 0 | TRY(stream.write_value<u8>(Nf)); |
474 | | |
475 | | // Encode Nf components |
476 | 0 | for (u8 i {}; i < Nf; ++i) { |
477 | | // Ci |
478 | 0 | TRY(stream.write_value<u8>(i + 1)); |
479 | | |
480 | | // Hi and Vi |
481 | 0 | TRY(stream.write_value<u8>((1 << 4) | 1)); |
482 | | |
483 | | // Tqi |
484 | 0 | TRY(stream.write_value<u8>((i == 0 || i == 3 ? context.luminance_quantization_table() : context.chrominance_quantization_table()).id)); |
485 | 0 | } |
486 | | |
487 | 0 | return {}; |
488 | 0 | } |
489 | | |
490 | | ErrorOr<void> add_ycck_color_transform_header(Stream& stream) |
491 | 0 | { |
492 | | // T-REC-T.872-201206-I!!PDF-E.pdf, 6.5.3 APP14 marker segment for colour encoding |
493 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_APPN14)); |
494 | 0 | TRY(stream.write_value<BigEndian<u16>>(14)); |
495 | | |
496 | 0 | TRY(stream.write_until_depleted("Adobe\0"sv.bytes())); |
497 | | |
498 | | // These values are ignored. |
499 | 0 | TRY(stream.write_value<u8>(0x64)); |
500 | 0 | TRY(stream.write_value<BigEndian<u16>>(0x0000)); |
501 | 0 | TRY(stream.write_value<BigEndian<u16>>(0x0000)); |
502 | | |
503 | | // YCCK |
504 | 0 | TRY(stream.write_value<u8>(0x2)); |
505 | 0 | return {}; |
506 | 0 | } |
507 | | |
508 | | ErrorOr<void> add_quantization_table(Stream& stream, QuantizationTable const& table) |
509 | 0 | { |
510 | | // B.2.4.1 - Quantization table-specification syntax |
511 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_DQT)); |
512 | | |
513 | | // Lq = 2 + 1 * 65 |
514 | 0 | TRY(stream.write_value<BigEndian<u16>>(2 + 65)); |
515 | | |
516 | | // Pq and Tq |
517 | 0 | TRY(stream.write_value<u8>((0 << 4) | table.id)); |
518 | | |
519 | 0 | for (u8 i = 0; i < 64; ++i) |
520 | 0 | TRY(stream.write_value<u8>(table.table[zigzag_map[i]])); |
521 | | |
522 | 0 | return {}; |
523 | 0 | } |
524 | | |
525 | | ErrorOr<Vector<Vector<u8>, 16>> sort_symbols_per_size(OutputHuffmanTable const& table) |
526 | 0 | { |
527 | | // JPEG only allows symbol with a size less than or equal to 16. |
528 | 0 | Vector<Vector<u8>, 16> output {}; |
529 | 0 | TRY(output.try_resize(16)); |
530 | | |
531 | 0 | for (auto const& symbol : table.table) |
532 | 0 | TRY(output[symbol.code_length - 1].try_append(symbol.input_byte)); |
533 | | |
534 | 0 | return output; |
535 | 0 | } |
536 | | |
537 | | ErrorOr<void> add_huffman_table(Stream& stream, OutputHuffmanTable const& table) |
538 | 0 | { |
539 | | // B.2.4.2 - Huffman table-specification syntax |
540 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_DHT)); |
541 | | |
542 | | // Lh |
543 | 0 | TRY(stream.write_value<BigEndian<u16>>(2 + 17 + table.table.size())); |
544 | | |
545 | | // Tc and Th |
546 | 0 | TRY(stream.write_value<u8>(table.id)); |
547 | | |
548 | 0 | auto const vectorized_table = TRY(sort_symbols_per_size(table)); |
549 | 0 | for (auto const& symbol_vector : vectorized_table) |
550 | 0 | TRY(stream.write_value<u8>(symbol_vector.size())); |
551 | | |
552 | 0 | for (auto const& symbol_vector : vectorized_table) { |
553 | 0 | for (auto symbol : symbol_vector) |
554 | 0 | TRY(stream.write_value<u8>(symbol)); |
555 | 0 | } |
556 | | |
557 | 0 | return {}; |
558 | 0 | } |
559 | | |
560 | | ErrorOr<void> add_scan_header(Stream& stream, Mode mode) |
561 | 0 | { |
562 | | // B.2.3 - Scan header syntax |
563 | 0 | TRY(stream.write_value<BigEndian<Marker>>(JPEG_SOS)); |
564 | | |
565 | 0 | u16 const Ns = mode == Mode::CMYK ? 4 : 3; |
566 | | |
567 | | // Ls - 6 + 2 × Ns |
568 | 0 | TRY(stream.write_value<BigEndian<u16>>(6 + 2 * Ns)); |
569 | | |
570 | | // Ns |
571 | 0 | TRY(stream.write_value<u8>(Ns)); |
572 | | |
573 | | // Encode Ns components |
574 | 0 | for (u8 i {}; i < Ns; ++i) { |
575 | | // Csj |
576 | 0 | TRY(stream.write_value<u8>(i + 1)); |
577 | | |
578 | | // Tdj and Taj |
579 | | // We're using 0 for luminance and 1 for chrominance |
580 | 0 | u8 const huffman_identifier = i == 0 || i == 3 ? 0 : 1; |
581 | 0 | TRY(stream.write_value<u8>((huffman_identifier << 4) | huffman_identifier)); |
582 | 0 | } |
583 | | |
584 | | // Ss |
585 | 0 | TRY(stream.write_value<u8>(0)); |
586 | | |
587 | | // Se |
588 | 0 | TRY(stream.write_value<u8>(63)); |
589 | | |
590 | | // Ah and Al |
591 | 0 | TRY(stream.write_value<u8>((0 << 4) | 0)); |
592 | | |
593 | 0 | return {}; |
594 | 0 | } |
595 | | |
596 | | ErrorOr<void> add_headers(Stream& stream, JPEGEncodingContext& context, JPEGWriter::Options const& options, IntSize size, Mode mode) |
597 | 0 | { |
598 | 0 | context.set_luminance_quantization_table(s_default_luminance_quantization_table, options.quality); |
599 | 0 | context.set_chrominance_quantization_table(s_default_chrominance_quantization_table, options.quality); |
600 | |
|
601 | 0 | context.dc_luminance_huffman_table = s_default_dc_luminance_huffman_table; |
602 | 0 | context.dc_chrominance_huffman_table = s_default_dc_chrominance_huffman_table; |
603 | |
|
604 | 0 | context.ac_luminance_huffman_table = s_default_ac_luminance_huffman_table; |
605 | 0 | context.ac_chrominance_huffman_table = s_default_ac_chrominance_huffman_table; |
606 | |
|
607 | 0 | TRY(add_start_of_image(stream)); |
608 | | |
609 | 0 | if (options.icc_data.has_value()) |
610 | 0 | TRY(add_icc_data(stream, options.icc_data.value())); |
611 | | |
612 | 0 | if (mode == Mode::CMYK) |
613 | 0 | TRY(add_ycck_color_transform_header(stream)); |
614 | 0 | TRY(add_frame_header(stream, context, size, mode)); |
615 | | |
616 | 0 | TRY(add_quantization_table(stream, context.luminance_quantization_table())); |
617 | 0 | TRY(add_quantization_table(stream, context.chrominance_quantization_table())); |
618 | | |
619 | 0 | TRY(add_huffman_table(stream, context.dc_luminance_huffman_table)); |
620 | 0 | TRY(add_huffman_table(stream, context.dc_chrominance_huffman_table)); |
621 | 0 | TRY(add_huffman_table(stream, context.ac_luminance_huffman_table)); |
622 | 0 | TRY(add_huffman_table(stream, context.ac_chrominance_huffman_table)); |
623 | | |
624 | 0 | TRY(add_scan_header(stream, mode)); |
625 | 0 | return {}; |
626 | 0 | } |
627 | | |
628 | | ErrorOr<void> add_image(Stream& stream, JPEGEncodingContext& context, Mode mode) |
629 | 0 | { |
630 | 0 | context.fdct_and_quantization(mode); |
631 | 0 | TRY(context.write_huffman_stream(mode)); |
632 | 0 | TRY(add_end_of_image(stream)); |
633 | 0 | return {}; |
634 | 0 | } |
635 | | |
636 | | } |
637 | | |
638 | | ErrorOr<void> JPEGWriter::encode(Stream& stream, Bitmap const& bitmap, Options const& options) |
639 | 0 | { |
640 | 0 | JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } }; |
641 | 0 | TRY(add_headers(stream, context, options, bitmap.size(), Mode::RGB)); |
642 | 0 | TRY(context.initialize_mcu(bitmap)); |
643 | 0 | TRY(add_image(stream, context, Mode::RGB)); |
644 | 0 | return {}; |
645 | 0 | } |
646 | | |
647 | | ErrorOr<void> JPEGWriter::encode(Stream& stream, CMYKBitmap const& bitmap, Options const& options) |
648 | 0 | { |
649 | 0 | JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } }; |
650 | 0 | TRY(add_headers(stream, context, options, bitmap.size(), Mode::CMYK)); |
651 | 0 | TRY(context.initialize_mcu(bitmap)); |
652 | 0 | TRY(add_image(stream, context, Mode::CMYK)); |
653 | 0 | return {}; |
654 | 0 | } |
655 | | |
656 | | } |