From 2a888ca626bfd0f1771f40168beb9ffb75c2f665 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 16 Jun 2024 16:08:31 +0200 Subject: [PATCH] LibGfx: Remove home-grown JPEG codec in favor of libjpeg-turbo --- AK/Debug.h.in | 4 - Meta/CMake/all_the_debug_macros.cmake | 1 - Meta/Lagom/CMakeLists.txt | 5 - Meta/gn/secondary/AK/BUILD.gn | 1 - Tests/LibGfx/TestImageDecoder.cpp | 23 +- .../test-inputs/jpg/12-bit-progressive.jpg | Bin 12439 -> 0 bytes Tests/LibGfx/test-inputs/jpg/12-bit.jpg | Bin 12968 -> 0 bytes Userland/Libraries/LibGfx/CMakeLists.txt | 5 + .../LibGfx/ImageFormats/JPEGLoader.cpp | 2085 +---------------- .../LibGfx/ImageFormats/JPEGLoader.h | 20 +- .../LibGfx/ImageFormats/JPEGShared.h | 111 - .../LibGfx/ImageFormats/JPEGWriter.cpp | 715 +----- .../LibGfx/ImageFormats/JPEGWriter.h | 6 + .../LibGfx/ImageFormats/JPEGWriterTables.h | 457 ---- Userland/Utilities/CMakeLists.txt | 2 - Userland/Utilities/image.cpp | 11 - Userland/Utilities/test-jpeg-roundtrip.cpp | 105 - vcpkg.json | 5 + 18 files changed, 215 insertions(+), 3341 deletions(-) delete mode 100644 Tests/LibGfx/test-inputs/jpg/12-bit-progressive.jpg delete mode 100644 Tests/LibGfx/test-inputs/jpg/12-bit.jpg delete mode 100644 Userland/Libraries/LibGfx/ImageFormats/JPEGShared.h delete mode 100644 Userland/Libraries/LibGfx/ImageFormats/JPEGWriterTables.h delete mode 100644 Userland/Utilities/test-jpeg-roundtrip.cpp diff --git a/AK/Debug.h.in b/AK/Debug.h.in index 02d3be5e06..f9b6494289 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -134,10 +134,6 @@ # cmakedefine01 JOB_DEBUG #endif -#ifndef JPEG_DEBUG -# cmakedefine01 JPEG_DEBUG -#endif - #ifndef JPEG2000_DEBUG # cmakedefine01 JPEG2000_DEBUG #endif diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 8f187983d2..07a633eac5 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -29,7 +29,6 @@ set(IMAGE_DECODER_DEBUG ON) set(IMAGE_LOADER_DEBUG ON) set(JBIG2_DEBUG ON) set(JOB_DEBUG ON) -set(JPEG_DEBUG ON) set(JPEG2000_DEBUG ON) set(JS_BYTECODE_DEBUG ON) set(JS_MODULE_DEBUG ON) diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index fd97f87ad2..207f7925e4 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -573,11 +573,6 @@ if (BUILD_TESTING) # It is therefore not reasonable to run it on Lagom, and we only run the Regex test lagom_test(../../Tests/LibRegex/Regex.cpp LIBS LibRegex WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../Tests/LibRegex) - # test-jpeg-roundtrip - add_executable(test-jpeg-roundtrip - ../../Userland/Utilities/test-jpeg-roundtrip.cpp) - target_link_libraries(test-jpeg-roundtrip AK LibGfx LibMain) - # JavaScriptTestRunner + LibTest tests # test-js add_executable(test-js diff --git a/Meta/gn/secondary/AK/BUILD.gn b/Meta/gn/secondary/AK/BUILD.gn index 01b49efd4b..a55d2ef9ed 100644 --- a/Meta/gn/secondary/AK/BUILD.gn +++ b/Meta/gn/secondary/AK/BUILD.gn @@ -250,7 +250,6 @@ write_cmake_config("ak_debug_gen") { "IMAGE_LOADER_DEBUG=", "JBIG2_DEBUG=", "JOB_DEBUG=", - "JPEG_DEBUG=", "JPEG2000_DEBUG=", "JS_BYTECODE_DEBUG=", "JS_MODULE_DEBUG=", diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index 911f675899..18704da9a1 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -508,24 +508,6 @@ TEST_CASE(test_jpeg_sof2_successive_aproximation) TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 600, 800 })); } -TEST_CASE(test_jpeg_sof1_12bits) -{ - auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpg/12-bit.jpg"sv))); - EXPECT(Gfx::JPEGImageDecoderPlugin::sniff(file->bytes())); - auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEGImageDecoderPlugin::create(file->bytes())); - - TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 320, 240 })); -} - -TEST_CASE(test_jpeg_sof2_12bits) -{ - auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpg/12-bit-progressive.jpg"sv))); - EXPECT(Gfx::JPEGImageDecoderPlugin::sniff(file->bytes())); - auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEGImageDecoderPlugin::create(file->bytes())); - - TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 320, 240 })); -} - TEST_CASE(test_jpeg_empty_icc) { auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("jpg/gradient_empty_icc.jpg"sv))); @@ -561,8 +543,9 @@ TEST_CASE(test_jpeg_malformed_header) for (auto test_input : test_inputs) { auto file = TRY_OR_FAIL(Core::MappedFile::map(test_input)); - auto plugin_decoder_or_error = Gfx::JPEGImageDecoderPlugin::create(file->bytes()); - EXPECT(plugin_decoder_or_error.is_error()); + auto plugin_decoder = TRY_OR_FAIL(Gfx::JPEGImageDecoderPlugin::create(file->bytes())); + auto frame_or_error = plugin_decoder->frame(0); + EXPECT(frame_or_error.is_error()); } } diff --git a/Tests/LibGfx/test-inputs/jpg/12-bit-progressive.jpg b/Tests/LibGfx/test-inputs/jpg/12-bit-progressive.jpg deleted file mode 100644 index a463f7b32c9b3c60c3432efa35f647a56e027166..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12439 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 Rzc$#WD#n{{-^QP%{{fco&wl^_ diff --git a/Tests/LibGfx/test-inputs/jpg/12-bit.jpg b/Tests/LibGfx/test-inputs/jpg/12-bit.jpg deleted file mode 100644 index d7173d08e1ee9d5d5ab1e956ce4fb396bccf6a6c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12968 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 tc;6Gf=0d$?Zu_2pa_{J}$GpRQ{CxY + * Copyright (c) 2024, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include -#include -#include +#include +#include namespace Gfx { -struct MacroblockMeta { - u32 total { 0 }; - u32 padded_total { 0 }; - u32 hcount { 0 }; - u32 vcount { 0 }; - u32 hpadded_count { 0 }; - u32 vpadded_count { 0 }; -}; - -struct SamplingFactors { - u8 horizontal {}; - u8 vertical {}; - - bool operator==(SamplingFactors const&) const = default; -}; - -// In the JPEG format, components are defined first at the frame level, then -// referenced in each scan and aggregated with scan-specific information. The -// two following structs mimic this hierarchy. - -struct Component { - // B.2.2 - Frame header syntax - u8 id { 0 }; // Ci, Component identifier - SamplingFactors sampling_factors { 1, 1 }; // Hi, Horizontal sampling factor and Vi, Vertical sampling factor - u8 quantization_table_id { 0 }; // Tqi, Quantization table destination selector - - // The JPEG specification does not specify which component corresponds to - // Y, Cb or Cr. This field (actually the index in the parent Vector) will - // act as an authority to determine the *real* component. - // Please note that this is implementation specific. - u8 index { 0 }; -}; - -struct ScanComponent { - // B.2.3 - Scan header syntax - Component& component; - u8 dc_destination_id { 0 }; // Tdj, DC entropy coding table destination selector - u8 ac_destination_id { 0 }; // Taj, AC entropy coding table destination selector -}; - -struct StartOfFrame { - - // Of these, only the first 3 are in mainstream use, and refers to SOF0-2. - enum class FrameType { - Baseline_DCT = 0, - Extended_Sequential_DCT = 1, - Progressive_DCT = 2, - Sequential_Lossless = 3, - Differential_Sequential_DCT = 5, - Differential_Progressive_DCT = 6, - Differential_Sequential_Lossless = 7, - Extended_Sequential_DCT_Arithmetic = 9, - Progressive_DCT_Arithmetic = 10, - Sequential_Lossless_Arithmetic = 11, - Differential_Sequential_DCT_Arithmetic = 13, - Differential_Progressive_DCT_Arithmetic = 14, - Differential_Sequential_Lossless_Arithmetic = 15, - }; - - FrameType type { FrameType::Baseline_DCT }; - u8 precision { 0 }; - u16 height { 0 }; - u16 width { 0 }; -}; - -struct HuffmanTable { - u8 type { 0 }; - u8 destination_id { 0 }; - u8 code_counts[16] = { 0 }; - Vector symbols; - Vector codes; - - // Note: The value 8 is chosen quite arbitrarily, the only current constraint - // is that both the symbol and the size fit in an u16. I've tested more - // values but none stand out, and 8 is the value used by libjpeg-turbo. - static constexpr u8 bits_per_cached_code = 8; - static constexpr u8 maximum_bits_per_code = 16; - u8 first_non_cached_code_index {}; - - ErrorOr generate_codes() - { - unsigned code = 0; - for (auto number_of_codes : code_counts) { - for (int i = 0; i < number_of_codes; i++) - codes.append(code++); - code <<= 1; - } - - TRY(generate_lookup_table()); - return {}; - } - - struct SymbolAndSize { - u8 symbol {}; - u8 size {}; - }; - - ErrorOr symbol_from_code(u16 code) const - { - static constexpr u8 shift_for_cache = maximum_bits_per_code - bits_per_cached_code; - - if (lookup_table[code >> shift_for_cache] != invalid_entry) { - u8 const code_length = lookup_table[code >> shift_for_cache] >> bits_per_cached_code; - return SymbolAndSize { static_cast(lookup_table[code >> shift_for_cache]), code_length }; - } - - u64 code_cursor = first_non_cached_code_index; - - for (u8 i = HuffmanTable::bits_per_cached_code; i < 16; i++) { - auto const result = code >> (maximum_bits_per_code - 1 - i); - for (u32 j = 0; j < code_counts[i]; j++) { - if (result == codes[code_cursor]) - return SymbolAndSize { symbols[code_cursor], static_cast(i + 1) }; - - code_cursor++; - } - } - - return Error::from_string_literal("This kind of JPEG is not yet supported by the decoder"); - } - -private: - static constexpr u16 invalid_entry = 0xFF; - - ErrorOr generate_lookup_table() - { - lookup_table.fill(invalid_entry); - - u32 code_offset = 0; - for (u8 code_length = 1; code_length <= bits_per_cached_code; code_length++) { - for (u32 i = 0; i < code_counts[code_length - 1]; i++, code_offset++) { - u32 code_key = codes[code_offset] << (bits_per_cached_code - code_length); - u8 duplicate_count = 1 << (bits_per_cached_code - code_length); - if (code_key + duplicate_count >= lookup_table.size()) - return Error::from_string_literal("Malformed Huffman table"); - - for (; duplicate_count > 0; duplicate_count--) { - lookup_table[code_key] = (code_length << bits_per_cached_code) | symbols[code_offset]; - code_key++; - } - } - } - return {}; - } - - Array lookup_table {}; -}; - -class HuffmanStream; - -class JPEGStream { -public: - static ErrorOr create(NonnullOwnPtr stream) - { - Vector buffer; - TRY(buffer.try_resize(buffer_size)); - JPEGStream jpeg_stream { move(stream), move(buffer) }; - - TRY(jpeg_stream.refill_buffer()); - jpeg_stream.m_offset_from_start = 0; - return jpeg_stream; - } - - ALWAYS_INLINE ErrorOr read_u8() - { - if (m_byte_offset == m_current_size) - TRY(refill_buffer()); - return m_buffer[m_byte_offset++]; - } - - ALWAYS_INLINE ErrorOr read_u16() - { - if (m_saved_marker.has_value()) - return m_saved_marker.release_value(); - - return (static_cast(TRY(read_u8())) << 8) | TRY(read_u8()); - } - - ALWAYS_INLINE ErrorOr discard(u64 bytes) - { - auto const discarded_from_buffer = min(m_current_size - m_byte_offset, bytes); - m_byte_offset += discarded_from_buffer; - - if (discarded_from_buffer < bytes) { - m_offset_from_start += bytes - discarded_from_buffer; - TRY(m_stream->discard(bytes - discarded_from_buffer)); - } - - return {}; - } - - ErrorOr read_until_filled(Bytes bytes) - { - auto const copied = m_buffer.span().slice(m_byte_offset).copy_trimmed_to(bytes); - m_byte_offset += copied; - - if (copied < bytes.size()) { - m_offset_from_start += bytes.size() - copied; - TRY(m_stream->read_until_filled(bytes.slice(copied))); - } - - return {}; - } - - Optional& saved_marker(Badge) - { - return m_saved_marker; - } - - u64 byte_offset() const - { - return m_offset_from_start + m_byte_offset; - } - -private: - JPEGStream(NonnullOwnPtr stream, Vector buffer) - : m_stream(move(stream)) - , m_buffer(move(buffer)) - { - } - - ErrorOr refill_buffer() - { - VERIFY(m_byte_offset == m_current_size); - - m_offset_from_start += m_byte_offset; - - m_current_size = TRY(m_stream->read_some(m_buffer.span())).size(); - if (m_current_size == 0) - return Error::from_string_literal("Unexpected end of file"); - - m_byte_offset = 0; - - return {}; - } - - static constexpr auto buffer_size = 4096; - - NonnullOwnPtr m_stream; - - Optional m_saved_marker {}; - - Vector m_buffer {}; - u64 m_offset_from_start { 0 }; - u64 m_byte_offset { buffer_size }; - u64 m_current_size { buffer_size }; -}; - -class HuffmanStream { -public: - ALWAYS_INLINE ErrorOr next_symbol(HuffmanTable const& table) - { - u16 const code = TRY(peek_bits(HuffmanTable::maximum_bits_per_code)); - - auto const symbol_and_size = TRY(table.symbol_from_code(code)); - - TRY(discard_bits(symbol_and_size.size)); - return symbol_and_size.symbol; - } - - ALWAYS_INLINE ErrorOr read_bits(u8 count = 1) - { - if (count > NumericLimits::digits()) { - dbgln_if(JPEG_DEBUG, "Can't read {} bits at once!", count); - return Error::from_string_literal("Reading too much huffman bits at once"); - } - - u16 const value = TRY(peek_bits(count)); - TRY(discard_bits(count)); - return value; - } - - ALWAYS_INLINE ErrorOr peek_bits(u8 count) - { - if (count == 0) - return 0; - - if (count + m_bit_offset > bits_in_reservoir) - TRY(refill_reservoir()); - - auto const mask = NumericLimits::max() >> (NumericLimits::digits() - count); - - return static_cast((m_bit_reservoir >> (bits_in_reservoir - m_bit_offset - count)) & mask); - } - - ALWAYS_INLINE ErrorOr discard_bits(u8 count) - { - m_bit_offset += count; - - if (m_bit_offset > bits_in_reservoir) - TRY(refill_reservoir()); - - return {}; - } - - ErrorOr advance_to_byte_boundary() - { - if (auto remainder = m_bit_offset % 8; remainder != 0) - TRY(discard_bits(bits_per_byte - remainder)); - - return {}; - } - - HuffmanStream(JPEGStream& stream) - : jpeg_stream(stream) - { - } - -private: - ALWAYS_INLINE ErrorOr refill_reservoir() - { - auto const bytes_needed = m_bit_offset / bits_per_byte; - - u8 bytes_added {}; - - auto const append_byte = [&](u8 byte) { - m_last_byte_was_ff = false; - m_bit_reservoir <<= 8; - m_bit_reservoir |= byte; - m_bit_offset -= 8; - bytes_added++; - }; - - do { - // Note: We fake zeroes when we have reached another segment - // It allows us to continue peeking seamlessly. - u8 const next_byte = jpeg_stream.saved_marker({}).has_value() ? 0 : TRY(jpeg_stream.read_u8()); - - if (m_last_byte_was_ff) { - if (next_byte == 0xFF) - continue; - - if (next_byte == 0x00) { - append_byte(0xFF); - continue; - } - - Marker const marker = 0xFF00 | next_byte; - if (marker < JPEG_RST0 || marker > JPEG_RST7) { - // Note: The only way to know that we reached the end of a segment is to read - // the marker of the following one. So we store it for later use. - jpeg_stream.saved_marker({}) = marker; - m_last_byte_was_ff = false; - continue; - } - } - - if (next_byte == 0xFF) { - m_last_byte_was_ff = true; - continue; - } - - append_byte(next_byte); - } while (bytes_added < bytes_needed); - - return {}; - } - - JPEGStream& jpeg_stream; - - using Reservoir = u64; - static constexpr auto bits_per_byte = 8; - static constexpr auto bits_in_reservoir = sizeof(Reservoir) * bits_per_byte; - - Reservoir m_bit_reservoir {}; - u8 m_bit_offset { bits_in_reservoir }; - - bool m_last_byte_was_ff { false }; -}; - -struct ICCMultiChunkState { - u8 seen_number_of_icc_chunks { 0 }; - FixedArray chunks; -}; - -struct Scan { - Scan(HuffmanStream stream) - : huffman_stream(stream) - { - } - - // B.2.3 - Scan header syntax - Vector components; - - u8 spectral_selection_start {}; // Ss - u8 spectral_selection_end {}; // Se - u8 successive_approximation_high {}; // Ah - u8 successive_approximation_low {}; // Al - - HuffmanStream huffman_stream; - - u64 end_of_bands_run_count { 0 }; - - // See the note on Figure B.4 - Scan header syntax - bool are_components_interleaved() const - { - return components.size() != 1; - } -}; - -enum class ColorTransform { - // https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.872-201206-I!!PDF-E&type=items - // 6.5.3 - APP14 marker segment for colour encoding - CmykOrRgb = 0, - YCbCr = 1, - YCCK = 2, -}; - struct JPEGLoadingContext { - JPEGLoadingContext(JPEGStream jpeg_stream, JPEGDecoderOptions options) - : stream(move(jpeg_stream)) - , options(options) - { - } - - static ErrorOr> create(NonnullOwnPtr stream, JPEGDecoderOptions options) - { - auto jpeg_stream = TRY(JPEGStream::create(move(stream))); - return make(move(jpeg_stream), options); - } - - enum State { - NotDecoded = 0, + enum class State { + NotDecoded, Error, - FrameDecoded, - HeaderDecoded, - BitmapDecoded + Decoded, }; State state { State::NotDecoded }; - Array, 4> quantization_tables {}; - Array registered_quantization_tables {}; - - StartOfFrame frame; - SamplingFactors sampling_factors {}; - - Optional current_scan {}; - - Vector components; - - RefPtr bitmap; + RefPtr rgb_bitmap; RefPtr cmyk_bitmap; - u16 dc_restart_interval { 0 }; - HashMap dc_tables; - HashMap ac_tables; - Array previous_dc_values {}; - MacroblockMeta mblock_meta; - JPEGStream stream; - JPEGDecoderOptions options; + ReadonlyBytes data; + Vector icc_data; - Optional color_transform {}; + JPEGLoadingContext(ReadonlyBytes data) + : data(data) + { + } - OwnPtr exif_metadata {}; - - Optional icc_multi_chunk_state; - Optional icc_data; + ErrorOr decode(); }; -static inline auto* get_component(Macroblock& block, unsigned component) -{ - switch (component) { - case 0: - return block.y; - case 1: - return block.cb; - case 2: - return block.cr; - case 3: - return block.k; - default: - VERIFY_NOT_REACHED(); - } -} - -static ErrorOr refine_coefficient(Scan& scan, auto& coefficient) -{ - // G.1.2.3 - Coding model for subsequent scans of successive approximation - // See the correction bit from rule b. - u8 const bit = TRY(scan.huffman_stream.read_bits(1)); - if (bit == 1) - coefficient |= 1 << scan.successive_approximation_low; - - return {}; -} - -enum class JPEGDecodingMode { - Sequential, - Progressive +struct JPEGErrorManager : jpeg_error_mgr { + jmp_buf setjmp_buffer {}; }; -template -static ErrorOr add_dc(JPEGLoadingContext& context, Macroblock& macroblock, ScanComponent const& scan_component) +ErrorOr JPEGLoadingContext::decode() { - auto maybe_table = context.dc_tables.get(scan_component.dc_destination_id); - if (!maybe_table.has_value()) { - dbgln_if(JPEG_DEBUG, "Unable to find a DC table with id: {}", scan_component.dc_destination_id); - return Error::from_string_literal("Unable to find corresponding DC table"); + struct jpeg_decompress_struct cinfo; + struct JPEGErrorManager jerr; + cinfo.err = jpeg_std_error(&jerr); + + jpeg_source_mgr source_manager {}; + + if (setjmp(jerr.setjmp_buffer)) { + jpeg_destroy_decompress(&cinfo); + state = State::Error; + return Error::from_string_literal("Failed to decode JPEG"); } - auto& dc_table = maybe_table.value(); - auto& scan = *context.current_scan; - - auto* select_component = get_component(macroblock, scan_component.component.index); - auto& coefficient = select_component[0]; - - if (DecodingMode == JPEGDecodingMode::Progressive && scan.successive_approximation_high > 0) { - TRY(refine_coefficient(scan, coefficient)); - return {}; - } - - // For DC coefficients, symbol encodes the length of the coefficient. - auto dc_length = TRY(scan.huffman_stream.next_symbol(dc_table)); - - // F.1.2.1.2 - Defining Huffman tables for the DC coefficients - // F.1.5.1 - Structure of DC code table for 12-bit sample precision - if ((context.frame.precision == 8 && dc_length > 11) - || (context.frame.precision == 12 && dc_length > 15)) { - dbgln_if(JPEG_DEBUG, "DC coefficient too long: {}!", dc_length); - return Error::from_string_literal("DC coefficient too long"); - } - - // DC coefficients are encoded as the difference between previous and current DC values. - i16 dc_diff = TRY(scan.huffman_stream.read_bits(dc_length)); - - // If MSB in diff is 0, the difference is -ve. Otherwise +ve. - if (dc_length != 0 && dc_diff < (1 << (dc_length - 1))) - dc_diff -= (1 << dc_length) - 1; - - auto& previous_dc = context.previous_dc_values[scan_component.component.index]; - previous_dc += dc_diff; - coefficient = previous_dc << scan.successive_approximation_low; - - return {}; -} - -template -static ALWAYS_INLINE ErrorOr read_eob(Scan& scan, u32 symbol) -{ - // OPTIMIZATION: This is a fast path for sequential JPEGs, these - // only supports EOB with a value of one block. - if constexpr (DecodingMode == JPEGDecodingMode::Sequential) - return symbol == 0x00; - - // G.1.2.2 - Progressive encoding of AC coefficients with Huffman coding - // Note: We also use it for non-progressive encoding as it supports both EOB and ZRL - - if (auto const eob = symbol & 0x0F; eob == 0 && symbol != JPEG_ZRL) { - // We encountered an EOB marker - auto const eob_base = symbol >> 4; - auto const additional_value = TRY(scan.huffman_stream.read_bits(eob_base)); - - scan.end_of_bands_run_count = additional_value + (1 << eob_base) - 1; - - // end_of_bands_run_count is decremented at the end of `build_macroblocks`. - // And we need to now that we reached End of Block in `add_ac`. - ++scan.end_of_bands_run_count; - - return true; - } - - return false; -} - -static bool is_progressive(StartOfFrame::FrameType frame_type) -{ - return frame_type == StartOfFrame::FrameType::Progressive_DCT - || frame_type == StartOfFrame::FrameType::Progressive_DCT_Arithmetic - || frame_type == StartOfFrame::FrameType::Differential_Progressive_DCT - || frame_type == StartOfFrame::FrameType::Differential_Progressive_DCT_Arithmetic; -} - -template -static ErrorOr add_ac(JPEGLoadingContext& context, Macroblock& macroblock, ScanComponent const& scan_component) -{ - auto maybe_table = context.ac_tables.get(scan_component.ac_destination_id); - if (!maybe_table.has_value()) { - dbgln_if(JPEG_DEBUG, "Unable to find a AC table with id: {}", scan_component.ac_destination_id); - return Error::from_string_literal("Unable to find corresponding AC table"); - } - - auto& ac_table = maybe_table.value(); - auto* select_component = get_component(macroblock, scan_component.component.index); - - auto& scan = *context.current_scan; - - // Compute the AC coefficients. - - // 0th coefficient is the dc, which is already handled - auto first_coefficient = max(1, scan.spectral_selection_start); - - u32 to_skip = 0; - Optional saved_symbol; - Optional saved_bit_for_rule_a; - bool in_zrl = false; - - for (int j = first_coefficient; j <= scan.spectral_selection_end; ++j) { - auto& coefficient = select_component[zigzag_map[j]]; - - // AC symbols encode 2 pieces of information, the high 4 bits represent - // number of zeroes to be stuffed before reading the coefficient. Low 4 - // bits represent the magnitude of the coefficient. - if (!in_zrl && scan.end_of_bands_run_count == 0 && !saved_symbol.has_value()) { - saved_symbol = TRY(scan.huffman_stream.next_symbol(ac_table)); - - if (!TRY(read_eob(scan, *saved_symbol))) { - to_skip = *saved_symbol >> 4; - - in_zrl = *saved_symbol == JPEG_ZRL; - if (in_zrl) { - to_skip++; - saved_symbol.clear(); - } - - if constexpr (DecodingMode == JPEGDecodingMode::Sequential) { - j += to_skip - 1; - to_skip = 0; - in_zrl = false; - continue; - } - - if constexpr (DecodingMode == JPEGDecodingMode::Progressive) { - if (!in_zrl && scan.successive_approximation_high != 0) { - // G.1.2.3 - Coding model for subsequent scans of successive approximation - // Bit sign from rule a - saved_bit_for_rule_a = TRY(scan.huffman_stream.read_bits(1)); - } - } - } else if constexpr (DecodingMode == JPEGDecodingMode::Sequential) { - break; - } - } - - if constexpr (DecodingMode == JPEGDecodingMode::Progressive) { - if (coefficient != 0) { - TRY(refine_coefficient(scan, coefficient)); - continue; - } - } - - if (to_skip > 0) { - --to_skip; - if (to_skip == 0) - in_zrl = false; - continue; - } - - if (scan.end_of_bands_run_count > 0) - continue; - - if (DecodingMode == JPEGDecodingMode::Progressive && scan.successive_approximation_high != 0) { - // G.1.2.3 - Coding model for subsequent scans of successive approximation - if (auto const low_bits = *saved_symbol & 0x0F; low_bits != 1) { - dbgln_if(JPEG_DEBUG, "AC coefficient low bits isn't equal to 1: {}!", low_bits); - return Error::from_string_literal("AC coefficient low bits isn't equal to 1"); - } - - coefficient = (*saved_bit_for_rule_a == 0 ? -1 : 1) << scan.successive_approximation_low; - saved_bit_for_rule_a.clear(); - } else { - // F.1.2.2 - Huffman encoding of AC coefficients - u8 const coeff_length = *saved_symbol & 0x0F; - - // F.1.2.2.1 - Structure of AC code table - // F.1.5.2 - Structure of AC code table for 12-bit sample precision - if ((context.frame.precision == 8 && coeff_length > 10) - || (context.frame.precision == 12 && coeff_length > 14)) { - dbgln_if(JPEG_DEBUG, "AC coefficient too long: {}!", coeff_length); - return Error::from_string_literal("AC coefficient too long"); - } - - if (coeff_length != 0) { - i32 ac_coefficient = TRY(scan.huffman_stream.read_bits(coeff_length)); - if (ac_coefficient < (1 << (coeff_length - 1))) - ac_coefficient -= (1 << coeff_length) - 1; - - coefficient = ac_coefficient * (1 << scan.successive_approximation_low); - } - } - - saved_symbol.clear(); - } - - if (to_skip > 0) { - dbgln_if(JPEG_DEBUG, "Run-length exceeded boundaries. Cursor: {}, Skipping: {}!", scan.spectral_selection_end + to_skip, to_skip); - return Error::from_string_literal("Run-length exceeded boundaries"); - } - - return {}; -} - -/** - * Build the macroblocks possible by reading single (MCU) subsampled pair of CbCr. - * Depending on the sampling factors, we may not see triples of y, cb, cr in that - * order. If sample factors differ from one, we'll read more than one block of y- - * coefficients before we get to read a cb-cr block. - - * In the function below, `hcursor` and `vcursor` denote the location of the block - * we're building in the macroblock matrix. `vfactor_i` and `hfactor_i` are cursors - * that iterate over the vertical and horizontal subsampling factors, respectively. - * When we finish one iteration of the innermost loop, we'll have the coefficients - * of one of the components of block at position `macroblock_index`. When the outermost - * loop finishes first iteration, we'll have all the luminance coefficients for all the - * macroblocks that share the chrominance data. Next two iterations (assuming that - * we are dealing with three components) will fill up the blocks with chroma data. - */ -template -static ErrorOr build_macroblocks(JPEGLoadingContext& context, Vector& macroblocks, u32 hcursor, u32 vcursor) -{ - for (auto const& scan_component : context.current_scan->components) { - for (u8 vfactor_i = 0; vfactor_i < scan_component.component.sampling_factors.vertical; vfactor_i++) { - for (u8 hfactor_i = 0; hfactor_i < scan_component.component.sampling_factors.horizontal; hfactor_i++) { - // A.2.3 - Interleaved order - u32 macroblock_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hfactor_i + hcursor); - if (!context.current_scan->are_components_interleaved()) { - macroblock_index = vcursor * context.mblock_meta.hpadded_count + (hfactor_i + (hcursor * scan_component.component.sampling_factors.vertical) + (vfactor_i * scan_component.component.sampling_factors.horizontal)); - - // A.2.4 Completion of partial MCU - // If the component is [and only if!] to be interleaved, the encoding process - // shall also extend the number of samples by one or more additional blocks. - - // Horizontally - if (macroblock_index >= context.mblock_meta.hcount && macroblock_index % context.mblock_meta.hpadded_count >= context.mblock_meta.hcount) - continue; - // Vertically - if (macroblock_index >= context.mblock_meta.hpadded_count * context.mblock_meta.vcount) - continue; - } - - Macroblock& block = macroblocks[macroblock_index]; - - if constexpr (DecodingMode == JPEGDecodingMode::Sequential) { - TRY(add_dc(context, block, scan_component)); - TRY(add_ac(context, block, scan_component)); - } else { - if (context.current_scan->spectral_selection_start == 0) - TRY(add_dc(context, block, scan_component)); - if (context.current_scan->spectral_selection_end != 0) - TRY(add_ac(context, block, scan_component)); - - // G.1.2.2 - Progressive encoding of AC coefficients with Huffman coding - if (context.current_scan->end_of_bands_run_count > 0) { - --context.current_scan->end_of_bands_run_count; - continue; - } - } - } - } - } - - return {}; -} - -static bool is_dct_based(StartOfFrame::FrameType frame_type) -{ - return frame_type == StartOfFrame::FrameType::Baseline_DCT - || frame_type == StartOfFrame::FrameType::Extended_Sequential_DCT - || frame_type == StartOfFrame::FrameType::Progressive_DCT - || frame_type == StartOfFrame::FrameType::Differential_Sequential_DCT - || frame_type == StartOfFrame::FrameType::Differential_Progressive_DCT - || frame_type == StartOfFrame::FrameType::Progressive_DCT_Arithmetic - || frame_type == StartOfFrame::FrameType::Differential_Sequential_DCT_Arithmetic - || frame_type == StartOfFrame::FrameType::Differential_Progressive_DCT_Arithmetic; -} - -static void reset_decoder(JPEGLoadingContext& context) -{ - // G.1.2.2 - Progressive encoding of AC coefficients with Huffman coding - context.current_scan->end_of_bands_run_count = 0; - - // E.2.4 Control procedure for decoding a restart interval - if (is_dct_based(context.frame.type)) { - context.previous_dc_values = {}; - return; - } - - VERIFY_NOT_REACHED(); -} - -static ErrorOr decode_huffman_stream(JPEGLoadingContext& context, Vector& macroblocks) -{ - for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.sampling_factors.vertical) { - for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.sampling_factors.horizontal) { - // FIXME: This is likely wrong for non-interleaved scans. - VERIFY(context.mblock_meta.hpadded_count % context.sampling_factors.horizontal == 0); - u32 number_of_mcus_decoded_so_far = ((vcursor / context.sampling_factors.vertical) * context.mblock_meta.hpadded_count + hcursor) / context.sampling_factors.horizontal; - - auto& huffman_stream = context.current_scan->huffman_stream; - - if (context.dc_restart_interval > 0) { - if (number_of_mcus_decoded_so_far != 0 && number_of_mcus_decoded_so_far % context.dc_restart_interval == 0) { - reset_decoder(context); - - // Restart markers are stored in byte boundaries. Advance the huffman stream cursor to - // the 0th bit of the next byte. - TRY(huffman_stream.advance_to_byte_boundary()); - - // Skip the restart marker (RSTn). - TRY(huffman_stream.discard_bits(8)); - } - } - - auto result = [&]() { - if (is_progressive(context.frame.type)) - return build_macroblocks(context, macroblocks, hcursor, vcursor); - return build_macroblocks(context, macroblocks, hcursor, vcursor); - }(); - - if (result.is_error()) { - if constexpr (JPEG_DEBUG) { - dbgln("Failed to build Macroblock {}: {}", number_of_mcus_decoded_so_far, result.error()); - dbgln("Huffman stream byte offset {:#x}", context.stream.byte_offset()); - } - return result.release_error(); - } - } - } - return {}; -} - -static bool is_frame_marker(Marker const marker) -{ - // B.1.1.3 - Marker assignments - bool const is_sof_marker = marker >= JPEG_SOF0 && marker <= JPEG_SOF15; - - // Start of frame markers are valid for JPEG_SOF0 to JPEG_SOF15 except number 4, 8 (reserved) and 12. - bool const is_defined_marker = marker != JPEG_DHT && marker != 0xFFC8 && marker != JPEG_DAC; - - return is_sof_marker && is_defined_marker; -} - -static inline bool is_supported_marker(Marker const marker) -{ - if (marker >= JPEG_APPN0 && marker <= JPEG_APPN15) { - - if (marker != JPEG_APPN0 && marker != JPEG_APPN14) - dbgln_if(JPEG_DEBUG, "{:#04x} not supported yet. The decoder may fail!", marker); - return true; - } - if (marker >= JPEG_RESERVED1 && marker <= JPEG_RESERVEDD) - return true; - if (marker >= JPEG_RST0 && marker <= JPEG_RST7) - return true; - switch (marker) { - case JPEG_COM: - case JPEG_DHP: - case JPEG_EXP: - case JPEG_DHT: - case JPEG_DQT: - case JPEG_DRI: - case JPEG_EOI: - case JPEG_SOF0: - case JPEG_SOF1: - case JPEG_SOF2: - case JPEG_SOI: - case JPEG_SOS: - return true; - } - - if (is_frame_marker(marker)) - dbgln_if(JPEG_DEBUG, "Decoding this frame-type (SOF{}) is not currently supported. Decoder will fail!", marker & 0xf); - - return false; -} - -static inline ErrorOr read_marker_at_cursor(JPEGStream& stream) -{ - u16 marker = TRY(stream.read_u16()); - - if (marker == 0xFFFF) { - u8 next { 0xFF }; - - while (next == 0xFF) - next = TRY(stream.read_u8()); - - marker = 0xFF00 | next; - } - - if (is_supported_marker(marker)) - return marker; - - dbgln_if(JPEG_DEBUG, "Unsupported marker: {:#04x} around offset {:#x}", marker, stream.byte_offset()); - return Error::from_string_literal("Reached an unsupported marker"); -} - -static ErrorOr read_effective_chunk_size(JPEGStream& stream) -{ - // The stored chunk size includes the size of `stored_size` itself. - u16 const stored_size = TRY(stream.read_u16()); - if (stored_size < 2) - return Error::from_string_literal("Stored chunk size is too small"); - return stored_size - 2; -} - -static ErrorOr ensure_quantization_tables_are_present(JPEGLoadingContext& context) -{ - for (auto const& component : context.current_scan->components) { - if (!context.registered_quantization_tables[component.component.quantization_table_id]) - return Error::from_string_literal("Unknown quantization table id"); - } - return {}; -} - -static ErrorOr read_start_of_scan(JPEGStream& stream, JPEGLoadingContext& context) -{ - // B.2.3 - Scan header syntax - - if (context.state < JPEGLoadingContext::State::FrameDecoded) - return Error::from_string_literal("SOS found before reading a SOF"); - - [[maybe_unused]] u16 const bytes_to_read = TRY(read_effective_chunk_size(stream)); - u8 const component_count = TRY(stream.read_u8()); - - Scan current_scan(HuffmanStream { context.stream }); - - Optional last_read; - u8 component_read = 0; - for (auto& component : context.components) { - // See the Csj paragraph: - // [...] the ordering in the scan header shall follow the ordering in the frame header. - if (component_read == component_count) - break; - - if (!last_read.has_value()) - last_read = TRY(stream.read_u8()); - - if (component.id != *last_read) - continue; - - u8 const table_ids = TRY(stream.read_u8()); - - current_scan.components.empend(component, static_cast(table_ids >> 4), static_cast(table_ids & 0x0F)); - - component_read++; - last_read.clear(); - } - - if constexpr (JPEG_DEBUG) { - StringBuilder builder; - TRY(builder.try_append("Components in scan: "sv)); - for (auto const& scan_component : current_scan.components) { - TRY(builder.try_append(TRY(String::number(scan_component.component.id)))); - TRY(builder.try_append(' ')); - } - dbgln(builder.string_view()); - } - - current_scan.spectral_selection_start = TRY(stream.read_u8()); - current_scan.spectral_selection_end = TRY(stream.read_u8()); - auto const successive_approximation = TRY(stream.read_u8()); - current_scan.successive_approximation_high = successive_approximation >> 4; - current_scan.successive_approximation_low = successive_approximation & 0x0F; - - dbgln_if(JPEG_DEBUG, "Start of Selection: {}, End of Selection: {}, Successive Approximation High: {}, Successive Approximation Low: {}", - current_scan.spectral_selection_start, - current_scan.spectral_selection_end, - current_scan.successive_approximation_high, - current_scan.successive_approximation_low); - - if (current_scan.spectral_selection_start > 63 || current_scan.spectral_selection_end > 63 || current_scan.successive_approximation_high > 13 || current_scan.successive_approximation_low > 13) { - dbgln_if(JPEG_DEBUG, "ERROR! Start of Selection: {}, End of Selection: {}, Successive Approximation High: {}, Successive Approximation Low: {}!", - current_scan.spectral_selection_start, - current_scan.spectral_selection_end, - current_scan.successive_approximation_high, - current_scan.successive_approximation_low); - return Error::from_string_literal("Spectral selection is not [0,63] or successive approximation is not null"); - } - - context.current_scan = move(current_scan); - - TRY(ensure_quantization_tables_are_present(context)); - - return {}; -} - -static ErrorOr read_restart_interval(JPEGStream& stream, JPEGLoadingContext& context) -{ - // B.2.4.4 - Restart interval definition syntax - u16 bytes_to_read = TRY(read_effective_chunk_size(stream)); - if (bytes_to_read != 2) { - dbgln_if(JPEG_DEBUG, "Malformed DRI marker found!"); - return Error::from_string_literal("Malformed DRI marker found"); - } - context.dc_restart_interval = TRY(stream.read_u16()); - dbgln_if(JPEG_DEBUG, "Restart marker: {}", context.dc_restart_interval); - return {}; -} - -static ErrorOr read_huffman_table(JPEGStream& stream, JPEGLoadingContext& context) -{ - // B.2.4.2 - Huffman table-specification syntax - - u16 bytes_to_read = TRY(read_effective_chunk_size(stream)); - - while (bytes_to_read > 0) { - HuffmanTable table; - u8 const table_info = TRY(stream.read_u8()); - u8 const table_type = table_info >> 4; - u8 const table_destination_id = table_info & 0x0F; - if (table_type > 1) { - dbgln_if(JPEG_DEBUG, "Unrecognized huffman table: {}!", table_type); - return Error::from_string_literal("Unrecognized huffman table"); - } - - if ((context.frame.type == StartOfFrame::FrameType::Baseline_DCT && table_destination_id > 1) - || (context.frame.type != StartOfFrame::FrameType::Baseline_DCT && table_destination_id > 3)) { - dbgln_if(JPEG_DEBUG, "Invalid huffman table destination id: {}!", table_destination_id); - return Error::from_string_literal("Invalid huffman table destination id"); - } - - table.type = table_type; - table.destination_id = table_destination_id; - u32 total_codes = 0; - - // Read code counts. At each index K, the value represents the number of K+1 bit codes in this header. - for (int i = 0; i < 16; i++) { - if (i == HuffmanTable::bits_per_cached_code) - table.first_non_cached_code_index = total_codes; - u8 const count = TRY(stream.read_u8()); - total_codes += count; - table.code_counts[i] = count; - } - - table.codes.ensure_capacity(total_codes); - table.symbols.ensure_capacity(total_codes); - - // Read symbols. Read X bytes, where X is the sum of the counts of codes read in the previous step. - for (u32 i = 0; i < total_codes; i++) { - u8 symbol = TRY(stream.read_u8()); - table.symbols.append(symbol); - } - - TRY(table.generate_codes()); - - auto& huffman_table = table.type == 0 ? context.dc_tables : context.ac_tables; - huffman_table.set(table.destination_id, table); - - bytes_to_read -= 1 + 16 + total_codes; - } - - if (bytes_to_read != 0) { - dbgln_if(JPEG_DEBUG, "Extra bytes detected in huffman header!"); - return Error::from_string_literal("Extra bytes detected in huffman header"); - } - return {}; -} - -static ErrorOr read_icc_profile(JPEGStream& stream, JPEGLoadingContext& context, int bytes_to_read) -{ - // https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf, page 5, "JFIF". - if (bytes_to_read <= 2) { - dbgln_if(JPEG_DEBUG, "icc marker too small"); - TRY(stream.discard(bytes_to_read)); - return {}; - } - - auto chunk_sequence_number = TRY(stream.read_u8()); // 1-based - auto number_of_chunks = TRY(stream.read_u8()); - bytes_to_read -= 2; - - if (!context.icc_multi_chunk_state.has_value()) - context.icc_multi_chunk_state.emplace(ICCMultiChunkState { 0, TRY(FixedArray::create(number_of_chunks)) }); - auto& chunk_state = context.icc_multi_chunk_state; - - u8 index {}; - - auto const ensure_correctness = [&]() -> ErrorOr { - if (chunk_state->seen_number_of_icc_chunks >= number_of_chunks) - return Error::from_string_literal("Too many ICC chunks"); - - if (chunk_state->chunks.size() != number_of_chunks) - return Error::from_string_literal("Inconsistent number of total ICC chunks"); - - if (chunk_sequence_number == 0) - return Error::from_string_literal("ICC chunk sequence number not 1 based"); - - index = chunk_sequence_number - 1; - - if (index >= chunk_state->chunks.size()) - return Error::from_string_literal("ICC chunk sequence number larger than number of chunks"); - - if (!chunk_state->chunks[index].is_empty()) - return Error::from_string_literal("Duplicate ICC chunk at sequence number"); - - return {}; + jerr.error_exit = [](j_common_ptr cinfo) { + char buffer[JMSG_LENGTH_MAX]; + (*cinfo->err->format_message)(cinfo, buffer); + dbgln("JPEG error: {}", buffer); + longjmp(static_cast(cinfo->err)->setjmp_buffer, 1); }; + jpeg_create_decompress(&cinfo); - if (auto result = ensure_correctness(); result.is_error()) { - dbgln_if(JPEG_DEBUG, "JPEG: {}", result.release_error()); - TRY(stream.discard(bytes_to_read)); - return {}; - } - - chunk_state->chunks[index] = TRY(ByteBuffer::create_zeroed(bytes_to_read)); - TRY(stream.read_until_filled(chunk_state->chunks[index])); - - chunk_state->seen_number_of_icc_chunks++; - - if (chunk_state->seen_number_of_icc_chunks != chunk_state->chunks.size()) - return {}; - - if (number_of_chunks == 1) { - context.icc_data = move(chunk_state->chunks[0]); - return {}; - } - - size_t total_size = 0; - for (auto const& chunk : chunk_state->chunks) - total_size += chunk.size(); - - auto icc_bytes = TRY(ByteBuffer::create_zeroed(total_size)); - size_t start = 0; - for (auto const& chunk : chunk_state->chunks) { - memcpy(icc_bytes.data() + start, chunk.data(), chunk.size()); - start += chunk.size(); - } - - context.icc_data = move(icc_bytes); - - return {}; -} - -static ErrorOr read_colour_encoding(JPEGStream& stream, [[maybe_unused]] JPEGLoadingContext& context, int bytes_to_read) -{ - // The App 14 segment is application specific in the first JPEG standard. - // However, the Adobe implementation is globally accepted and the value of the color transform - // was latter standardized as a JPEG-1 extension. - - // For the structure of the App 14 segment, see: - // https://www.pdfa.org/norm-refs/5116.DCT_Filter.pdf - // 18 Adobe Application-Specific JPEG Marker - - // For the value of color_transform, see: - // https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.872-201206-I!!PDF-E&type=items - // 6.5.3 - APP14 marker segment for colour encoding - - if (bytes_to_read < 6) - return Error::from_string_literal("App14 segment too small"); - - [[maybe_unused]] auto const version = TRY(stream.read_u8()); - [[maybe_unused]] u16 const flag0 = TRY(stream.read_u16()); - [[maybe_unused]] u16 const flag1 = TRY(stream.read_u16()); - auto const color_transform = TRY(stream.read_u8()); - - if (bytes_to_read > 6) { - dbgln_if(JPEG_DEBUG, "Unread bytes in App14 segment: {}", bytes_to_read - 6); - TRY(stream.discard(bytes_to_read - 6)); - } - - switch (color_transform) { - case 0: - context.color_transform = ColorTransform::CmykOrRgb; - break; - case 1: - context.color_transform = ColorTransform::YCbCr; - break; - case 2: - context.color_transform = ColorTransform::YCCK; - break; - default: - dbgln("{:#x} is not a specified transform flag value, ignoring", color_transform); - } - - return {}; -} - -static ErrorOr read_exif(JPEGStream& stream, JPEGLoadingContext& context, int bytes_to_read) -{ - // This refers to Exif's specification, see TIFFLoader for more information. - // 4.7.2.2. - APP1 internal structure - if (bytes_to_read <= 1) { - TRY(stream.discard(bytes_to_read)); - return {}; - } - - // Discard padding byte - TRY(stream.discard(1)); - - auto exif_buffer = TRY(ByteBuffer::create_uninitialized(bytes_to_read - 1)); - TRY(stream.read_until_filled(exif_buffer)); - - context.exif_metadata = TRY(TIFFImageDecoderPlugin::read_exif_metadata(exif_buffer)); - - return {}; -} - -static ErrorOr read_app_marker(JPEGStream& stream, JPEGLoadingContext& context, int app_marker_number) -{ - // B.2.4.6 - Application data syntax - - u16 bytes_to_read = TRY(read_effective_chunk_size(stream)); - - StringBuilder builder; - for (;;) { - if (bytes_to_read == 0) { - dbgln_if(JPEG_DEBUG, "app marker {} does not start with zero-terminated string", app_marker_number); - return {}; + source_manager.next_input_byte = data.data(); + source_manager.bytes_in_buffer = data.size(); + source_manager.init_source = [](j_decompress_ptr) {}; + source_manager.fill_input_buffer = [](j_decompress_ptr) -> boolean { return false; }; + source_manager.skip_input_data = [](j_decompress_ptr context, long num_bytes) { + if (num_bytes > static_cast(context->src->bytes_in_buffer)) { + context->src->bytes_in_buffer = 0; + return; } + context->src->next_input_byte += num_bytes; + context->src->bytes_in_buffer -= num_bytes; + }; + source_manager.resync_to_restart = jpeg_resync_to_restart; + source_manager.term_source = [](j_decompress_ptr) {}; - auto c = TRY(stream.read_u8()); - bytes_to_read--; + cinfo.src = &source_manager; - if (c == '\0') - break; - - TRY(builder.try_append(c)); + jpeg_save_markers(&cinfo, JPEG_APP0 + 2, 0xFFFF); + if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) { + jpeg_destroy_decompress(&cinfo); + return Error::from_string_literal("Failed to read JPEG header"); } - auto app_id = TRY(builder.to_string()); - - if (app_marker_number == 1 && app_id == "Exif"sv) - return read_exif(stream, context, bytes_to_read); - if (app_marker_number == 2 && app_id == "ICC_PROFILE"sv) - return read_icc_profile(stream, context, bytes_to_read); - if (app_marker_number == 14 && app_id == "Adobe"sv) - return read_colour_encoding(stream, context, bytes_to_read); - - return stream.discard(bytes_to_read); -} - -static inline bool validate_sampling_factors_and_modify_context(SamplingFactors const& sampling_factors, JPEGLoadingContext& context) -{ - if ((sampling_factors.horizontal == 1 || sampling_factors.horizontal == 2) && (sampling_factors.vertical == 1 || sampling_factors.vertical == 2)) { - context.mblock_meta.hpadded_count += sampling_factors.horizontal == 1 ? 0 : context.mblock_meta.hcount % 2; - context.mblock_meta.vpadded_count += sampling_factors.vertical == 1 ? 0 : context.mblock_meta.vcount % 2; - context.mblock_meta.padded_total = context.mblock_meta.hpadded_count * context.mblock_meta.vpadded_count; - // For easy reference to relevant sample factors. - context.sampling_factors = sampling_factors; - - return true; - } - return false; -} - -static inline void set_macroblock_metadata(JPEGLoadingContext& context) -{ - context.mblock_meta.hcount = ceil_div(context.frame.width, 8); - context.mblock_meta.vcount = ceil_div(context.frame.height, 8); - context.mblock_meta.hpadded_count = context.mblock_meta.hcount; - context.mblock_meta.vpadded_count = context.mblock_meta.vcount; - context.mblock_meta.total = context.mblock_meta.hcount * context.mblock_meta.vcount; -} - -static ErrorOr ensure_standard_precision(StartOfFrame const& frame) -{ - // B.2.2 - Frame header syntax - // Table B.2 - Frame header parameter sizes and values - - if (frame.precision == 8) - return {}; - - if (frame.type == StartOfFrame::FrameType::Extended_Sequential_DCT && frame.precision == 12) - return {}; - - if (frame.type == StartOfFrame::FrameType::Progressive_DCT && frame.precision == 12) - return {}; - - dbgln_if(JPEG_DEBUG, "Unsupported precision: {}, for SOF type: {}!", frame.precision, static_cast(frame.type)); - return Error::from_string_literal("Unsupported SOF precision."); -} - -static ErrorOr read_start_of_frame(JPEGStream& stream, JPEGLoadingContext& context) -{ - if (context.state == JPEGLoadingContext::FrameDecoded) { - dbgln_if(JPEG_DEBUG, "SOF repeated!"); - return Error::from_string_literal("SOF repeated"); + if (cinfo.jpeg_color_space == JCS_CMYK || cinfo.jpeg_color_space == JCS_YCCK) { + cinfo.out_color_space = JCS_CMYK; + } else { + cinfo.out_color_space = JCS_EXT_BGRX; } - // B.2.2 Frame header syntax + jpeg_start_decompress(&cinfo); - [[maybe_unused]] u16 const bytes_to_read = TRY(read_effective_chunk_size(stream)); - - context.frame.precision = TRY(stream.read_u8()); - - TRY(ensure_standard_precision(context.frame)); - - context.frame.height = TRY(stream.read_u16()); - context.frame.width = TRY(stream.read_u16()); - if (!context.frame.width || !context.frame.height) { - dbgln_if(JPEG_DEBUG, "ERROR! Image height: {}, Image width: {}!", context.frame.height, context.frame.width); - return Error::from_string_literal("Image frame height of width null"); - } - - set_macroblock_metadata(context); - - auto component_count = TRY(stream.read_u8()); - if (component_count != 1 && component_count != 3 && component_count != 4) { - dbgln_if(JPEG_DEBUG, "Unsupported number of components in SOF: {}!", component_count); - return Error::from_string_literal("Unsupported number of components in SOF"); - } - - for (u8 i = 0; i < component_count; i++) { - Component component; - component.id = TRY(stream.read_u8()); - component.index = i; - - u8 subsample_factors = TRY(stream.read_u8()); - component.sampling_factors.horizontal = subsample_factors >> 4; - component.sampling_factors.vertical = subsample_factors & 0x0F; - - if (component_count == 1) { - // 4.8.2 Minimum coded unit: "If the compressed image data is non-interleaved, the MCU is defined to be one data unit." - component.sampling_factors = { 1, 1 }; + if (cinfo.out_color_space == JCS_EXT_BGRX) { + rgb_bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, { static_cast(cinfo.output_width), static_cast(cinfo.output_height) })); + while (cinfo.output_scanline < cinfo.output_height) { + auto* row_ptr = (u8*)rgb_bitmap->scanline(cinfo.output_scanline); + jpeg_read_scanlines(&cinfo, &row_ptr, 1); } - - dbgln_if(JPEG_DEBUG, "Component subsampling: {}, {}", component.sampling_factors.horizontal, component.sampling_factors.vertical); - - if (i == 0) { - // By convention, downsampling is applied only on chroma components. So we should - // hope to see the maximum sampling factor in the luma component. - if (!validate_sampling_factors_and_modify_context(component.sampling_factors, context)) { - dbgln_if(JPEG_DEBUG, "Unsupported luma subsampling factors: horizontal: {}, vertical: {}", - component.sampling_factors.horizontal, - component.sampling_factors.vertical); - return Error::from_string_literal("Unsupported luma subsampling factors"); - } - } else { - auto const& y_component = context.components[0]; - if (y_component.sampling_factors.horizontal % component.sampling_factors.horizontal != 0 - || y_component.sampling_factors.vertical % component.sampling_factors.vertical != 0) { - dbgln_if(JPEG_DEBUG, "Unsupported chroma subsampling factors: horizontal: {}, vertical: {}", - component.sampling_factors.horizontal, - component.sampling_factors.vertical); - return Error::from_string_literal("Unsupported chroma subsampling factors"); - } - } - - component.quantization_table_id = TRY(stream.read_u8()); - - context.components.append(move(component)); - } - - return {}; -} - -static ErrorOr read_quantization_table(JPEGStream& stream, JPEGLoadingContext& context) -{ - // B.2.4.1 - Quantization table-specification syntax - - u16 bytes_to_read = TRY(read_effective_chunk_size(stream)); - - while (bytes_to_read > 0) { - u8 const info_byte = TRY(stream.read_u8()); - u8 const element_unit_hint = info_byte >> 4; - if (element_unit_hint > 1) { - dbgln_if(JPEG_DEBUG, "Unsupported unit hint in quantization table: {}!", element_unit_hint); - return Error::from_string_literal("Unsupported unit hint in quantization table"); - } - u8 const table_id = info_byte & 0x0F; - - if (table_id > 3) { - dbgln_if(JPEG_DEBUG, "Unsupported quantization table id: {}!", table_id); - return Error::from_string_literal("Unsupported quantization table id"); - } - - context.registered_quantization_tables[table_id] = true; - - auto& table = context.quantization_tables[table_id]; - - for (int i = 0; i < 64; i++) { - if (element_unit_hint == 0) - table[zigzag_map[i]] = TRY(stream.read_u8()); - else - table[zigzag_map[i]] = TRY(stream.read_u16()); - } - - bytes_to_read -= 1 + (element_unit_hint == 0 ? 64 : 128); - } - if (bytes_to_read != 0) { - dbgln_if(JPEG_DEBUG, "Invalid length for one or more quantization tables!"); - return Error::from_string_literal("Invalid length for one or more quantization tables"); - } - - return {}; -} - -static ErrorOr skip_segment(JPEGStream& stream) -{ - u16 bytes_to_skip = TRY(read_effective_chunk_size(stream)); - TRY(stream.discard(bytes_to_skip)); - return {}; -} - -static void dequantize(JPEGLoadingContext& context, Vector& macroblocks) -{ - for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.sampling_factors.vertical) { - for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.sampling_factors.horizontal) { - for (u32 i = 0; i < context.components.size(); i++) { - auto const& component = context.components[i]; - - auto const& table = context.quantization_tables[component.quantization_table_id]; - - for (u32 vfactor_i = 0; vfactor_i < component.sampling_factors.vertical; vfactor_i++) { - for (u32 hfactor_i = 0; hfactor_i < component.sampling_factors.horizontal; hfactor_i++) { - u32 macroblock_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hfactor_i + hcursor); - Macroblock& block = macroblocks[macroblock_index]; - auto* block_component = get_component(block, i); - for (u32 k = 0; k < 64; k++) - block_component[k] *= table[k]; - } - } - } - } - } -} - -static void inverse_dct_8x8(i16* block_component) -{ - // Does a 2-D IDCT by doing two 1-D IDCTs as described in https://unix4lyfe.org/dct/ - // The 1-D DCT idea is described at https://unix4lyfe.org/dct-1d/, read aan.cc from bottom to top. - static float const m0 = 2.0f * AK::cos(1.0f / 16.0f * 2.0f * AK::Pi); - static float const m1 = 2.0f * AK::cos(2.0f / 16.0f * 2.0f * AK::Pi); - static float const m3 = 2.0f * AK::cos(2.0f / 16.0f * 2.0f * AK::Pi); - static float const m5 = 2.0f * AK::cos(3.0f / 16.0f * 2.0f * AK::Pi); - static float const m2 = m0 - m5; - static float const m4 = m0 + m5; - static float const s0 = AK::cos(0.0f / 16.0f * AK::Pi) / AK::sqrt(8.0f); - static float const s1 = AK::cos(1.0f / 16.0f * AK::Pi) / 2.0f; - static float const s2 = AK::cos(2.0f / 16.0f * AK::Pi) / 2.0f; - static float const s3 = AK::cos(3.0f / 16.0f * AK::Pi) / 2.0f; - static float const s4 = AK::cos(4.0f / 16.0f * AK::Pi) / 2.0f; - static float const s5 = AK::cos(5.0f / 16.0f * AK::Pi) / 2.0f; - static float const s6 = AK::cos(6.0f / 16.0f * AK::Pi) / 2.0f; - static float const s7 = AK::cos(7.0f / 16.0f * AK::Pi) / 2.0f; - - for (u32 k = 0; k < 8; ++k) { - float const g0 = block_component[0 * 8 + k] * s0; - float const g1 = block_component[4 * 8 + k] * s4; - float const g2 = block_component[2 * 8 + k] * s2; - float const g3 = block_component[6 * 8 + k] * s6; - float const g4 = block_component[5 * 8 + k] * s5; - float const g5 = block_component[1 * 8 + k] * s1; - float const g6 = block_component[7 * 8 + k] * s7; - float const g7 = block_component[3 * 8 + k] * s3; - - float const f0 = g0; - float const f1 = g1; - float const f2 = g2; - float const f3 = g3; - float const f4 = g4 - g7; - float const f5 = g5 + g6; - float const f6 = g5 - g6; - float const f7 = g4 + g7; - - float const e0 = f0; - float const e1 = f1; - float const e2 = f2 - f3; - float const e3 = f2 + f3; - float const e4 = f4; - float const e5 = f5 - f7; - float const e6 = f6; - float const e7 = f5 + f7; - float const e8 = f4 + f6; - - float const d0 = e0; - float const d1 = e1; - float const d2 = e2 * m1; - float const d3 = e3; - float const d4 = e4 * m2; - float const d5 = e5 * m3; - float const d6 = e6 * m4; - float const d7 = e7; - float const d8 = e8 * m5; - - float const c0 = d0 + d1; - float const c1 = d0 - d1; - float const c2 = d2 - d3; - float const c3 = d3; - float const c4 = d4 + d8; - float const c5 = d5 + d7; - float const c6 = d6 - d8; - float const c7 = d7; - float const c8 = c5 - c6; - - float const b0 = c0 + c3; - float const b1 = c1 + c2; - float const b2 = c1 - c2; - float const b3 = c0 - c3; - float const b4 = c4 - c8; - float const b5 = c8; - float const b6 = c6 - c7; - float const b7 = c7; - - block_component[0 * 8 + k] = b0 + b7; - block_component[1 * 8 + k] = b1 + b6; - block_component[2 * 8 + k] = b2 + b5; - block_component[3 * 8 + k] = b3 + b4; - block_component[4 * 8 + k] = b3 - b4; - block_component[5 * 8 + k] = b2 - b5; - block_component[6 * 8 + k] = b1 - b6; - block_component[7 * 8 + k] = b0 - b7; - } - for (u32 l = 0; l < 8; ++l) { - float const g0 = block_component[l * 8 + 0] * s0; - float const g1 = block_component[l * 8 + 4] * s4; - float const g2 = block_component[l * 8 + 2] * s2; - float const g3 = block_component[l * 8 + 6] * s6; - float const g4 = block_component[l * 8 + 5] * s5; - float const g5 = block_component[l * 8 + 1] * s1; - float const g6 = block_component[l * 8 + 7] * s7; - float const g7 = block_component[l * 8 + 3] * s3; - - float const f0 = g0; - float const f1 = g1; - float const f2 = g2; - float const f3 = g3; - float const f4 = g4 - g7; - float const f5 = g5 + g6; - float const f6 = g5 - g6; - float const f7 = g4 + g7; - - float const e0 = f0; - float const e1 = f1; - float const e2 = f2 - f3; - float const e3 = f2 + f3; - float const e4 = f4; - float const e5 = f5 - f7; - float const e6 = f6; - float const e7 = f5 + f7; - float const e8 = f4 + f6; - - float const d0 = e0; - float const d1 = e1; - float const d2 = e2 * m1; - float const d3 = e3; - float const d4 = e4 * m2; - float const d5 = e5 * m3; - float const d6 = e6 * m4; - float const d7 = e7; - float const d8 = e8 * m5; - - float const c0 = d0 + d1; - float const c1 = d0 - d1; - float const c2 = d2 - d3; - float const c3 = d3; - float const c4 = d4 + d8; - float const c5 = d5 + d7; - float const c6 = d6 - d8; - float const c7 = d7; - float const c8 = c5 - c6; - - float const b0 = c0 + c3; - float const b1 = c1 + c2; - float const b2 = c1 - c2; - float const b3 = c0 - c3; - float const b4 = c4 - c8; - float const b5 = c8; - float const b6 = c6 - c7; - float const b7 = c7; - - block_component[l * 8 + 0] = b0 + b7; - block_component[l * 8 + 1] = b1 + b6; - block_component[l * 8 + 2] = b2 + b5; - block_component[l * 8 + 3] = b3 + b4; - block_component[l * 8 + 4] = b3 - b4; - block_component[l * 8 + 5] = b2 - b5; - block_component[l * 8 + 6] = b1 - b6; - block_component[l * 8 + 7] = b0 - b7; - } -} - -static void inverse_dct(JPEGLoadingContext const& context, Vector& macroblocks) -{ - for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.sampling_factors.vertical) { - for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.sampling_factors.horizontal) { - for (u32 component_i = 0; component_i < context.components.size(); component_i++) { - auto& component = context.components[component_i]; - for (u8 vfactor_i = 0; vfactor_i < component.sampling_factors.vertical; vfactor_i++) { - for (u8 hfactor_i = 0; hfactor_i < component.sampling_factors.horizontal; hfactor_i++) { - u32 macroblock_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hfactor_i + hcursor); - Macroblock& block = macroblocks[macroblock_index]; - auto* block_component = get_component(block, component_i); - inverse_dct_8x8(block_component); - } - } - } + } else { + cmyk_bitmap = TRY(CMYKBitmap::create_with_size({ static_cast(cinfo.output_width), static_cast(cinfo.output_height) })); + while (cinfo.output_scanline < cinfo.output_height) { + auto* row_ptr = (u8*)cmyk_bitmap->scanline(cinfo.output_scanline); + jpeg_read_scanlines(&cinfo, &row_ptr, 1); } } - // F.2.1.5 - Inverse DCT (IDCT) - auto const level_shift = 1 << (context.frame.precision - 1); - auto const max_value = (1 << context.frame.precision) - 1; - for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.sampling_factors.vertical) { - for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.sampling_factors.horizontal) { - for (u8 vfactor_i = 0; vfactor_i < context.sampling_factors.vertical; ++vfactor_i) { - for (u8 hfactor_i = 0; hfactor_i < context.sampling_factors.horizontal; ++hfactor_i) { - u32 mb_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hcursor + hfactor_i); - for (u8 i = 0; i < 8; ++i) { - for (u8 j = 0; j < 8; ++j) { - - // FIXME: This just truncate all coefficients, it's an easy way to support (read hack) - // 12 bits JPEGs without rewriting all color transformations. - auto const clamp_to_8_bits = [&](u16 color) -> u8 { - if (context.frame.precision == 8) - return static_cast(color); - return static_cast(color >> 4); - }; - - macroblocks[mb_index].r[i * 8 + j] = clamp_to_8_bits(clamp(macroblocks[mb_index].r[i * 8 + j] + level_shift, 0, max_value)); - macroblocks[mb_index].g[i * 8 + j] = clamp_to_8_bits(clamp(macroblocks[mb_index].g[i * 8 + j] + level_shift, 0, max_value)); - macroblocks[mb_index].b[i * 8 + j] = clamp_to_8_bits(clamp(macroblocks[mb_index].b[i * 8 + j] + level_shift, 0, max_value)); - macroblocks[mb_index].k[i * 8 + j] = clamp_to_8_bits(clamp(macroblocks[mb_index].k[i * 8 + j] + level_shift, 0, max_value)); - } - } - } - } - } - } -} - -static void undo_subsampling(JPEGLoadingContext const& context, Vector& macroblocks) -{ - // The first component has sampling factors of context.sampling_factors, while the others - // divide the first component's sampling factors. This is enforced by read_start_of_frame(). - // This function undoes the subsampling by duplicating the values of the smaller components. - // See https://www.w3.org/Graphics/JPEG/itu-t81.pdf, A.2 Order of source image data encoding. - // - // FIXME: Allow more combinations of sampling factors. - // See https://calendar.perfplanet.com/2015/why-arent-your-images-using-chroma-subsampling/ for - // subsampling factors visble on the web. In PDF files, YCCK 2111 and 2112 and CMYK 2111 and 2112 are also present. - for (u32 component_i = 0; component_i < context.components.size(); component_i++) { - auto& component = context.components[component_i]; - if (component.sampling_factors == context.sampling_factors) - continue; - - for (u32 vcursor = 0; vcursor < context.mblock_meta.vcount; vcursor += context.sampling_factors.vertical) { - for (u32 hcursor = 0; hcursor < context.mblock_meta.hcount; hcursor += context.sampling_factors.horizontal) { - u32 const component_block_index = vcursor * context.mblock_meta.hpadded_count + hcursor; - Macroblock& component_block = macroblocks[component_block_index]; - auto* block_component_source = get_component(component_block, component_i); - - // Overflows are intentional. - for (u8 vfactor_i = context.sampling_factors.vertical - 1; vfactor_i < context.sampling_factors.vertical; --vfactor_i) { - for (u8 hfactor_i = context.sampling_factors.horizontal - 1; hfactor_i < context.sampling_factors.horizontal; --hfactor_i) { - u32 macroblock_index = (vcursor + vfactor_i) * context.mblock_meta.hpadded_count + (hfactor_i + hcursor); - Macroblock& block = macroblocks[macroblock_index]; - auto* block_component_destination = get_component(block, component_i); - for (u8 i = 7; i < 8; --i) { - for (u8 j = 7; j < 8; --j) { - u8 const pixel = i * 8 + j; - // The component is 8x8 subsampled 2x2. Upsample its 2x2 4x4 tiles. - u32 const component_pxrow = (i / context.sampling_factors.vertical) + 4 * vfactor_i; - u32 const component_pxcol = (j / context.sampling_factors.horizontal) + 4 * hfactor_i; - u32 const component_pixel = component_pxrow * 8 + component_pxcol; - block_component_destination[pixel] = block_component_source[component_pixel]; - } - } - } - } - } - } - } -} - -static void ycbcr_to_rgb(Vector& macroblocks) -{ - // Conversion from YCbCr to RGB isn't specified in the first JPEG specification but in the JFIF extension: - // See: https://www.itu.int/rec/dologin_pub.asp?lang=f&id=T-REC-T.871-201105-I!!PDF-E&type=items - // 7 - Conversion to and from RGB - for (auto& macroblock : macroblocks) { - auto* y = macroblock.y; - auto* cb = macroblock.cb; - auto* cr = macroblock.cr; - for (u8 i = 0; i < 64; ++i) { - int r = y[i] + 1.402f * (cr[i] - 128); - int g = y[i] - 0.3441f * (cb[i] - 128) - 0.7141f * (cr[i] - 128); - int b = y[i] + 1.772f * (cb[i] - 128); - y[i] = clamp(r, 0, 255); - cb[i] = clamp(g, 0, 255); - cr[i] = clamp(b, 0, 255); - } - } -} - -static void invert_colors_for_adobe_images(JPEGLoadingContext const& context, Vector& macroblocks) -{ - if (!context.color_transform.has_value()) - return; - - // From libjpeg-turbo's libjpeg.txt: - // https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/libjpeg.txt - // CAUTION: it appears that Adobe Photoshop writes inverted data in CMYK JPEG - // files: 0 represents 100% ink coverage, rather than 0% ink as you'd expect. - // This is arguably a bug in Photoshop, but if you need to work with Photoshop - // CMYK files, you will have to deal with it in your application. - for (auto& macroblock : macroblocks) { - for (u8 i = 0; i < 64; ++i) { - macroblock.r[i] = 255 - macroblock.r[i]; - macroblock.g[i] = 255 - macroblock.g[i]; - macroblock.b[i] = 255 - macroblock.b[i]; - macroblock.k[i] = 255 - macroblock.k[i]; - } - } -} - -static void ycck_to_cmyk(Vector& macroblocks) -{ - // 7 - Conversions between colour encodings - // YCCK is obtained from CMYK by converting the CMY channels to YCC channel. - - // To convert back into RGB, we only need the 3 first components, which are baseline YCbCr - ycbcr_to_rgb(macroblocks); - - // RGB to CMY, as mentioned in https://www.smcm.iqfr.csic.es/docs/intel/ipp/ipp_manual/IPPI/ippi_ch15/functn_YCCKToCMYK_JPEG.htm#functn_YCCKToCMYK_JPEG - for (auto& macroblock : macroblocks) { - for (u8 i = 0; i < 64; ++i) { - macroblock.r[i] = 255 - macroblock.r[i]; - macroblock.g[i] = 255 - macroblock.g[i]; - macroblock.b[i] = 255 - macroblock.b[i]; - } - } -} - -static ErrorOr handle_color_transform(JPEGLoadingContext const& context, Vector& macroblocks) -{ - // Note: This is non-standard but some encoder still add the App14 segment for grayscale images. - // So let's ignore the color transform value if we only have one component. - if (context.color_transform.has_value() && context.components.size() != 1) { - // https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.872-201206-I!!PDF-E&type=items - // 6.5.3 - APP14 marker segment for colour encoding - - switch (*context.color_transform) { - case ColorTransform::CmykOrRgb: - if (context.components.size() == 4) { - // Nothing to do here. - } else if (context.components.size() == 3) { - // Note: components.size() == 3 means that we have an RGB image, so no color transformation is needed. - } else { - return Error::from_string_literal("Wrong number of components for CMYK or RGB, aborting."); - } - break; - case ColorTransform::YCbCr: - ycbcr_to_rgb(macroblocks); - break; - case ColorTransform::YCCK: - ycck_to_cmyk(macroblocks); - break; - } - - return {}; + JOCTET* icc_data_ptr = nullptr; + unsigned int icc_data_length = 0; + if (jpeg_read_icc_profile(&cinfo, &icc_data_ptr, &icc_data_length)) { + icc_data.resize(icc_data_length); + memcpy(icc_data.data(), icc_data_ptr, icc_data_length); + free(icc_data_ptr); } - // No App14 segment is present, assuming : - // - 1 components means grayscale - // - 3 components means YCbCr - // - 4 components means CMYK (Nothing to do here). - if (context.components.size() == 3) - ycbcr_to_rgb(macroblocks); + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); - if (context.components.size() == 1) { - // With Cb and Cr being equal to zero, this function assign the Y - // value (luminosity) to R, G and B. Providing a proper conversion - // from grayscale to RGB. - ycbcr_to_rgb(macroblocks); - } + if (cmyk_bitmap && !rgb_bitmap) + rgb_bitmap = TRY(cmyk_bitmap->to_low_quality_rgb()); - return {}; -} - -static ErrorOr compose_bitmap(JPEGLoadingContext& context, Vector const& macroblocks) -{ - context.bitmap = TRY(Bitmap::create(BitmapFormat::BGRx8888, { context.frame.width, context.frame.height })); - - for (u32 y = context.frame.height - 1; y < context.frame.height; y--) { - u32 const block_row = y / 8; - u32 const pixel_row = y % 8; - for (u32 x = 0; x < context.frame.width; x++) { - u32 const block_column = x / 8; - auto& block = macroblocks[block_row * context.mblock_meta.hpadded_count + block_column]; - u32 const pixel_column = x % 8; - u32 const pixel_index = pixel_row * 8 + pixel_column; - Color const color { (u8)block.y[pixel_index], (u8)block.cb[pixel_index], (u8)block.cr[pixel_index] }; - context.bitmap->set_pixel(x, y, color); - } - } - - return {}; -} - -static ErrorOr compose_cmyk_bitmap(JPEGLoadingContext& context, Vector& macroblocks) -{ - if (context.options.cmyk == JPEGDecoderOptions::CMYK::Normal) - invert_colors_for_adobe_images(context, macroblocks); - - context.cmyk_bitmap = TRY(Gfx::CMYKBitmap::create_with_size({ context.frame.width, context.frame.height })); - - for (u32 y = context.frame.height - 1; y < context.frame.height; y--) { - u32 const block_row = y / 8; - u32 const pixel_row = y % 8; - for (u32 x = 0; x < context.frame.width; x++) { - u32 const block_column = x / 8; - auto& block = macroblocks[block_row * context.mblock_meta.hpadded_count + block_column]; - u32 const pixel_column = x % 8; - u32 const pixel_index = pixel_row * 8 + pixel_column; - context.cmyk_bitmap->scanline(y)[x] = { (u8)block.y[pixel_index], (u8)block.cb[pixel_index], (u8)block.cr[pixel_index], (u8)block.k[pixel_index] }; - } - } - - return {}; -} - -static bool is_app_marker(Marker const marker) -{ - return marker >= JPEG_APPN0 && marker <= JPEG_APPN15; -} - -static bool is_miscellaneous_or_table_marker(Marker const marker) -{ - // B.2.4 - Table-specification and miscellaneous marker segment syntax - // See also B.6 - Summary: Figure B.17 – Flow of marker segment - - bool const is_misc = marker == JPEG_COM || marker == JPEG_DRI || is_app_marker(marker); - bool const is_table = marker == JPEG_DQT || marker == JPEG_DAC || marker == JPEG_DHT; - - return is_misc || is_table; -} - -static ErrorOr handle_miscellaneous_or_table(JPEGStream& stream, JPEGLoadingContext& context, Marker const marker) -{ - if (is_app_marker(marker)) { - TRY(read_app_marker(stream, context, marker - JPEG_APPN0)); - return {}; - } - - switch (marker) { - case JPEG_COM: - case JPEG_DAC: - dbgln_if(JPEG_DEBUG, "TODO: implement marker \"{:x}\"", marker); - if (auto result = skip_segment(stream); result.is_error()) { - dbgln_if(JPEG_DEBUG, "Error skipping marker: {:x}!", marker); - return result.release_error(); - } - break; - case JPEG_DHT: - TRY(read_huffman_table(stream, context)); - break; - case JPEG_DQT: - TRY(read_quantization_table(stream, context)); - break; - case JPEG_DRI: - TRY(read_restart_interval(stream, context)); - break; - default: - dbgln("Unexpected marker: {:x}", marker); - VERIFY_NOT_REACHED(); - } - - return {}; -} - -static ErrorOr parse_header(JPEGStream& stream, JPEGLoadingContext& context) -{ - auto marker = TRY(read_marker_at_cursor(stream)); - if (marker != JPEG_SOI) { - dbgln_if(JPEG_DEBUG, "SOI not found: {:x}!", marker); - return Error::from_string_literal("SOI not found"); - } - for (;;) { - marker = TRY(read_marker_at_cursor(stream)); - - if (is_miscellaneous_or_table_marker(marker)) { - TRY(handle_miscellaneous_or_table(stream, context, marker)); - continue; - } - - // Set frame type if the marker marks a new frame. - if (is_frame_marker(marker)) - context.frame.type = static_cast(marker & 0xF); - - switch (marker) { - case JPEG_RST0: - case JPEG_RST1: - case JPEG_RST2: - case JPEG_RST3: - case JPEG_RST4: - case JPEG_RST5: - case JPEG_RST6: - case JPEG_RST7: - case JPEG_SOI: - case JPEG_EOI: - dbgln_if(JPEG_DEBUG, "Unexpected marker {:x}!", marker); - return Error::from_string_literal("Unexpected marker"); - case JPEG_SOF0: - case JPEG_SOF1: - case JPEG_SOF2: - TRY(read_start_of_frame(stream, context)); - context.state = JPEGLoadingContext::FrameDecoded; - return {}; - default: - if (auto result = skip_segment(stream); result.is_error()) { - dbgln_if(JPEG_DEBUG, "Error skipping marker: {:x}!", marker); - return result.release_error(); - } - break; - } - } - - VERIFY_NOT_REACHED(); -} - -static ErrorOr decode_header(JPEGLoadingContext& context) -{ - VERIFY(context.state < JPEGLoadingContext::State::HeaderDecoded); - TRY(parse_header(context.stream, context)); - - if constexpr (JPEG_DEBUG) { - dbgln("Image width: {}", context.frame.width); - dbgln("Image height: {}", context.frame.height); - dbgln("Macroblocks in a row: {}", context.mblock_meta.hpadded_count); - dbgln("Macroblocks in a column: {}", context.mblock_meta.vpadded_count); - dbgln("Macroblock meta padded total: {}", context.mblock_meta.padded_total); - } - - context.state = JPEGLoadingContext::State::HeaderDecoded; - return {}; -} - -static ErrorOr> construct_macroblocks(JPEGLoadingContext& context) -{ - // B.6 - Summary - // See: Figure B.16 – Flow of compressed data syntax - // This function handles the "Multi-scan" loop. - - Vector macroblocks; - TRY(macroblocks.try_resize(context.mblock_meta.padded_total)); - - Marker marker = TRY(read_marker_at_cursor(context.stream)); - while (true) { - if (is_miscellaneous_or_table_marker(marker)) { - TRY(handle_miscellaneous_or_table(context.stream, context, marker)); - } else if (marker == JPEG_SOS) { - TRY(read_start_of_scan(context.stream, context)); - TRY(decode_huffman_stream(context, macroblocks)); - } else if (marker == JPEG_EOI) { - return macroblocks; - } else { - dbgln_if(JPEG_DEBUG, "Unexpected marker {:x}!", marker); - return Error::from_string_literal("Unexpected marker"); - } - - marker = TRY(read_marker_at_cursor(context.stream)); - } -} - -static ErrorOr decode_jpeg(JPEGLoadingContext& context) -{ - auto macroblocks = TRY(construct_macroblocks(context)); - dequantize(context, macroblocks); - inverse_dct(context, macroblocks); - undo_subsampling(context, macroblocks); - TRY(handle_color_transform(context, macroblocks)); - if (context.components.size() == 4) - TRY(compose_cmyk_bitmap(context, macroblocks)); - else - TRY(compose_bitmap(context, macroblocks)); + state = State::Decoded; return {}; } @@ -1991,7 +132,16 @@ JPEGImageDecoderPlugin::~JPEGImageDecoderPlugin() = default; IntSize JPEGImageDecoderPlugin::size() { - return { m_context->frame.width, m_context->frame.height }; + if (m_context->state == JPEGLoadingContext::State::NotDecoded) + (void)frame(0); + + if (m_context->state == JPEGLoadingContext::State::Error) + return {}; + if (m_context->rgb_bitmap) + return m_context->rgb_bitmap->size(); + if (m_context->cmyk_bitmap) + return m_context->cmyk_bitmap->size(); + return {}; } bool JPEGImageDecoderPlugin::sniff(ReadonlyBytes data) @@ -2004,16 +154,7 @@ bool JPEGImageDecoderPlugin::sniff(ReadonlyBytes data) ErrorOr> JPEGImageDecoderPlugin::create(ReadonlyBytes data) { - return create_with_options(data, {}); -} - -ErrorOr> JPEGImageDecoderPlugin::create_with_options(ReadonlyBytes data, JPEGDecoderOptions options) -{ - auto stream = TRY(try_make(data)); - auto context = TRY(JPEGLoadingContext::create(move(stream), options)); - auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) JPEGImageDecoderPlugin(move(context)))); - TRY(decode_header(*plugin->m_context)); - return plugin; + return adopt_own(*new JPEGImageDecoderPlugin(make(data))); } ErrorOr JPEGImageDecoderPlugin::frame(size_t index, Optional) @@ -2024,58 +165,48 @@ ErrorOr JPEGImageDecoderPlugin::frame(size_t index, Option if (m_context->state == JPEGLoadingContext::State::Error) return Error::from_string_literal("JPEGImageDecoderPlugin: Decoding failed"); - if (m_context->state < JPEGLoadingContext::State::BitmapDecoded) { - if (auto result = decode_jpeg(*m_context); result.is_error()) { - m_context->state = JPEGLoadingContext::State::Error; - return result.release_error(); - } - m_context->state = JPEGLoadingContext::State::BitmapDecoded; + if (m_context->state < JPEGLoadingContext::State::Decoded) { + TRY(m_context->decode()); + m_context->state = JPEGLoadingContext::State::Decoded; } - if (m_context->cmyk_bitmap && !m_context->bitmap) - return ImageFrameDescriptor { TRY(m_context->cmyk_bitmap->to_low_quality_rgb()), 0 }; - - return ImageFrameDescriptor { m_context->bitmap, 0 }; + return ImageFrameDescriptor { m_context->rgb_bitmap, 0 }; } Optional JPEGImageDecoderPlugin::metadata() { - if (m_context->exif_metadata) - return *m_context->exif_metadata; return OptionalNone {}; } ErrorOr> JPEGImageDecoderPlugin::icc_data() { - if (m_context->icc_data.has_value()) - return *m_context->icc_data; + if (m_context->state == JPEGLoadingContext::State::NotDecoded) + (void)frame(0); + + if (!m_context->icc_data.is_empty()) + return m_context->icc_data; return OptionalNone {}; } NaturalFrameFormat JPEGImageDecoderPlugin::natural_frame_format() const { - if (m_context->state == JPEGLoadingContext::State::Error) - return NaturalFrameFormat::RGB; - VERIFY(m_context->state >= JPEGLoadingContext::State::HeaderDecoded); - if (m_context->components.size() == 1) - return NaturalFrameFormat::Grayscale; - if (m_context->components.size() == 4) + if (m_context->state == JPEGLoadingContext::State::NotDecoded) + (void)const_cast(*this).frame(0); + + if (m_context->cmyk_bitmap) return NaturalFrameFormat::CMYK; return NaturalFrameFormat::RGB; } ErrorOr> JPEGImageDecoderPlugin::cmyk_frame() { - VERIFY(natural_frame_format() == NaturalFrameFormat::CMYK); - - if (m_context->state < JPEGLoadingContext::State::BitmapDecoded) { - if (auto result = decode_jpeg(*m_context); result.is_error()) { - m_context->state = JPEGLoadingContext::State::Error; - return result.release_error(); - } - m_context->state = JPEGLoadingContext::State::BitmapDecoded; - } + if (m_context->state == JPEGLoadingContext::State::NotDecoded) + (void)frame(0); + if (m_context->state == JPEGLoadingContext::State::Error) + return Error::from_string_literal("JPEGImageDecoderPlugin: Decoding failed"); + if (!m_context->cmyk_bitmap) + return Error::from_string_literal("JPEGImageDecoderPlugin: No CMYK data available"); return *m_context->cmyk_bitmap; } diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGLoader.h b/Userland/Libraries/LibGfx/ImageFormats/JPEGLoader.h index 00a33217f0..b5d51420b5 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGLoader.h +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEGLoader.h @@ -1,37 +1,21 @@ /* - * Copyright (c) 2020, the SerenityOS developers. - * Copyright (c) 2022-2023, Lucas Chollet + * Copyright (c) 2024, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once -#include #include namespace Gfx { struct JPEGLoadingContext; -// For the specification, see: https://www.w3.org/Graphics/JPEG/itu-t81.pdf - -struct JPEGDecoderOptions { - enum class CMYK { - // For standalone jpeg files. - Normal, - - // For jpeg data embedded in PDF files. - PDF, - }; - CMYK cmyk { CMYK::Normal }; -}; - class JPEGImageDecoderPlugin : public ImageDecoderPlugin { public: static bool sniff(ReadonlyBytes); static ErrorOr> create(ReadonlyBytes); - static ErrorOr> create_with_options(ReadonlyBytes, JPEGDecoderOptions = {}); virtual ~JPEGImageDecoderPlugin() override; virtual IntSize size() override; @@ -46,7 +30,7 @@ public: virtual ErrorOr> cmyk_frame() override; private: - JPEGImageDecoderPlugin(NonnullOwnPtr); + explicit JPEGImageDecoderPlugin(NonnullOwnPtr); NonnullOwnPtr m_context; }; diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGShared.h b/Userland/Libraries/LibGfx/ImageFormats/JPEGShared.h deleted file mode 100644 index 62369fbf04..0000000000 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGShared.h +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) 2023, Lucas Chollet - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -// These names are defined in B.1.1.3 - Marker assignments - -#define JPEG_APPN0 0XFFE0 -#define JPEG_APPN1 0XFFE1 -#define JPEG_APPN2 0XFFE2 -#define JPEG_APPN3 0XFFE3 -#define JPEG_APPN4 0XFFE4 -#define JPEG_APPN5 0XFFE5 -#define JPEG_APPN6 0XFFE6 -#define JPEG_APPN7 0XFFE7 -#define JPEG_APPN8 0XFFE8 -#define JPEG_APPN9 0XFFE9 -#define JPEG_APPN10 0XFFEA -#define JPEG_APPN11 0XFFEB -#define JPEG_APPN12 0XFFEC -#define JPEG_APPN13 0XFFED -#define JPEG_APPN14 0xFFEE -#define JPEG_APPN15 0xFFEF - -#define JPEG_RESERVED1 0xFFF1 -#define JPEG_RESERVED2 0xFFF2 -#define JPEG_RESERVED3 0xFFF3 -#define JPEG_RESERVED4 0xFFF4 -#define JPEG_RESERVED5 0xFFF5 -#define JPEG_RESERVED6 0xFFF6 -#define JPEG_RESERVED7 0xFFF7 -#define JPEG_RESERVED8 0xFFF8 -#define JPEG_RESERVED9 0xFFF9 -#define JPEG_RESERVEDA 0xFFFA -#define JPEG_RESERVEDB 0xFFFB -#define JPEG_RESERVEDC 0xFFFC -#define JPEG_RESERVEDD 0xFFFD - -#define JPEG_RST0 0xFFD0 -#define JPEG_RST1 0xFFD1 -#define JPEG_RST2 0xFFD2 -#define JPEG_RST3 0xFFD3 -#define JPEG_RST4 0xFFD4 -#define JPEG_RST5 0xFFD5 -#define JPEG_RST6 0xFFD6 -#define JPEG_RST7 0xFFD7 - -#define JPEG_ZRL 0xF0 - -#define JPEG_DHP 0xFFDE -#define JPEG_EXP 0xFFDF - -#define JPEG_DAC 0XFFCC -#define JPEG_DHT 0XFFC4 -#define JPEG_DQT 0XFFDB -#define JPEG_EOI 0xFFD9 -#define JPEG_DRI 0XFFDD -#define JPEG_SOF0 0XFFC0 -#define JPEG_SOF1 0XFFC1 -#define JPEG_SOF2 0xFFC2 -#define JPEG_SOF15 0xFFCF -#define JPEG_SOI 0XFFD8 -#define JPEG_SOS 0XFFDA -#define JPEG_COM 0xFFFE - -namespace Gfx { - -using Marker = u16; - -constexpr static u8 zigzag_map[64] { - 0, 1, 8, 16, 9, 2, 3, 10, - 17, 24, 32, 25, 18, 11, 4, 5, - 12, 19, 26, 33, 40, 48, 41, 34, - 27, 20, 13, 6, 7, 14, 21, 28, - 35, 42, 49, 56, 57, 50, 43, 36, - 29, 22, 15, 23, 30, 37, 44, 51, - 58, 59, 52, 45, 38, 31, 39, 46, - 53, 60, 61, 54, 47, 55, 62, 63 -}; - -/** - * MCU means group of data units that are coded together. A data unit is an 8x8 - * block of component data. In interleaved scans, number of non-interleaved data - * units of a component C is Ch * Cv, where Ch and Cv represent the horizontal & - * vertical subsampling factors of the component, respectively. A MacroBlock is - * an 8x8 block of RGB values before encoding, and 8x8 block of YCbCr values when - * we're done decoding the huffman stream. - */ -struct Macroblock { - union { - i16 y[64] = { 0 }; - i16 r[64]; - }; - - union { - i16 cb[64] = { 0 }; - i16 g[64]; - }; - - union { - i16 cr[64] = { 0 }; - i16 b[64]; - }; - - i16 k[64] = { 0 }; -}; - -} diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp index 08ff790824..a685ff9878 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.cpp @@ -1,656 +1,113 @@ /* * Copyright (c) 2023, Lucas Chollet + * Copyright (c) 2024, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ -#include "JPEGWriter.h" -#include "JPEGShared.h" -#include "JPEGWriterTables.h" -#include -#include -#include +#include #include #include +#include +#include namespace Gfx { -namespace { +struct MemoryDestinationManager : public jpeg_destination_mgr { + Vector& buffer; + static constexpr size_t BUFFER_SIZE_INCREMENT = 65536; -enum Mode { - RGB, - CMYK, + MemoryDestinationManager(Vector& buffer) + : buffer(buffer) + { + init_destination = [](j_compress_ptr cinfo) { + auto* dest = static_cast(cinfo->dest); + dest->buffer.resize(BUFFER_SIZE_INCREMENT); + dest->next_output_byte = dest->buffer.data(); + dest->free_in_buffer = dest->buffer.capacity(); + }; + + empty_output_buffer = [](j_compress_ptr cinfo) -> boolean { + auto* dest = static_cast(cinfo->dest); + size_t old_size = dest->buffer.size(); + dest->buffer.resize(old_size + BUFFER_SIZE_INCREMENT); + dest->next_output_byte = dest->buffer.data() + old_size; + dest->free_in_buffer = BUFFER_SIZE_INCREMENT; + return TRUE; + }; + + term_destination = [](j_compress_ptr cinfo) { + auto* dest = static_cast(cinfo->dest); + dest->buffer.resize(dest->buffer.size() - dest->free_in_buffer); + }; + + next_output_byte = nullptr; + free_in_buffer = 0; + } }; -// This is basically a BigEndianOutputBitStream, the only difference -// is that it appends 0x00 after each 0xFF when it writes bits. -class JPEGBigEndianOutputBitStream : public Stream { -public: - explicit JPEGBigEndianOutputBitStream(Stream& stream) - : m_stream(stream) - { - } - - virtual ErrorOr read_some(Bytes) override - { - return Error::from_errno(EBADF); - } - - virtual ErrorOr write_some(ReadonlyBytes bytes) override - { - VERIFY(m_bit_offset == 0); - return m_stream.write_some(bytes); - } - - template - ErrorOr write_bits(T value, size_t bit_count) - { - VERIFY(m_bit_offset <= 7); - - while (bit_count > 0) { - u8 const next_bit = (value >> (bit_count - 1)) & 1; - bit_count--; - - m_current_byte <<= 1; - m_current_byte |= next_bit; - m_bit_offset++; - - if (m_bit_offset > 7) { - TRY(m_stream.write_value(m_current_byte)); - if (m_current_byte == 0xFF) - TRY(m_stream.write_value(0)); - - m_bit_offset = 0; - m_current_byte = 0; - } - } - - return {}; - } - - virtual bool is_eof() const override - { - return true; - } - - virtual bool is_open() const override - { - return m_stream.is_open(); - } - - virtual void close() override - { - } - - ErrorOr align_to_byte_boundary(u8 filler = 0x0) - { - if (m_bit_offset == 0) - return {}; - - TRY(write_bits(filler, 8 - m_bit_offset)); - VERIFY(m_bit_offset == 0); - return {}; - } - -private: - Stream& m_stream; - u8 m_current_byte { 0 }; - size_t m_bit_offset { 0 }; -}; - -class JPEGEncodingContext { -public: - JPEGEncodingContext(JPEGBigEndianOutputBitStream output_stream) - : m_bit_stream(move(output_stream)) - { - } - - ErrorOr initialize_mcu(Bitmap const& bitmap) - { - u64 const horizontal_macroblocks = ceil_div(bitmap.width(), 8); - u64 const vertical_macroblocks = ceil_div(bitmap.height(), 8); - TRY(m_macroblocks.try_resize(horizontal_macroblocks * vertical_macroblocks)); - - for (u16 y {}; y < bitmap.height(); ++y) { - u16 const vertical_macroblock_index = y / 8; - u16 const vertical_pixel_offset = y - vertical_macroblock_index * 8; - - for (u16 x {}; x < bitmap.width(); ++x) { - u16 const horizontal_macroblock_index = x / 8; - u16 const horizontal_pixel_offset = x - horizontal_macroblock_index * 8; - - auto& macroblock = m_macroblocks[vertical_macroblock_index * horizontal_macroblocks + horizontal_macroblock_index]; - auto const pixel_offset = vertical_pixel_offset * 8 + horizontal_pixel_offset; - - auto const original_pixel = bitmap.get_pixel(x, y); - - // Conversion from YCbCr to RGB isn't specified in the first JPEG specification but in the JFIF extension: - // See: https://www.itu.int/rec/dologin_pub.asp?lang=f&id=T-REC-T.871-201105-I!!PDF-E&type=items - // 7 - Conversion to and from RGB - auto const y_ = clamp(0.299 * original_pixel.red() + 0.587 * original_pixel.green() + 0.114 * original_pixel.blue(), 0, 255); - auto const cb = clamp(-0.1687 * original_pixel.red() - 0.3313 * original_pixel.green() + 0.5 * original_pixel.blue() + 128, 0, 255); - auto const cr = clamp(0.5 * original_pixel.red() - 0.4187 * original_pixel.green() - 0.0813 * original_pixel.blue() + 128, 0, 255); - - // A.3.1 - Level shift - macroblock.r[pixel_offset] = y_ - 128; - macroblock.g[pixel_offset] = cb - 128; - macroblock.b[pixel_offset] = cr - 128; - } - } - - return {}; - } - - ErrorOr initialize_mcu(CMYKBitmap const& bitmap) - { - u64 const horizontal_macroblocks = ceil_div(bitmap.size().width(), 8); - u64 const vertical_macroblocks = ceil_div(bitmap.size().height(), 8); - TRY(m_macroblocks.try_resize(horizontal_macroblocks * vertical_macroblocks)); - - for (u16 y {}; y < bitmap.size().height(); ++y) { - u16 const vertical_macroblock_index = y / 8; - u16 const vertical_pixel_offset = y - vertical_macroblock_index * 8; - - for (u16 x {}; x < bitmap.size().width(); ++x) { - u16 const horizontal_macroblock_index = x / 8; - u16 const horizontal_pixel_offset = x - horizontal_macroblock_index * 8; - - auto& macroblock = m_macroblocks[vertical_macroblock_index * horizontal_macroblocks + horizontal_macroblock_index]; - auto const pixel_offset = vertical_pixel_offset * 8 + horizontal_pixel_offset; - - auto const original_pixel = bitmap.scanline(y)[x]; - - // To get YCCK, the CMY part is converted to RGB (ignoring the K component), and then the RGB is converted to YCbCr. - // r is `255 - c` (and similar for g/m b/y), but with the Adobe YCCK color transform marker, the CMY - // channels are stored inverted, which cancels out: 255 - (255 - x) == x. - // K is stored as-is (meaning it's inverted once for the color transform). - u8 r = original_pixel.c; - u8 g = original_pixel.m; - u8 b = original_pixel.y; - u8 k = 255 - original_pixel.k; - - // See: https://www.itu.int/rec/dologin_pub.asp?lang=f&id=T-REC-T.871-201105-I!!PDF-E&type=items - // 7 - Conversion to and from RGB - auto const y_ = clamp(0.299 * r + 0.587 * g + 0.114 * b, 0, 255); - auto const cb = clamp(-0.1687 * r - 0.3313 * g + 0.5 * b + 128, 0, 255); - auto const cr = clamp(0.5 * r - 0.4187 * g - 0.0813 * b + 128, 0, 255); - - // A.3.1 - Level shift - macroblock.r[pixel_offset] = y_ - 128; - macroblock.g[pixel_offset] = cb - 128; - macroblock.b[pixel_offset] = cr - 128; - macroblock.k[pixel_offset] = k - 128; - } - } - - return {}; - } - - static Array create_cosine_lookup_table() - { - static constexpr double pi_over_16 = AK::Pi / 16; - - Array table; - - for (u8 u = 0; u < 8; ++u) { - for (u8 x = 0; x < 8; ++x) - table[u * 8 + x] = cos((2 * x + 1) * u * pi_over_16); - } - - return table; - } - - void fdct_and_quantization(Mode mode) - { - static auto cosine_table = create_cosine_lookup_table(); - - for (auto& macroblock : m_macroblocks) { - constexpr double inverse_sqrt_2 = M_SQRT1_2; - - auto const convert_one_component = [&](i16 component[], QuantizationTable const& table) { - Array result {}; - - auto const sum_xy = [&](u8 u, u8 v) { - double sum {}; - for (u8 y {}; y < 8; ++y) { - for (u8 x {}; x < 8; ++x) - sum += component[y * 8 + x] * cosine_table[u * 8 + x] * cosine_table[v * 8 + y]; - } - return sum; - }; - - for (u8 v {}; v < 8; ++v) { - double const cv = v == 0 ? inverse_sqrt_2 : 1; - for (u8 u {}; u < 8; ++u) { - auto const table_index = v * 8 + u; - - double const cu = u == 0 ? inverse_sqrt_2 : 1; - - // A.3.3 - FDCT and IDCT - double const fdct = cu * cv * sum_xy(u, v) / 4; - - // A.3.4 - DCT coefficient quantization - i16 const quantized = round(fdct / table.table[table_index]); - - result[table_index] = quantized; - } - } - - for (u8 i {}; i < result.size(); ++i) - component[i] = result[i]; - }; - - convert_one_component(macroblock.y, m_luminance_quantization_table); - convert_one_component(macroblock.cb, m_chrominance_quantization_table); - convert_one_component(macroblock.cr, m_chrominance_quantization_table); - if (mode == Mode::CMYK) - convert_one_component(macroblock.k, m_luminance_quantization_table); - } - } - - ErrorOr write_huffman_stream(Mode mode) - { - for (auto& macroblock : m_macroblocks) { - TRY(encode_dc(dc_luminance_huffman_table, macroblock.y, 0)); - TRY(encode_ac(ac_luminance_huffman_table, macroblock.y)); - - TRY(encode_dc(dc_chrominance_huffman_table, macroblock.cb, 1)); - TRY(encode_ac(ac_chrominance_huffman_table, macroblock.cb)); - - TRY(encode_dc(dc_chrominance_huffman_table, macroblock.cr, 2)); - TRY(encode_ac(ac_chrominance_huffman_table, macroblock.cr)); - - if (mode == Mode::CMYK) { - TRY(encode_dc(dc_luminance_huffman_table, macroblock.k, 3)); - TRY(encode_ac(ac_luminance_huffman_table, macroblock.k)); - } - } - - TRY(m_bit_stream.align_to_byte_boundary(0xFF)); - - return {}; - } - - void set_luminance_quantization_table(QuantizationTable const& table, int quality) - { - set_quantization_table(m_luminance_quantization_table, table, quality); - } - - void set_chrominance_quantization_table(QuantizationTable const& table, int quality) - { - set_quantization_table(m_chrominance_quantization_table, table, quality); - } - - QuantizationTable const& luminance_quantization_table() const - { - return m_luminance_quantization_table; - } - - QuantizationTable const& chrominance_quantization_table() const - { - return m_chrominance_quantization_table; - } - - OutputHuffmanTable dc_luminance_huffman_table; - OutputHuffmanTable dc_chrominance_huffman_table; - - OutputHuffmanTable ac_luminance_huffman_table; - OutputHuffmanTable ac_chrominance_huffman_table; - -private: - static void set_quantization_table(QuantizationTable& destination, QuantizationTable const& source, int quality) - { - // In order to be compatible with libjpeg-turbo, we use the same coefficients as them. - - quality = clamp(quality, 1, 100); - - if (quality < 50) - quality = 5000 / quality; - else - quality = 200 - quality * 2; - - destination = source; - for (u8 i {}; i < 64; ++i) { - auto const shifted_value = (destination.table[i] * quality + 50) / 100; - destination.table[i] = clamp(shifted_value, 1, 255); - } - } - - ErrorOr write_symbol(OutputHuffmanTable::Symbol symbol) - { - return m_bit_stream.write_bits(symbol.word, symbol.code_length); - } - - ErrorOr encode_dc(OutputHuffmanTable const& dc_table, i16 const component[], u8 component_id) - { - // F.1.2.1.3 - Huffman encoding procedures for DC coefficients - auto diff = component[0] - m_last_dc_values[component_id]; - m_last_dc_values[component_id] = component[0]; - - auto const size = csize(diff); - TRY(write_symbol(dc_table.from_input_byte(size))); - - if (diff < 0) - diff -= 1; - - TRY(m_bit_stream.write_bits(diff, size)); - return {}; - } - - ErrorOr encode_ac(OutputHuffmanTable const& ac_table, i16 const component[]) - { - { - // F.2 - Procedure for sequential encoding of AC coefficients with Huffman coding - u32 k {}; - u32 r {}; - - while (k < 63) { - k++; - - auto coefficient = component[zigzag_map[k]]; - if (coefficient == 0) { - if (k == 63) { - TRY(write_symbol(ac_table.from_input_byte(0x00))); - break; - } - r += 1; - continue; - } - - while (r > 15) { - TRY(write_symbol(ac_table.from_input_byte(0xF0))); - r -= 16; - } - - { - // F.3 - Sequential encoding of a non-zero AC coefficient - auto const ssss = csize(coefficient); - auto const rs = (r << 4) + ssss; - TRY(write_symbol(ac_table.from_input_byte(rs))); - - if (coefficient < 0) - coefficient -= 1; - - TRY(m_bit_stream.write_bits(coefficient, ssss)); - } - - r = 0; - } - } - return {}; - } - - static u8 csize(i16 coefficient) - { - VERIFY(coefficient >= -2047 && coefficient <= 2047); - - if (coefficient == 0) - return 0; - - return floor(log2(abs(coefficient))) + 1; - } - - QuantizationTable m_luminance_quantization_table {}; - QuantizationTable m_chrominance_quantization_table {}; - - Vector m_macroblocks {}; - Array m_last_dc_values {}; - - JPEGBigEndianOutputBitStream m_bit_stream; -}; - -ErrorOr add_start_of_image(Stream& stream) +ErrorOr JPEGWriter::encode_impl(Stream& stream, auto const& bitmap, Options const& options, ColorSpace color_space) { - TRY(stream.write_value>(JPEG_SOI)); + struct jpeg_compress_struct cinfo { }; + struct jpeg_error_mgr jerr { }; + + cinfo.err = jpeg_std_error(&jerr); + + jpeg_create_compress(&cinfo); + + Vector buffer; + MemoryDestinationManager dest_manager(buffer); + cinfo.dest = &dest_manager; + + cinfo.image_width = bitmap.size().width(); + cinfo.image_height = bitmap.size().height(); + cinfo.input_components = 4; + + switch (color_space) { + case ColorSpace::RGB: + cinfo.in_color_space = JCS_EXT_BGRX; + break; + case ColorSpace::CMYK: + cinfo.in_color_space = JCS_CMYK; + break; + default: + VERIFY_NOT_REACHED(); + } + + jpeg_set_defaults(&cinfo); + jpeg_set_colorspace(&cinfo, JCS_YCbCr); + jpeg_set_quality(&cinfo, options.quality, TRUE); + + if (options.icc_data.has_value()) { + jpeg_write_icc_profile(&cinfo, options.icc_data->data(), options.icc_data->size()); + } + + jpeg_start_compress(&cinfo, TRUE); + + Vector row_buffer; + row_buffer.resize(bitmap.size().width() * 4); + + while (cinfo.next_scanline < cinfo.image_height) { + auto const* row_ptr = reinterpret_cast(bitmap.scanline(cinfo.next_scanline)); + JSAMPROW row_pointer = (JSAMPROW)row_ptr; + jpeg_write_scanlines(&cinfo, &row_pointer, 1); + } + + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + TRY(stream.write_until_depleted(buffer)); return {}; } -ErrorOr add_end_of_image(Stream& stream) -{ - TRY(stream.write_value>(JPEG_EOI)); - return {}; -} - -ErrorOr add_icc_data(Stream& stream, ReadonlyBytes icc_data) -{ - // https://www.color.org/technotes/ICC-Technote-ProfileEmbedding.pdf, JFIF section - constexpr StringView icc_chunk_name = "ICC_PROFILE\0"sv; - - // One JPEG chunk is at most 65535 bytes long, which includes the size of the 2-byte - // "length" field. This leaves 65533 bytes for the actual data. One ICC chunk needs - // 12 bytes for the "ICC_PROFILE\0" app id and then one byte each for the current - // sequence number and the number of ICC chunks. This leaves 65519 bytes for the - // ICC data. - constexpr size_t icc_chunk_header_size = 2 + icc_chunk_name.length() + 1 + 1; - constexpr size_t max_chunk_size = 65535 - icc_chunk_header_size; - static_assert(max_chunk_size == 65519); - - constexpr size_t max_number_of_icc_chunks = 255; // Chunk IDs are stored in an u8 and start at 1. - constexpr size_t max_icc_data_size = max_chunk_size * max_number_of_icc_chunks; - - // "The 1-byte chunk count limits the size of embeddable profiles to 16 707 345 bytes."" - static_assert(max_icc_data_size == 16'707'345); - - if (icc_data.size() > max_icc_data_size) - return Error::from_string_view("JPEGWriter: icc data too large for jpeg format"sv); - - size_t const number_of_icc_chunks = AK::ceil_div(icc_data.size(), max_chunk_size); - for (size_t chunk_id = 1; chunk_id <= number_of_icc_chunks; ++chunk_id) { - size_t const chunk_size = min(icc_data.size(), max_chunk_size); - - TRY(stream.write_value>(JPEG_APPN2)); - TRY(stream.write_value>(icc_chunk_header_size + chunk_size)); - TRY(stream.write_until_depleted(icc_chunk_name.bytes())); - TRY(stream.write_value(chunk_id)); - TRY(stream.write_value(number_of_icc_chunks)); - TRY(stream.write_until_depleted(icc_data.slice(0, chunk_size))); - icc_data = icc_data.slice(chunk_size); - } - VERIFY(icc_data.is_empty()); - return {}; -} - -ErrorOr add_frame_header(Stream& stream, JPEGEncodingContext const& context, IntSize size, Mode mode) -{ - // B.2.2 - Frame header syntax - TRY(stream.write_value>(JPEG_SOF0)); - - u16 const Nf = mode == Mode::CMYK ? 4 : 3; - - // Lf = 8 + 3 × Nf - TRY(stream.write_value>(8 + 3 * Nf)); - - // P - TRY(stream.write_value(8)); - - // Y - TRY(stream.write_value>(size.height())); - - // X - TRY(stream.write_value>(size.width())); - - // Nf - TRY(stream.write_value(Nf)); - - // Encode Nf components - for (u8 i {}; i < Nf; ++i) { - // Ci - TRY(stream.write_value(i + 1)); - - // Hi and Vi - TRY(stream.write_value((1 << 4) | 1)); - - // Tqi - TRY(stream.write_value((i == 0 || i == 3 ? context.luminance_quantization_table() : context.chrominance_quantization_table()).id)); - } - - return {}; -} - -ErrorOr add_ycck_color_transform_header(Stream& stream) -{ - // T-REC-T.872-201206-I!!PDF-E.pdf, 6.5.3 APP14 marker segment for colour encoding - TRY(stream.write_value>(JPEG_APPN14)); - TRY(stream.write_value>(14)); - - TRY(stream.write_until_depleted("Adobe\0"sv.bytes())); - - // These values are ignored. - TRY(stream.write_value(0x64)); - TRY(stream.write_value>(0x0000)); - TRY(stream.write_value>(0x0000)); - - // YCCK - TRY(stream.write_value(0x2)); - return {}; -} - -ErrorOr add_quantization_table(Stream& stream, QuantizationTable const& table) -{ - // B.2.4.1 - Quantization table-specification syntax - TRY(stream.write_value>(JPEG_DQT)); - - // Lq = 2 + 1 * 65 - TRY(stream.write_value>(2 + 65)); - - // Pq and Tq - TRY(stream.write_value((0 << 4) | table.id)); - - for (u8 i = 0; i < 64; ++i) - TRY(stream.write_value(table.table[zigzag_map[i]])); - - return {}; -} - -ErrorOr, 16>> sort_symbols_per_size(OutputHuffmanTable const& table) -{ - // JPEG only allows symbol with a size less than or equal to 16. - Vector, 16> output {}; - TRY(output.try_resize(16)); - - for (auto const& symbol : table.table) - TRY(output[symbol.code_length - 1].try_append(symbol.input_byte)); - - return output; -} - -ErrorOr add_huffman_table(Stream& stream, OutputHuffmanTable const& table) -{ - // B.2.4.2 - Huffman table-specification syntax - TRY(stream.write_value>(JPEG_DHT)); - - // Lh - TRY(stream.write_value>(2 + 17 + table.table.size())); - - // Tc and Th - TRY(stream.write_value(table.id)); - - auto const vectorized_table = TRY(sort_symbols_per_size(table)); - for (auto const& symbol_vector : vectorized_table) - TRY(stream.write_value(symbol_vector.size())); - - for (auto const& symbol_vector : vectorized_table) { - for (auto symbol : symbol_vector) - TRY(stream.write_value(symbol)); - } - - return {}; -} - -ErrorOr add_scan_header(Stream& stream, Mode mode) -{ - // B.2.3 - Scan header syntax - TRY(stream.write_value>(JPEG_SOS)); - - u16 const Ns = mode == Mode::CMYK ? 4 : 3; - - // Ls - 6 + 2 × Ns - TRY(stream.write_value>(6 + 2 * Ns)); - - // Ns - TRY(stream.write_value(Ns)); - - // Encode Ns components - for (u8 i {}; i < Ns; ++i) { - // Csj - TRY(stream.write_value(i + 1)); - - // Tdj and Taj - // We're using 0 for luminance and 1 for chrominance - u8 const huffman_identifier = i == 0 || i == 3 ? 0 : 1; - TRY(stream.write_value((huffman_identifier << 4) | huffman_identifier)); - } - - // Ss - TRY(stream.write_value(0)); - - // Se - TRY(stream.write_value(63)); - - // Ah and Al - TRY(stream.write_value((0 << 4) | 0)); - - return {}; -} - -ErrorOr add_headers(Stream& stream, JPEGEncodingContext& context, JPEGWriter::Options const& options, IntSize size, Mode mode) -{ - context.set_luminance_quantization_table(s_default_luminance_quantization_table, options.quality); - context.set_chrominance_quantization_table(s_default_chrominance_quantization_table, options.quality); - - context.dc_luminance_huffman_table = s_default_dc_luminance_huffman_table; - context.dc_chrominance_huffman_table = s_default_dc_chrominance_huffman_table; - - context.ac_luminance_huffman_table = s_default_ac_luminance_huffman_table; - context.ac_chrominance_huffman_table = s_default_ac_chrominance_huffman_table; - - TRY(add_start_of_image(stream)); - - if (options.icc_data.has_value()) - TRY(add_icc_data(stream, options.icc_data.value())); - - if (mode == Mode::CMYK) - TRY(add_ycck_color_transform_header(stream)); - TRY(add_frame_header(stream, context, size, mode)); - - TRY(add_quantization_table(stream, context.luminance_quantization_table())); - TRY(add_quantization_table(stream, context.chrominance_quantization_table())); - - TRY(add_huffman_table(stream, context.dc_luminance_huffman_table)); - TRY(add_huffman_table(stream, context.dc_chrominance_huffman_table)); - TRY(add_huffman_table(stream, context.ac_luminance_huffman_table)); - TRY(add_huffman_table(stream, context.ac_chrominance_huffman_table)); - - TRY(add_scan_header(stream, mode)); - return {}; -} - -ErrorOr add_image(Stream& stream, JPEGEncodingContext& context, Mode mode) -{ - context.fdct_and_quantization(mode); - TRY(context.write_huffman_stream(mode)); - TRY(add_end_of_image(stream)); - return {}; -} - -} - ErrorOr JPEGWriter::encode(Stream& stream, Bitmap const& bitmap, Options const& options) { - JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } }; - TRY(add_headers(stream, context, options, bitmap.size(), Mode::RGB)); - TRY(context.initialize_mcu(bitmap)); - TRY(add_image(stream, context, Mode::RGB)); - return {}; + return encode_impl(stream, bitmap, options, ColorSpace::RGB); } ErrorOr JPEGWriter::encode(Stream& stream, CMYKBitmap const& bitmap, Options const& options) { - JPEGEncodingContext context { JPEGBigEndianOutputBitStream { stream } }; - TRY(add_headers(stream, context, options, bitmap.size(), Mode::CMYK)); - TRY(context.initialize_mcu(bitmap)); - TRY(add_image(stream, context, Mode::CMYK)); - return {}; + return encode_impl(stream, bitmap, options, ColorSpace::CMYK); } } diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h index 4608623dda..f89d8fcf7e 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h +++ b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriter.h @@ -24,6 +24,12 @@ public: static ErrorOr encode(Stream&, CMYKBitmap const&, Options const& = {}); private: + enum class ColorSpace { + RGB, + CMYK, + }; + static ErrorOr encode_impl(Stream&, auto const&, Options const&, ColorSpace); + JPEGWriter() = delete; }; diff --git a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriterTables.h b/Userland/Libraries/LibGfx/ImageFormats/JPEGWriterTables.h deleted file mode 100644 index 26a5c52f34..0000000000 --- a/Userland/Libraries/LibGfx/ImageFormats/JPEGWriterTables.h +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Copyright (c) 2023, Lucas Chollet - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include - -namespace Gfx { - -struct QuantizationTable { - Array table {}; - u8 id {}; -}; - -// K.1 - Quantization tables for luminance and chrominance components - -// clang-format off -constexpr static QuantizationTable s_default_luminance_quantization_table { - .table = { - 16, 11, 10, 16, 124, 140, 151, 161, - 12, 12, 14, 19, 126, 158, 160, 155, - 14, 13, 16, 24, 140, 157, 169, 156, - 14, 17, 22, 29, 151, 187, 180, 162, - 18, 22, 37, 56, 168, 109, 103, 177, - 24, 35, 55, 64, 181, 104, 113, 192, - 49, 64, 78, 87, 103, 121, 120, 101, - 72, 92, 95, 98, 112, 100, 103, 199, - }, - .id = 0, -}; - -constexpr static QuantizationTable s_default_chrominance_quantization_table { - .table = { - 17, 18, 24, 47, 99, 99, 99, 99, - 18, 21, 26, 66, 99, 99, 99, 99, - 24, 26, 56, 99, 99, 99, 99, 99, - 47, 66, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - }, - .id = 1, -}; - -constexpr static QuantizationTable s_dummy_quantization_table { - .table = { - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - }, - .id = 1, -}; - -// clang-format on - -struct OutputHuffmanTable { - struct Symbol { - u8 input_byte {}; - u8 code_length {}; - u16 word {}; - }; - - Symbol from_input_byte(u8 input_byte) const - { - for (auto symbol : table) { - if (symbol.input_byte == input_byte) - return symbol; - } - VERIFY_NOT_REACHED(); - } - - Vector table {}; - u8 id {}; -}; - -static OutputHuffmanTable s_default_dc_luminance_huffman_table { - .table = { - { 0, 2, 0b00 }, - { 1, 3, 0b010 }, - { 2, 3, 0b011 }, - { 3, 3, 0b100 }, - { 4, 3, 0b101 }, - { 5, 3, 0b110 }, - { 6, 4, 0b1110 }, - { 7, 5, 0b11110 }, - { 8, 6, 0b111110 }, - { 9, 7, 0b1111110 }, - { 10, 8, 0b11111110 }, - { 11, 9, 0b111111110 }, - }, - .id = (0 << 4) | 0, -}; - -static OutputHuffmanTable s_default_dc_chrominance_huffman_table { - .table = { - { 0, 2, 0b00 }, - { 1, 2, 0b01 }, - { 2, 2, 0b10 }, - { 3, 3, 0b110 }, - { 4, 4, 0b1110 }, - { 5, 5, 0b11110 }, - { 6, 6, 0b111110 }, - { 7, 7, 0b1111110 }, - { 8, 8, 0b11111110 }, - { 9, 9, 0b111111110 }, - { 10, 10, 0b1111111110 }, - { 11, 11, 0b11111111110 }, - }, - .id = (0 << 4) | 1, -}; - -static OutputHuffmanTable s_default_ac_luminance_huffman_table { - .table = { - { 0x01, 2, 0b00 }, - { 0x02, 2, 0b01 }, - { 0x03, 3, 0b100 }, - { 0x00, 4, 0b1010 }, - { 0x04, 4, 0b1011 }, - { 0x11, 4, 0b1100 }, - { 0x05, 5, 0b11010 }, - { 0x12, 5, 0b11011 }, - { 0x21, 5, 0b11100 }, - { 0x31, 6, 0b111010 }, - { 0x41, 6, 0b111011 }, - { 0x06, 7, 0b1111000 }, - { 0x13, 7, 0b1111001 }, - { 0x51, 7, 0b1111010 }, - { 0x61, 7, 0b1111011 }, - { 0x07, 8, 0b11111000 }, - { 0x22, 8, 0b11111001 }, - { 0x71, 8, 0b11111010 }, - { 0x14, 9, 0b111110110 }, - { 0x32, 9, 0b111110111 }, - { 0x81, 9, 0b111111000 }, - { 0x91, 9, 0b111111001 }, - { 0xA1, 9, 0b111111010 }, - { 0x08, 10, 0b1111110110 }, - { 0x23, 10, 0b1111110111 }, - { 0x42, 10, 0b1111111000 }, - { 0xB1, 10, 0b1111111001 }, - { 0xC1, 10, 0b1111111010 }, - { 0x15, 11, 0b11111110110 }, - { 0x52, 11, 0b11111110111 }, - { 0xD1, 11, 0b11111111000 }, - { 0xF0, 11, 0b11111111001 }, - { 0x24, 12, 0b111111110100 }, - { 0x33, 12, 0b111111110101 }, - { 0x62, 12, 0b111111110110 }, - { 0x72, 12, 0b111111110111 }, - { 0x82, 15, 0b111111111000000 }, - { 0x09, 16, 0b1111111110000010 }, - { 0x0A, 16, 0b1111111110000011 }, - { 0x16, 16, 0b1111111110000100 }, - { 0x17, 16, 0b1111111110000101 }, - { 0x18, 16, 0b1111111110000110 }, - { 0x19, 16, 0b1111111110000111 }, - { 0x1A, 16, 0b1111111110001000 }, - { 0x25, 16, 0b1111111110001001 }, - { 0x26, 16, 0b1111111110001010 }, - { 0x27, 16, 0b1111111110001011 }, - { 0x28, 16, 0b1111111110001100 }, - { 0x29, 16, 0b1111111110001101 }, - { 0x2A, 16, 0b1111111110001110 }, - { 0x34, 16, 0b1111111110001111 }, - { 0x35, 16, 0b1111111110010000 }, - { 0x36, 16, 0b1111111110010001 }, - { 0x37, 16, 0b1111111110010010 }, - { 0x38, 16, 0b1111111110010011 }, - { 0x39, 16, 0b1111111110010100 }, - { 0x3A, 16, 0b1111111110010101 }, - { 0x43, 16, 0b1111111110010110 }, - { 0x44, 16, 0b1111111110010111 }, - { 0x45, 16, 0b1111111110011000 }, - { 0x46, 16, 0b1111111110011001 }, - { 0x47, 16, 0b1111111110011010 }, - { 0x48, 16, 0b1111111110011011 }, - { 0x49, 16, 0b1111111110011100 }, - { 0x4A, 16, 0b1111111110011101 }, - { 0x53, 16, 0b1111111110011110 }, - { 0x54, 16, 0b1111111110011111 }, - { 0x55, 16, 0b1111111110100000 }, - { 0x56, 16, 0b1111111110100001 }, - { 0x57, 16, 0b1111111110100010 }, - { 0x58, 16, 0b1111111110100011 }, - { 0x59, 16, 0b1111111110100100 }, - { 0x5A, 16, 0b1111111110100101 }, - { 0x63, 16, 0b1111111110100110 }, - { 0x64, 16, 0b1111111110100111 }, - { 0x65, 16, 0b1111111110101000 }, - { 0x66, 16, 0b1111111110101001 }, - { 0x67, 16, 0b1111111110101010 }, - { 0x68, 16, 0b1111111110101011 }, - { 0x69, 16, 0b1111111110101100 }, - { 0x6A, 16, 0b1111111110101101 }, - { 0x73, 16, 0b1111111110101110 }, - { 0x74, 16, 0b1111111110101111 }, - { 0x75, 16, 0b1111111110110000 }, - { 0x76, 16, 0b1111111110110001 }, - { 0x77, 16, 0b1111111110110010 }, - { 0x78, 16, 0b1111111110110011 }, - { 0x79, 16, 0b1111111110110100 }, - { 0x7A, 16, 0b1111111110110101 }, - { 0x83, 16, 0b1111111110110110 }, - { 0x84, 16, 0b1111111110110111 }, - { 0x85, 16, 0b1111111110111000 }, - { 0x86, 16, 0b1111111110111001 }, - { 0x87, 16, 0b1111111110111010 }, - { 0x88, 16, 0b1111111110111011 }, - { 0x89, 16, 0b1111111110111100 }, - { 0x8A, 16, 0b1111111110111101 }, - { 0x92, 16, 0b1111111110111110 }, - { 0x93, 16, 0b1111111110111111 }, - { 0x94, 16, 0b1111111111000000 }, - { 0x95, 16, 0b1111111111000001 }, - { 0x96, 16, 0b1111111111000010 }, - { 0x97, 16, 0b1111111111000011 }, - { 0x98, 16, 0b1111111111000100 }, - { 0x99, 16, 0b1111111111000101 }, - { 0x9A, 16, 0b1111111111000110 }, - { 0xA2, 16, 0b1111111111000111 }, - { 0xA3, 16, 0b1111111111001000 }, - { 0xA4, 16, 0b1111111111001001 }, - { 0xA5, 16, 0b1111111111001010 }, - { 0xA6, 16, 0b1111111111001011 }, - { 0xA7, 16, 0b1111111111001100 }, - { 0xA8, 16, 0b1111111111001101 }, - { 0xA9, 16, 0b1111111111001110 }, - { 0xAA, 16, 0b1111111111001111 }, - { 0xB2, 16, 0b1111111111010000 }, - { 0xB3, 16, 0b1111111111010001 }, - { 0xB4, 16, 0b1111111111010010 }, - { 0xB5, 16, 0b1111111111010011 }, - { 0xB6, 16, 0b1111111111010100 }, - { 0xB7, 16, 0b1111111111010101 }, - { 0xB8, 16, 0b1111111111010110 }, - { 0xB9, 16, 0b1111111111010111 }, - { 0xBA, 16, 0b1111111111011000 }, - { 0xC2, 16, 0b1111111111011001 }, - { 0xC3, 16, 0b1111111111011010 }, - { 0xC4, 16, 0b1111111111011011 }, - { 0xC5, 16, 0b1111111111011100 }, - { 0xC6, 16, 0b1111111111011101 }, - { 0xC7, 16, 0b1111111111011110 }, - { 0xC8, 16, 0b1111111111011111 }, - { 0xC9, 16, 0b1111111111100000 }, - { 0xCA, 16, 0b1111111111100001 }, - { 0xD2, 16, 0b1111111111100010 }, - { 0xD3, 16, 0b1111111111100011 }, - { 0xD4, 16, 0b1111111111100100 }, - { 0xD5, 16, 0b1111111111100101 }, - { 0xD6, 16, 0b1111111111100110 }, - { 0xD7, 16, 0b1111111111100111 }, - { 0xD8, 16, 0b1111111111101000 }, - { 0xD9, 16, 0b1111111111101001 }, - { 0xDA, 16, 0b1111111111101010 }, - { 0xE1, 16, 0b1111111111101011 }, - { 0xE2, 16, 0b1111111111101100 }, - { 0xE3, 16, 0b1111111111101101 }, - { 0xE4, 16, 0b1111111111101110 }, - { 0xE5, 16, 0b1111111111101111 }, - { 0xE6, 16, 0b1111111111110000 }, - { 0xE7, 16, 0b1111111111110001 }, - { 0xE8, 16, 0b1111111111110010 }, - { 0xE9, 16, 0b1111111111110011 }, - { 0xEA, 16, 0b1111111111110100 }, - { 0xF1, 16, 0b1111111111110101 }, - { 0xF2, 16, 0b1111111111110110 }, - { 0xF3, 16, 0b1111111111110111 }, - { 0xF4, 16, 0b1111111111111000 }, - { 0xF5, 16, 0b1111111111111001 }, - { 0xF6, 16, 0b1111111111111010 }, - { 0xF7, 16, 0b1111111111111011 }, - { 0xF8, 16, 0b1111111111111100 }, - { 0xF9, 16, 0b1111111111111101 }, - { 0xFA, 16, 0b1111111111111110 }, - }, - .id = (1 << 4) | 0, -}; - -static OutputHuffmanTable s_default_ac_chrominance_huffman_table { - .table = { - { 0x00, 2, 0b00 }, - { 0x01, 2, 0b01 }, - { 0x02, 3, 0b100 }, - { 0x03, 4, 0b1010 }, - { 0x11, 4, 0b1011 }, - { 0x04, 5, 0b11000 }, - { 0x05, 5, 0b11001 }, - { 0x21, 5, 0b11010 }, - { 0x31, 5, 0b11011 }, - { 0x06, 6, 0b111000 }, - { 0x12, 6, 0b111001 }, - { 0x41, 6, 0b111010 }, - { 0x51, 6, 0b111011 }, - { 0x07, 7, 0b1111000 }, - { 0x61, 7, 0b1111001 }, - { 0x71, 7, 0b1111010 }, - { 0x13, 8, 0b11110110 }, - { 0x22, 8, 0b11110111 }, - { 0x32, 8, 0b11111000 }, - { 0x81, 8, 0b11111001 }, - { 0x08, 9, 0b111110100 }, - { 0x14, 9, 0b111110101 }, - { 0x42, 9, 0b111110110 }, - { 0x91, 9, 0b111110111 }, - { 0xA1, 9, 0b111111000 }, - { 0xB1, 9, 0b111111001 }, - { 0xC1, 9, 0b111111010 }, - { 0x09, 10, 0b1111110110 }, - { 0x23, 10, 0b1111110111 }, - { 0x33, 10, 0b1111111000 }, - { 0x52, 10, 0b1111111001 }, - { 0xF0, 10, 0b1111111010 }, - { 0x15, 11, 0b11111110110 }, - { 0x62, 11, 0b11111110111 }, - { 0x72, 11, 0b11111111000 }, - { 0xD1, 11, 0b11111111001 }, - { 0x0A, 12, 0b111111110100 }, - { 0x16, 12, 0b111111110101 }, - { 0x24, 12, 0b111111110110 }, - { 0x34, 12, 0b111111110111 }, - { 0xE1, 14, 0b11111111100000 }, - { 0x25, 15, 0b111111111000010 }, - { 0xF1, 15, 0b111111111000011 }, - { 0x17, 16, 0b1111111110001000 }, - { 0x18, 16, 0b1111111110001001 }, - { 0x19, 16, 0b1111111110001010 }, - { 0x1A, 16, 0b1111111110001011 }, - { 0x26, 16, 0b1111111110001100 }, - { 0x27, 16, 0b1111111110001101 }, - { 0x28, 16, 0b1111111110001110 }, - { 0x29, 16, 0b1111111110001111 }, - { 0x2A, 16, 0b1111111110010000 }, - { 0x35, 16, 0b1111111110010001 }, - { 0x36, 16, 0b1111111110010010 }, - { 0x37, 16, 0b1111111110010011 }, - { 0x38, 16, 0b1111111110010100 }, - { 0x39, 16, 0b1111111110010101 }, - { 0x3A, 16, 0b1111111110010110 }, - { 0x43, 16, 0b1111111110010111 }, - { 0x44, 16, 0b1111111110011000 }, - { 0x45, 16, 0b1111111110011001 }, - { 0x46, 16, 0b1111111110011010 }, - { 0x47, 16, 0b1111111110011011 }, - { 0x48, 16, 0b1111111110011100 }, - { 0x49, 16, 0b1111111110011101 }, - { 0x4A, 16, 0b1111111110011110 }, - { 0x53, 16, 0b1111111110011111 }, - { 0x54, 16, 0b1111111110100000 }, - { 0x55, 16, 0b1111111110100001 }, - { 0x56, 16, 0b1111111110100010 }, - { 0x57, 16, 0b1111111110100011 }, - { 0x58, 16, 0b1111111110100100 }, - { 0x59, 16, 0b1111111110100101 }, - { 0x5A, 16, 0b1111111110100110 }, - { 0x63, 16, 0b1111111110100111 }, - { 0x64, 16, 0b1111111110101000 }, - { 0x65, 16, 0b1111111110101001 }, - { 0x66, 16, 0b1111111110101010 }, - { 0x67, 16, 0b1111111110101011 }, - { 0x68, 16, 0b1111111110101100 }, - { 0x69, 16, 0b1111111110101101 }, - { 0x6A, 16, 0b1111111110101110 }, - { 0x73, 16, 0b1111111110101111 }, - { 0x74, 16, 0b1111111110110000 }, - { 0x75, 16, 0b1111111110110001 }, - { 0x76, 16, 0b1111111110110010 }, - { 0x77, 16, 0b1111111110110011 }, - { 0x78, 16, 0b1111111110110100 }, - { 0x79, 16, 0b1111111110110101 }, - { 0x7A, 16, 0b1111111110110110 }, - { 0x82, 16, 0b1111111110110111 }, - { 0x83, 16, 0b1111111110111000 }, - { 0x84, 16, 0b1111111110111001 }, - { 0x85, 16, 0b1111111110111010 }, - { 0x86, 16, 0b1111111110111011 }, - { 0x87, 16, 0b1111111110111100 }, - { 0x88, 16, 0b1111111110111101 }, - { 0x89, 16, 0b1111111110111110 }, - { 0x8A, 16, 0b1111111110111111 }, - { 0x92, 16, 0b1111111111000000 }, - { 0x93, 16, 0b1111111111000001 }, - { 0x94, 16, 0b1111111111000010 }, - { 0x95, 16, 0b1111111111000011 }, - { 0x96, 16, 0b1111111111000100 }, - { 0x97, 16, 0b1111111111000101 }, - { 0x98, 16, 0b1111111111000110 }, - { 0x99, 16, 0b1111111111000111 }, - { 0x9A, 16, 0b1111111111001000 }, - { 0xA2, 16, 0b1111111111001001 }, - { 0xA3, 16, 0b1111111111001010 }, - { 0xA4, 16, 0b1111111111001011 }, - { 0xA5, 16, 0b1111111111001100 }, - { 0xA6, 16, 0b1111111111001101 }, - { 0xA7, 16, 0b1111111111001110 }, - { 0xA8, 16, 0b1111111111001111 }, - { 0xA9, 16, 0b1111111111010000 }, - { 0xAA, 16, 0b1111111111010001 }, - { 0xB2, 16, 0b1111111111010010 }, - { 0xB3, 16, 0b1111111111010011 }, - { 0xB4, 16, 0b1111111111010100 }, - { 0xB5, 16, 0b1111111111010101 }, - { 0xB6, 16, 0b1111111111010110 }, - { 0xB7, 16, 0b1111111111010111 }, - { 0xB8, 16, 0b1111111111011000 }, - { 0xB9, 16, 0b1111111111011001 }, - { 0xBA, 16, 0b1111111111011010 }, - { 0xC2, 16, 0b1111111111011011 }, - { 0xC3, 16, 0b1111111111011100 }, - { 0xC4, 16, 0b1111111111011101 }, - { 0xC5, 16, 0b1111111111011110 }, - { 0xC6, 16, 0b1111111111011111 }, - { 0xC7, 16, 0b1111111111100000 }, - { 0xC8, 16, 0b1111111111100001 }, - { 0xC9, 16, 0b1111111111100010 }, - { 0xCA, 16, 0b1111111111100011 }, - { 0xD2, 16, 0b1111111111100100 }, - { 0xD3, 16, 0b1111111111100101 }, - { 0xD4, 16, 0b1111111111100110 }, - { 0xD5, 16, 0b1111111111100111 }, - { 0xD6, 16, 0b1111111111101000 }, - { 0xD7, 16, 0b1111111111101001 }, - { 0xD8, 16, 0b1111111111101010 }, - { 0xD9, 16, 0b1111111111101011 }, - { 0xDA, 16, 0b1111111111101100 }, - { 0xE2, 16, 0b1111111111101101 }, - { 0xE3, 16, 0b1111111111101110 }, - { 0xE4, 16, 0b1111111111101111 }, - { 0xE5, 16, 0b1111111111110000 }, - { 0xE6, 16, 0b1111111111110001 }, - { 0xE7, 16, 0b1111111111110010 }, - { 0xE8, 16, 0b1111111111110011 }, - { 0xE9, 16, 0b1111111111110100 }, - { 0xEA, 16, 0b1111111111110101 }, - { 0xF2, 16, 0b1111111111110110 }, - { 0xF3, 16, 0b1111111111110111 }, - { 0xF4, 16, 0b1111111111111000 }, - { 0xF5, 16, 0b1111111111111001 }, - { 0xF6, 16, 0b1111111111111010 }, - { 0xF7, 16, 0b1111111111111011 }, - { 0xF8, 16, 0b1111111111111100 }, - { 0xF9, 16, 0b1111111111111101 }, - { 0xFA, 16, 0b1111111111111110 }, - }, - .id = (1 << 4) | 1, -}; - -} diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index c64aefbb26..9670ebd54c 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -10,7 +10,6 @@ set(CMD_SOURCES js.cpp lzcat.cpp tar.cpp - test-jpeg-roundtrip.cpp ttfdisasm.cpp unzip.cpp wasm.cpp @@ -43,7 +42,6 @@ target_link_libraries(isobmff PRIVATE LibGfx) target_link_libraries(js PRIVATE LibCrypto LibJS LibLine LibLocale LibTextCodec) target_link_libraries(lzcat PRIVATE LibCompress) target_link_libraries(tar PRIVATE LibArchive LibCompress LibFileSystem) -target_link_libraries(test-jpeg-roundtrip PRIVATE LibGfx) target_link_libraries(ttfdisasm PRIVATE LibGfx) target_link_libraries(unzip PRIVATE LibArchive LibCompress LibCrypto LibFileSystem) target_link_libraries(wasm PRIVATE LibFileSystem LibJS LibLine LibWasm) diff --git a/Userland/Utilities/image.cpp b/Userland/Utilities/image.cpp index a174beec69..09623f1566 100644 --- a/Userland/Utilities/image.cpp +++ b/Userland/Utilities/image.cpp @@ -157,17 +157,6 @@ static ErrorOr save_image(LoadedImage& image, StringView out_path, bool pp return Core::OutputBufferedFile::create(move(output_stream)); }; - if (image.bitmap.has>()) { - auto& cmyk_frame = image.bitmap.get>(); - - if (out_path.ends_with(".jpg"sv, CaseSensitivity::CaseInsensitive) || out_path.ends_with(".jpeg"sv, CaseSensitivity::CaseInsensitive)) { - TRY(Gfx::JPEGWriter::encode(*TRY(stream()), *cmyk_frame, { .icc_data = image.icc_data, .quality = jpeg_quality })); - return {}; - } - - return Error::from_string_view("Can save CMYK bitmaps only as .jpg, convert to RGB first with --convert-to-color-profile"sv); - } - auto& frame = image.bitmap.get>(); if (out_path.ends_with(".gif"sv, CaseSensitivity::CaseInsensitive)) { diff --git a/Userland/Utilities/test-jpeg-roundtrip.cpp b/Userland/Utilities/test-jpeg-roundtrip.cpp deleted file mode 100644 index 3b2971b9a0..0000000000 --- a/Userland/Utilities/test-jpeg-roundtrip.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2024, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include -#include -#include -#include - -struct Fixpoint { - Gfx::Color fixpoint; - int number_of_iterations {}; -}; - -static ErrorOr compute_fixpoint(Gfx::Color start_color) -{ - auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, { 8, 8 })); - bitmap->fill(start_color); - - int number_of_iterations = 1; - Color last_color = start_color; - while (true) { - AllocatingMemoryStream stream; - TRY(Gfx::JPEGWriter::encode(stream, *bitmap)); - auto data = TRY(stream.read_until_eof()); - auto plugin_decoder = TRY(Gfx::JPEGImageDecoderPlugin::create(data)); - auto frame = TRY(plugin_decoder->frame(0)); - - Color current_color = frame.image->get_pixel(4, 4); - if (current_color == last_color) - break; - - ++number_of_iterations; - last_color = current_color; - bitmap = *frame.image; - } - return Fixpoint { last_color, number_of_iterations }; -} - -static ErrorOr perceived_distance_in_sRGB(Gfx::Color a, Gfx::Color b) -{ - auto sRGB = TRY(Gfx::ICC::sRGB()); - - Array array_a { a.red(), a.green(), a.blue() }; - Array array_b { b.red(), b.green(), b.blue() }; - - return DeltaE(TRY(sRGB->to_lab(array_a)), TRY(sRGB->to_lab(array_b))); -} - -struct Stats { - float max_delta {}; - int max_number_of_iterations {}; -}; - -static ErrorOr test(Gfx::Color color, Stats& stats) -{ - auto fixpoint = TRY(compute_fixpoint(color)); - - float perceived_distance = TRY(perceived_distance_in_sRGB(color, fixpoint.fixpoint)); - - outln("color {} converges to {} after saving {} times, delta {}", color, fixpoint.fixpoint, fixpoint.number_of_iterations, perceived_distance); - - stats.max_delta = max(stats.max_delta, perceived_distance); - stats.max_number_of_iterations = max(stats.max_number_of_iterations, fixpoint.number_of_iterations); - - return {}; -} - -ErrorOr serenity_main(Main::Arguments) -{ - Stats stats; - - TRY(test(Gfx::Color::Red, stats)); - TRY(test(Gfx::Color::Green, stats)); - TRY(test(Gfx::Color::Blue, stats)); - - TRY(test(Gfx::Color::LightBlue, stats)); - - TRY(test(Gfx::Color::MidRed, stats)); - TRY(test(Gfx::Color::MidGreen, stats)); - TRY(test(Gfx::Color::MidBlue, stats)); - - TRY(test(Gfx::Color::DarkRed, stats)); - TRY(test(Gfx::Color::DarkGreen, stats)); - TRY(test(Gfx::Color::DarkBlue, stats)); - - TRY(test(Gfx::Color::Cyan, stats)); - TRY(test(Gfx::Color::Magenta, stats)); - TRY(test(Gfx::Color::Yellow, stats)); - - TRY(test(Gfx::Color::Black, stats)); - TRY(test(Gfx::Color::DarkGray, stats)); - TRY(test(Gfx::Color::MidGray, stats)); - TRY(test(Gfx::Color::LightGray, stats)); - TRY(test(Gfx::Color::White, stats)); - - outln(); - outln("max delta {}, max number of iterations {}", stats.max_delta, stats.max_number_of_iterations); - - return 0; -} diff --git a/vcpkg.json b/vcpkg.json index a54ffc64a4..ca6d0202f1 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -6,6 +6,7 @@ "platform": "linux | freebsd | openbsd" }, "icu", + "libjpeg-turbo", "sqlite3", "woff2" ], @@ -18,6 +19,10 @@ "name": "icu", "version": "74.2#1" }, + { + "name": "libjpeg-turbo", + "version": "3.0.2" + }, { "name": "sqlite3", "version": "3.45.3"