From 64912d4d0246cfbcd0a4e83508db6740d4fa900f Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Sat, 25 Nov 2023 16:34:56 -0500 Subject: [PATCH] LibGfx/TIFF: Add support for images with CCITT3 1D compression This compression (tag Compression=2) is not very popular on its own, but a base to implement CCITT3 2D and CCITT4 compressions. As the format has no real benefits, it is quite hard to find an app that accepts tho encode that for you. So I used the following program that calls `libtiff` directly: ```cpp #include #include #include #include // An array containing 0 and 1 of length width * height. extern std::vector array; int main() { // From: https://stackoverflow.com/a/34257789 TIFF *image = TIFFOpen("input.tif", "w"); int const width = 400; int const height = 300; TIFFSetField(image, TIFFTAG_IMAGEWIDTH, width); TIFFSetField(image, TIFFTAG_IMAGELENGTH, height); TIFFSetField(image, TIFFTAG_PHOTOMETRIC, 0); TIFFSetField(image, TIFFTAG_COMPRESSION, COMPRESSION_CCITTRLE); TIFFSetField(image, TIFFTAG_BITSPERSAMPLE, 1); TIFFSetField(image, TIFFTAG_SAMPLESPERPIXEL, 1); TIFFSetField(image, TIFFTAG_ROWSPERSTRIP, 1); std::vector scan_line(width / 8 + 8, 0); int count = 0; for (int i = 0; i < height; i++) { std::fill(scan_line.begin(), scan_line.end(), 0); for (int x = 0; x < width; ++x) { uint8_t eight_pixels = scan_line.at(x / 8); eight_pixels = eight_pixels << 1; eight_pixels |= !array.at(i * width + x); scan_line.at(x / 8) = eight_pixels; } int bytes = int(width / 8.0 + 0.5); if (TIFFWriteScanline(image, scan_line.data(), i, bytes) != 1) std::cerr << "Something went wrong\n"; } TIFFClose(image); } ``` --- Tests/LibGfx/TestImageDecoder.cpp | 12 + Tests/LibGfx/test-inputs/tiff/ccitt3_1d.tiff | Bin 0 -> 4116 bytes Userland/Libraries/LibGfx/CMakeLists.txt | 3 +- .../LibGfx/ImageFormats/CCITTDecoder.cpp | 333 ++++++++++++++++++ .../LibGfx/ImageFormats/CCITTDecoder.h | 26 ++ .../LibGfx/ImageFormats/TIFFLoader.cpp | 15 + 6 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 Tests/LibGfx/test-inputs/tiff/ccitt3_1d.tiff create mode 100644 Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.cpp create mode 100644 Userland/Libraries/LibGfx/ImageFormats/CCITTDecoder.h 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 0000000000000000000000000000000000000000..40b883343e653ee981d98d87706f6167694544d2 GIT binary patch literal 4116 zcmebD)M9XAV_+~}G1#C6^WP@118X~^nGd~*5Kd+^Gf-o1X4HL&zC9x# zCzyBdR0hAK_U%R&&TZh04vn_mIhB#qadTQjwC&C*%xvkp4$-zdyO^bmr!9!K-Pzgv zzAp+S)!BNNFRCF*cV{Q(CMbiyN@goaK=4oYcZ=C>T7F~cdBCBPNoLSM@ z7sUW#^{1XgX4uY}G4ajL(+-iOGA7?Jp4I@;b@PoRL_3uj8nfio0O7(hgbWmL-?=}+WyY>0p{RBii`+&8o<96)7k z<#TKRi)>|AVnAZ-?45T-rbE>hiM~2#0+^3MPw7*w-r@1h=ShKTwWjT4OBpbw833Y| zSi&e3-6?XHWLAKvGcwCmbf<`2l3Au=J5>%!osroC;=w499U$I;z8GZc4oL8}^wS3_ z#t8{L2}GcHWLE_0~?ghz`(#D$jHI~Qk%lfz#s}02dNiBVv9r7Y+(cGU|;~5 zF^chr0|x^G0~Z4W0}mAQLTMPE8!FGqz`%fxL3#%Yqq_y<2V@L#HweSjf#hIpYSZXp zKx^}n?PF(PU?3N>FfcG+!>~MqO^%g;fdPg=*^Cqn(+8rlnN5xu$Sh=x%?y}-20IOk zS5P_y1_lKN1_mVt1_l)d z1_m_-1_liV1_mt#1_m7l1_nI_1_lEL1_mPr1_l!b1_m<*1_lcT1_mnz1_m1j1_nC@ z1_lQP1_mbv1_l=f1_n0<1_loX1_mz%1_mDn1_nO{28I9z28JL828Iv@28J*O28IX* z28JjG28I|028K8W28IL%28JXC28I*{28J{S28Ij<28JvK28J9428KKa28IF#28JRA z28I#_28J>Q28Id-28JpI28J3228KEY28IR(28JdE28I>}28K2U28Ip>28J#M28JF6 z28KRRJ;A`hFo}VIVG08S!!!m4h8YYD46_&*7#1-wFsxxt{Lpvh_Lq8(}!*oUlhWU&P49giA7}hf~Fl=XJVA#*d zz;K+Af#EzO1H*Mj28R2L3=Gd185rI(GBA8+WMKHu$iTqP#K6GM#K0iV#K0iW#K54= z#K54>#K2&|#K7Rl#K7Ri#J~{D#J~{C#K4fw#K2I<#K2I^#K6$T#K16-iGg7j69dCy zCI*JJObiU$m>3xLGchonU}9jn$i%>Ki;02ZF%tvB8zu&ZuS^UK|CksU*qIp^1eh5Z zB$*i)l$aS9w3!(gOqdxMY?&Dt+?W{{{FxaTBA6K%5}6qovX~hdikTT0s+k!Wnwc3G zx|tamCNncI%wc9=Sjx=6u#TC5VLLMe!vSUnhLg+;440T07;ZB&Fg#&qV0g>S!0?Tk zf#E+h0|N&O1A`z71A`O`1A{UP1A`6=1A{3G1A`q41A{vY1A{*c14B3q14BFu14B9s z14BLw14B6r14BIv14A1N14Azh1H%*+28P)z3=E4{7#LQvFfeRjVPM$K!oaYPg@NHH z3j@O$76yjPEDQ{{SQr={vM?~bU}0c*!@|JufrWwL3kw6o4;BW7KcEcH%D}+H%D}+F z%D}+E%D}+G%D^DN%D^DP%D^DO%D^DY%D|w+%D|w`%D|w@%D`aE%D`aB%D`aH%D~{t z%D~{w%D@oF%D@oL%D@oI%D|A!%D|Ax%D|A%%D_;<%D_;`%D_;^%D~XX%D~XZ%D~XY z%D~Xa%D^y*m4RUzD+9wURtAQ7tPBi`SQ!|Wu`)2MVr5`h$I8I4iIst28!H3DE>;GH zeXI-&hgcaHj<6>3|wps z3_NTM418=13<7Kn3_@%S3?gg{3}S2y3=(V%3{q?i3^HsC403D?3<_)v3`%Sa3@U64 z3~Fo)3>s_<3|edq3_5HK40>z~3It*3|4Fm3^r^G40db` J3=V7z3;?} + * + * 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 {