diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index a30626bca8..a2bf270d00 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -406,6 +406,18 @@ TEST_CASE(test_tiff_uncompressed) EXPECT_EQ(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::Red); } +TEST_CASE(test_tiff_ccitt3_1d) +{ + auto file = MUST(Core::MappedFile::map(TEST_INPUT("tiff/ccitt3_1d.tiff"sv))); + EXPECT(Gfx::TIFFImageDecoderPlugin::sniff(file->bytes())); + auto plugin_decoder = TRY_OR_FAIL(Gfx::TIFFImageDecoderPlugin::create(file->bytes())); + + auto frame = TRY_OR_FAIL(expect_single_frame_of_size(*plugin_decoder, { 400, 300 })); + + EXPECT_EQ(frame.image->get_pixel(0, 0), Gfx::Color::NamedColor::White); + EXPECT_EQ(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::Black); +} + TEST_CASE(test_tiff_lzw) { auto file = MUST(Core::MappedFile::map(TEST_INPUT("tiff/lzw.tiff"sv))); diff --git a/Tests/LibGfx/test-inputs/tiff/ccitt3_1d.tiff b/Tests/LibGfx/test-inputs/tiff/ccitt3_1d.tiff new file mode 100644 index 0000000000..40b883343e Binary files /dev/null and b/Tests/LibGfx/test-inputs/tiff/ccitt3_1d.tiff differ diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 99f8a06cf0..c4f2e8a20a 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -37,7 +37,7 @@ set(SOURCES ImageFormats/BMPLoader.cpp ImageFormats/BMPWriter.cpp ImageFormats/BooleanDecoder.cpp - ImageFormats/TIFFLoader.cpp + ImageFormats/CCITTDecoder.cpp ImageFormats/DDSLoader.cpp ImageFormats/GIFLoader.cpp ImageFormats/ICOLoader.cpp @@ -57,6 +57,7 @@ set(SOURCES ImageFormats/QOILoader.cpp ImageFormats/QOIWriter.cpp ImageFormats/TGALoader.cpp + ImageFormats/TIFFLoader.cpp ImageFormats/TinyVGLoader.cpp ImageFormats/WebPLoader.cpp ImageFormats/WebPLoaderLossless.cpp diff --git a/Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.cpp b/Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.cpp new file mode 100644 index 0000000000..5a7715b01b --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.cpp @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2023, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace Gfx::CCITT { + +namespace { + +struct Code { + u16 run_length {}; + u8 code_length {}; + u16 code {}; +}; + +// Table 2/T.4 – Terminating codes +constexpr Array white_terminating_codes = { + Code { 0, 8, 0b00110101 }, + Code { 1, 6, 0b000111 }, + Code { 2, 4, 0b0111 }, + Code { 3, 4, 0b1000 }, + Code { 4, 4, 0b1011 }, + Code { 5, 4, 0b1100 }, + Code { 6, 4, 0b1110 }, + Code { 7, 4, 0b1111 }, + Code { 8, 5, 0b10011 }, + Code { 9, 5, 0b10100 }, + Code { 10, 5, 0b00111 }, + Code { 11, 5, 0b01000 }, + Code { 12, 6, 0b001000 }, + Code { 13, 6, 0b000011 }, + Code { 14, 6, 0b110100 }, + Code { 15, 6, 0b110101 }, + Code { 16, 6, 0b101010 }, + Code { 17, 6, 0b101011 }, + Code { 18, 7, 0b0100111 }, + Code { 19, 7, 0b0001100 }, + Code { 20, 7, 0b0001000 }, + Code { 21, 7, 0b0010111 }, + Code { 22, 7, 0b0000011 }, + Code { 23, 7, 0b0000100 }, + Code { 24, 7, 0b0101000 }, + Code { 25, 7, 0b0101011 }, + Code { 26, 7, 0b0010011 }, + Code { 27, 7, 0b0100100 }, + Code { 28, 7, 0b0011000 }, + Code { 29, 8, 0b00000010 }, + Code { 30, 8, 0b00000011 }, + Code { 31, 8, 0b00011010 }, + Code { 32, 8, 0b00011011 }, + Code { 33, 8, 0b00010010 }, + Code { 34, 8, 0b00010011 }, + Code { 35, 8, 0b00010100 }, + Code { 36, 8, 0b00010101 }, + Code { 37, 8, 0b00010110 }, + Code { 38, 8, 0b00010111 }, + Code { 39, 8, 0b00101000 }, + Code { 40, 8, 0b00101001 }, + Code { 41, 8, 0b00101010 }, + Code { 42, 8, 0b00101011 }, + Code { 43, 8, 0b00101100 }, + Code { 44, 8, 0b00101101 }, + Code { 45, 8, 0b00000100 }, + Code { 46, 8, 0b00000101 }, + Code { 47, 8, 0b00001010 }, + Code { 48, 8, 0b00001011 }, + Code { 49, 8, 0b01010010 }, + Code { 50, 8, 0b01010011 }, + Code { 51, 8, 0b01010100 }, + Code { 52, 8, 0b01010101 }, + Code { 53, 8, 0b00100100 }, + Code { 54, 8, 0b00100101 }, + Code { 55, 8, 0b01011000 }, + Code { 56, 8, 0b01011001 }, + Code { 57, 8, 0b01011010 }, + Code { 58, 8, 0b01011011 }, + Code { 59, 8, 0b01001010 }, + Code { 60, 8, 0b01001011 }, + Code { 61, 8, 0b00110010 }, + Code { 62, 8, 0b00110011 }, + Code { 63, 8, 0b00110100 }, +}; + +// Table 2/T.4 – Terminating codes +constexpr Array black_terminating_codes = { + Code { 0, 10, 0b0000110111 }, + Code { 1, 3, 0b010 }, + Code { 2, 2, 0b11 }, + Code { 3, 2, 0b10 }, + Code { 4, 3, 0b011 }, + Code { 5, 4, 0b0011 }, + Code { 6, 4, 0b0010 }, + Code { 7, 5, 0b00011 }, + Code { 8, 6, 0b000101 }, + Code { 9, 6, 0b000100 }, + Code { 10, 7, 0b0000100 }, + Code { 11, 7, 0b0000101 }, + Code { 12, 7, 0b0000111 }, + Code { 13, 8, 0b00000100 }, + Code { 14, 8, 0b00000111 }, + Code { 15, 9, 0b000011000 }, + Code { 16, 10, 0b0000010111 }, + Code { 17, 10, 0b0000011000 }, + Code { 18, 10, 0b0000001000 }, + Code { 19, 11, 0b00001100111 }, + Code { 20, 11, 0b00001101000 }, + Code { 21, 11, 0b00001101100 }, + Code { 22, 11, 0b00000110111 }, + Code { 23, 11, 0b00000101000 }, + Code { 24, 11, 0b00000010111 }, + Code { 25, 11, 0b00000011000 }, + Code { 26, 12, 0b000011001010 }, + Code { 27, 12, 0b000011001011 }, + Code { 28, 12, 0b000011001100 }, + Code { 29, 12, 0b000011001101 }, + Code { 30, 12, 0b000001101000 }, + Code { 31, 12, 0b000001101001 }, + Code { 32, 12, 0b000001101010 }, + Code { 33, 12, 0b000001101011 }, + Code { 34, 12, 0b000011010010 }, + Code { 35, 12, 0b000011010011 }, + Code { 36, 12, 0b000011010100 }, + Code { 37, 12, 0b000011010101 }, + Code { 38, 12, 0b000011010110 }, + Code { 39, 12, 0b000011010111 }, + Code { 40, 12, 0b000001101100 }, + Code { 41, 12, 0b000001101101 }, + Code { 42, 12, 0b000011011010 }, + Code { 43, 12, 0b000011011011 }, + Code { 44, 12, 0b000001010100 }, + Code { 45, 12, 0b000001010101 }, + Code { 46, 12, 0b000001010110 }, + Code { 47, 12, 0b000001010111 }, + Code { 48, 12, 0b000001100100 }, + Code { 49, 12, 0b000001100101 }, + Code { 50, 12, 0b000001010010 }, + Code { 51, 12, 0b000001010011 }, + Code { 52, 12, 0b000000100100 }, + Code { 53, 12, 0b000000110111 }, + Code { 54, 12, 0b000000111000 }, + Code { 55, 12, 0b000000100111 }, + Code { 56, 12, 0b000000101000 }, + Code { 57, 12, 0b000001011000 }, + Code { 58, 12, 0b000001011001 }, + Code { 59, 12, 0b000000101011 }, + Code { 60, 12, 0b000000101100 }, + Code { 61, 12, 0b000001011010 }, + Code { 62, 12, 0b000001100110 }, + Code { 63, 12, 0b000001100111 }, +}; + +// Table 3a/T.4 – Make-up codes +constexpr Array white_make_up_codes = { + Code { 64, 5, 0b11011 }, + Code { 128, 5, 0b10010 }, + Code { 192, 6, 0b010111 }, + Code { 256, 7, 0b0110111 }, + Code { 320, 8, 0b00110110 }, + Code { 384, 8, 0b00110111 }, + Code { 448, 8, 0b01100100 }, + Code { 512, 8, 0b01100101 }, + Code { 576, 8, 0b01101000 }, + Code { 640, 8, 0b01100111 }, + Code { 704, 9, 0b011001100 }, + Code { 768, 9, 0b011001101 }, + Code { 832, 9, 0b011010010 }, + Code { 896, 9, 0b011010011 }, + Code { 960, 9, 0b011010100 }, + Code { 1024, 9, 0b011010101 }, + Code { 1088, 9, 0b011010110 }, + Code { 1152, 9, 0b011010111 }, + Code { 1216, 9, 0b011011000 }, + Code { 1280, 9, 0b011011001 }, + Code { 1344, 9, 0b011011010 }, + Code { 1408, 9, 0b011011011 }, + Code { 1472, 9, 0b010011000 }, + Code { 1536, 9, 0b010011001 }, + Code { 1600, 9, 0b010011010 }, + Code { 1664, 6, 0b011000 }, + Code { 1728, 9, 0b010011011 }, + Code { 0xE0L, 12, 0b000000000001 }, +}; + +// Table 3a/T.4 – Make-up codes +constexpr Array black_make_up_codes = { + Code { 64, 10, 0b0000001111 }, + Code { 128, 12, 0b000011001000 }, + Code { 192, 12, 0b000011001001 }, + Code { 256, 12, 0b000001011011 }, + Code { 320, 12, 0b000000110011 }, + Code { 384, 12, 0b000000110100 }, + Code { 448, 12, 0b000000110101 }, + Code { 512, 13, 0b0000001101100 }, + Code { 576, 13, 0b0000001101101 }, + Code { 640, 13, 0b0000001001010 }, + Code { 704, 13, 0b0000001001011 }, + Code { 768, 13, 0b0000001001100 }, + Code { 832, 13, 0b0000001001101 }, + Code { 896, 13, 0b0000001110010 }, + Code { 960, 13, 0b0000001110011 }, + Code { 1024, 13, 0b0000001110100 }, + Code { 1088, 13, 0b0000001110101 }, + Code { 1152, 13, 0b0000001110110 }, + Code { 1216, 13, 0b0000001110111 }, + Code { 1280, 13, 0b0000001010010 }, + Code { 1344, 13, 0b0000001010011 }, + Code { 1408, 13, 0b0000001010100 }, + Code { 1472, 13, 0b0000001010101 }, + Code { 1536, 13, 0b0000001011010 }, + Code { 1600, 13, 0b0000001011011 }, + Code { 1664, 13, 0b0000001100100 }, + Code { 1728, 13, 0b0000001100101 }, + Code { 0xE0L, 11, 0b00000000000 }, +}; + +// Table 3b/T.4 – Make-up codes +constexpr Array common_make_up_codes = { + Code { 1792, 11, 0b00000001000 }, + Code { 1856, 11, 0b00000001100 }, + Code { 1920, 11, 0b00000001101 }, + Code { 1984, 12, 0b000000010010 }, + Code { 2048, 12, 0b000000010011 }, + Code { 2112, 12, 0b000000010100 }, + Code { 2176, 12, 0b000000010101 }, + Code { 2240, 12, 0b000000010110 }, + Code { 2304, 12, 0b000000010111 }, + Code { 2368, 12, 0b000000011100 }, + Code { 2432, 12, 0b000000011101 }, + Code { 2496, 12, 0b000000011110 }, + Code { 2560, 12, 0b000000011111 }, +}; + +template +Optional get_code_from_table(Array const& array, u16 code_word, u8 code_size) +{ + for (auto const& code : array) { + if (code.code_length == code_size && code.code == code_word) + return code; + } + return OptionalNone {}; +} + +Optional get_markup_code(Color color, u16 code_word, u8 code_size) +{ + if (auto maybe_value = get_code_from_table(common_make_up_codes, code_word, code_size); maybe_value.has_value()) + return maybe_value.value(); + + if (color == Color::NamedColor::White) + return get_code_from_table(white_make_up_codes, code_word, code_size); + return get_code_from_table(black_make_up_codes, code_word, code_size); +} + +Optional get_terminal_code(Color color, u16 code_word, u8 code_size) +{ + if (color == Color::NamedColor::White) + return get_code_from_table(white_terminating_codes, code_word, code_size); + return get_code_from_table(black_terminating_codes, code_word, code_size); +} + +} + +ErrorOr decode_ccitt3_1d(ReadonlyBytes bytes, u32 image_width, u32 image_height) +{ + auto strip_stream = make(bytes); + auto bit_stream = make(MaybeOwned(*strip_stream)); + + ByteBuffer decoded_bytes = TRY(ByteBuffer::create_zeroed(ceil_div(image_width * image_height, 8))); + auto output_stream = make(decoded_bytes.bytes()); + auto decoded_bits = make(MaybeOwned(*output_stream)); + auto const ccitt_white = Color::NamedColor::White; + auto const ccitt_black = Color::NamedColor::Black; + + Color current_color { ccitt_black }; + u32 run_length {}; + + u32 column = 0; + + while (!bit_stream->is_eof() || run_length > 0) { + if (run_length > 0) { + run_length--; + TRY(decoded_bits->write_bits(current_color == ccitt_white ? 0u : 1u, 1)); + + ++column; + if (column == image_width) { + column = 0; + bit_stream->align_to_byte_boundary(); + TRY(decoded_bits->align_to_byte_boundary()); + } + continue; + } + + current_color = current_color == ccitt_white ? ccitt_black : ccitt_white; + + if (column == 0) { + current_color = ccitt_white; + } + + u8 size {}; + u16 potential_code {}; + while (size < 14) { + potential_code <<= 1; + potential_code |= TRY(bit_stream->read_bit()); + size++; + + if (auto const maybe_markup = get_markup_code(current_color, potential_code, size); maybe_markup.has_value()) { + run_length += maybe_markup->run_length; + // OK, let's reset the loop to read a terminal code now + size = 0; + potential_code = 0; + } else if (auto const maybe_terminal = get_terminal_code(current_color, potential_code, size); maybe_terminal.has_value()) { + run_length += maybe_terminal->run_length; + break; + } + } + + if (size == 14) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid CCITT code"); + + if (column + run_length > image_width) + return Error::from_string_literal("TIFFImageDecoderPlugin: CCITT codes encode for more that a line"); + } + + return decoded_bytes; +} + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.h b/Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.h new file mode 100644 index 0000000000..b4cd56b93f --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Gfx::CCITT { + +// You can find a great overview of CCITT compression schemes here: +// https://www.fileformat.info/mirror/egff/ch09_05.htm + +// The CCITT3 specification is accessible at this page: +// https://www.itu.int/rec/T-REC-T.4/en + +// The unidimensional scheme is originally described in: +// 4.1 One-dimensional coding scheme +// However, this function implements the TIFF variant (see TIFFLoader.h for a spec link), +// differences are detailed in section: +// Section 10: Modified Huffman Compression +ErrorOr decode_ccitt3_1d(ReadonlyBytes bytes, u32 image_width, u32 image_height); + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp index 185fb54af7..18bc15af46 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Gfx { @@ -161,6 +162,20 @@ private: TRY(loop_over_pixels(move(identity))); break; } + case Compression::CCITT: { + if (m_metadata.bits_per_sample()->size() > 1) + return Error::from_string_literal("TIFFImageDecoderPlugin: CCITT image with BitsPerSample greater than one, aborting..."); + + ByteBuffer decoded_bytes {}; + auto decode_ccitt_1D_strip = [&](u32 num_bytes) -> ErrorOr { + auto const encoded_bytes = TRY(m_stream->read_in_place(num_bytes)); + decoded_bytes = TRY(CCITT::decode_ccitt3_1d(encoded_bytes, *m_metadata.image_width(), *m_metadata.rows_per_strip())); + return decoded_bytes; + }; + + TRY(loop_over_pixels(move(decode_ccitt_1D_strip))); + break; + } case Compression::LZW: { ByteBuffer decoded_bytes {}; auto decode_lzw_strip = [&](u32 num_bytes) -> ErrorOr {