From 67522fab2e160b4e01d42fcd34215c21233cae8e Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Sun, 17 Dec 2023 23:35:21 -0500 Subject: [PATCH] LibGfx/TIFF: Add support for RGBPalette images TIFF images with the PhotometricInterpretation tag set to RGBPalette are based on indexed colors instead of explicitly describing the color for each pixel. Let's add support for them. The test case was generated with GIMP using the Indexed image mode after adding an alpha layer. Not all decoders are able to open this image, but GIMP can. --- Tests/LibGfx/TestImageDecoder.cpp | 12 +++++++++ .../test-inputs/tiff/rgb_palette_alpha.tiff | Bin 0 -> 9096 bytes .../LibGfx/ImageFormats/TIFFLoader.cpp | 24 ++++++++++++++++++ Userland/Libraries/LibGfx/TIFFGenerator.py | 1 + 4 files changed, 37 insertions(+) create mode 100644 Tests/LibGfx/test-inputs/tiff/rgb_palette_alpha.tiff diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index eb2c5e4ffa..b449d4ea2f 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -489,6 +489,18 @@ TEST_CASE(test_tiff_rgb_alpha) EXPECT_EQ(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::Red); } +TEST_CASE(test_tiff_palette_alpha) +{ + auto file = MUST(Core::MappedFile::map(TEST_INPUT("tiff/rgb_palette_alpha.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).alpha(), 0); + EXPECT_EQ(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::Red); +} + TEST_CASE(test_tiff_16_bits) { auto file = MUST(Core::MappedFile::map(TEST_INPUT("tiff/16_bits.tiff"sv))); diff --git a/Tests/LibGfx/test-inputs/tiff/rgb_palette_alpha.tiff b/Tests/LibGfx/test-inputs/tiff/rgb_palette_alpha.tiff new file mode 100644 index 0000000000000000000000000000000000000000..bbd37e86c0f8d1cb83f1c6adb35e0069682e6a68 GIT binary patch literal 9096 zcmebD)MDUZU|DKC~jwGSir=<;2_EX2A{+k z7$jsF7y^_S7}Qu97%W&B7|a7SU%qhr7)GNtMOJiVg_w)@=Fw!$L z&@(n+U@$T;GPW`_vNAGKFfz08Pf=WQC4Ol%)MnHF-`M-Jwh5)F529)-6ftWWzXP!AIRuXu!Q1l5cWe3{_RnEWw zH6O-@n+vrMroQ9%e=y*NDn#eQ^nGrBbsyxP1xGW%_HKasO91MQ6g`N$Vf;fM_k-Po zZXPIfK~|xg2XpTgzC&PrFmqt$&i(>1Pk@1e!GM8*ApvS%12nu~`eF9L!Uv=tWFIJH zfoL%h!N34c86YMorGaRHrb@8C7-1Sff<$0+|1dE82Zz%EXt?UY%+P^^kHfDJusyzw zknjZMQ;-svI0MXnsCtcSkZvj*S z7VaO;CWGyP@xlHTU|>*^1uKfyV=EKb008OtSvQWbZRGWe9htV+oF#9*>K=R)O zPLL`{c>pTk(8C|*{~u`P!_$YO95{Z!>6U?k!N$I#ATc>RwL~E)H9a%WR_Xoj{Yna% zDYi=CroINg16w1-Ypui3%0DIeEoa6}C!= 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 VX9{ZTGHue#D(bits_per_sample[0])); + auto const alpha = TRY(manage_extra_channels()); + + // SamplesPerPixel == 1 is a requirement for RGBPalette + // From description of PhotometricInterpretation in Section 8: Baseline Field Reference Guide + // "In a TIFF ColorMap, all the Red values come first, followed by the Green values, + // then the Blue values." + auto const size = 1 << (*m_metadata.bits_per_sample())[0]; + auto const red_offset = 0 * size; + auto const green_offset = 1 * size; + auto const blue_offset = 2 * size; + + auto const color_map = *m_metadata.color_map(); + + // FIXME: ColorMap's values are always 16-bits, stop truncating them when we support 16 bits bitmaps + return Color( + color_map[red_offset + index] >> 8, + color_map[green_offset + index] >> 8, + color_map[blue_offset + index] >> 8, + alpha); + } + if (*m_metadata.photometric_interpretation() == PhotometricInterpretation::WhiteIsZero || *m_metadata.photometric_interpretation() == PhotometricInterpretation::BlackIsZero) { auto luminosity = TRY(read_component(stream, bits_per_sample[0])); diff --git a/Userland/Libraries/LibGfx/TIFFGenerator.py b/Userland/Libraries/LibGfx/TIFFGenerator.py index 3470a6465d..c655dca594 100755 --- a/Userland/Libraries/LibGfx/TIFFGenerator.py +++ b/Userland/Libraries/LibGfx/TIFFGenerator.py @@ -123,6 +123,7 @@ known_tags: List[Tag] = [ Tag('296', [TIFFType.UnsignedShort], [], ResolutionUnit.Inch, "ResolutionUnit", ResolutionUnit), Tag('339', [TIFFType.UnsignedShort], [], SampleFormat.Unsigned, "SampleFormat", SampleFormat), Tag('317', [TIFFType.UnsignedShort], [1], Predictor.NoPrediction, "Predictor", Predictor), + Tag('320', [TIFFType.UnsignedShort], [], None, "ColorMap"), Tag('338', [TIFFType.UnsignedShort], [], None, "ExtraSamples", ExtraSample), Tag('34675', [TIFFType.Undefined], [], None, "ICCProfile"), ]