diff --git a/Tests/LibGfx/TestImageDecoder.cpp b/Tests/LibGfx/TestImageDecoder.cpp index 0afa5f2878..8253e27ebb 100644 --- a/Tests/LibGfx/TestImageDecoder.cpp +++ b/Tests/LibGfx/TestImageDecoder.cpp @@ -700,6 +700,19 @@ TEST_CASE(test_tiff_16_bits) EXPECT_EQ(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::Red); } +TEST_CASE(test_tiff_cmyk) +{ + auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("tiff/cmyk.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); + // I stripped the ICC profile from the image, so we can't test for equality with Red here. + EXPECT_NE(frame.image->get_pixel(60, 75), Gfx::Color::NamedColor::White); +} + TEST_CASE(test_tiff_invalid_tag) { auto file = TRY_OR_FAIL(Core::MappedFile::map(TEST_INPUT("tiff/invalid_tag.tiff"sv))); diff --git a/Tests/LibGfx/test-inputs/tiff/cmyk.tiff b/Tests/LibGfx/test-inputs/tiff/cmyk.tiff new file mode 100644 index 0000000000..973db00118 Binary files /dev/null and b/Tests/LibGfx/test-inputs/tiff/cmyk.tiff differ diff --git a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp index 6be1d72d55..5b47182dca 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +115,11 @@ public: return m_state; } + RefPtr cmyk_bitmap() const + { + return m_cmyk_bitmap; + } + RefPtr bitmap() const { return m_bitmap; @@ -144,6 +150,8 @@ private: return 1; case PhotometricInterpretation::RGB: return 3; + case PhotometricInterpretation::CMYK: + return 4; default: TODO(); } @@ -238,6 +246,22 @@ private: return Error::from_string_literal("Unsupported value for PhotometricInterpretation"); } + ErrorOr read_color_cmyk(BigEndianInputBitStream& stream) + { + VERIFY(m_metadata.photometric_interpretation() == PhotometricInterpretation::CMYK); + auto bits_per_sample = *m_metadata.bits_per_sample(); + + auto const first_component = TRY(read_component(stream, bits_per_sample[0])); + auto const second_component = TRY(read_component(stream, bits_per_sample[1])); + auto const third_component = TRY(read_component(stream, bits_per_sample[2])); + auto const fourth_component = TRY(read_component(stream, bits_per_sample[3])); + + // FIXME: We probably won't encounter CMYK images with an alpha channel, but if + // we do: the first step to support them is not dropping the value here! + [[maybe_unused]] auto const alpha = TRY(manage_extra_channels(stream, bits_per_sample)); + return CMYK { first_component, second_component, third_component, fourth_component }; + } + template, u32, u32> StripDecoder> ErrorOr loop_over_pixels(StripDecoder&& strip_decoder) { @@ -245,7 +269,11 @@ private: auto const strip_byte_counts = *m_metadata.strip_byte_counts(); auto const rows_per_strip = m_metadata.rows_per_strip().value_or(*m_metadata.image_height()); - auto oriented_bitmap = TRY(ExifOrientedBitmap::create(*metadata().orientation(), { *metadata().image_width(), *metadata().image_height() }, BitmapFormat::BGRA8888)); + Variant oriented_bitmap = TRY(([&]() -> ErrorOr> { + if (metadata().photometric_interpretation() == PhotometricInterpretation::CMYK) + return ExifOrientedCMYKBitmap::create(*metadata().orientation(), { *metadata().image_width(), *metadata().image_height() }); + return ExifOrientedBitmap::create(*metadata().orientation(), { *metadata().image_width(), *metadata().image_height() }, BitmapFormat::BGRA8888); + }())); for (u32 strip_index = 0; strip_index < strips_offset.size(); ++strip_index) { TRY(m_stream->seek(strips_offset[strip_index])); @@ -263,25 +291,35 @@ private: Optional last_color {}; for (u32 column = 0; column < *m_metadata.image_width(); ++column) { - auto color = TRY(read_color(*decoded_stream)); + if (metadata().photometric_interpretation() == PhotometricInterpretation::CMYK) { + auto const cmyk = TRY(read_color_cmyk(*decoded_stream)); + oriented_bitmap.get().set_pixel(column, scanline, cmyk); + } else { + auto color = TRY(read_color(*decoded_stream)); - if (m_metadata.predictor() == Predictor::HorizontalDifferencing && last_color.has_value()) { - color.set_red(last_color->red() + color.red()); - color.set_green(last_color->green() + color.green()); - color.set_blue(last_color->blue() + color.blue()); - if (alpha_channel_index().has_value()) - color.set_alpha(last_color->alpha() + color.alpha()); + // FIXME: We should do the differencing at the byte-stream level, that would make it + // compatible with both LibPDF and all color formats. + if (m_metadata.predictor() == Predictor::HorizontalDifferencing && last_color.has_value()) { + color.set_red(last_color->red() + color.red()); + color.set_green(last_color->green() + color.green()); + color.set_blue(last_color->blue() + color.blue()); + if (alpha_channel_index().has_value()) + color.set_alpha(last_color->alpha() + color.alpha()); + } + + last_color = color; + oriented_bitmap.get().set_pixel(column, scanline, color.value()); } - - last_color = color; - oriented_bitmap.set_pixel(column, scanline, color.value()); } decoded_stream->align_to_byte_boundary(); } } - m_bitmap = oriented_bitmap.bitmap(); + if (m_metadata.photometric_interpretation() == PhotometricInterpretation::CMYK) + m_cmyk_bitmap = oriented_bitmap.get().bitmap(); + else + m_bitmap = oriented_bitmap.get().bitmap(); return {}; } @@ -567,6 +605,7 @@ private: NonnullOwnPtr m_stream; State m_state {}; RefPtr m_bitmap {}; + RefPtr m_cmyk_bitmap {}; ByteOrder m_byte_order {}; Optional m_next_ifd {}; @@ -614,6 +653,9 @@ ErrorOr TIFFImageDecoderPlugin::frame(size_t index, Option if (m_context->state() < TIFF::TIFFLoadingContext::State::FrameDecoded) TRY(m_context->decode_frame()); + if (m_context->cmyk_bitmap()) + return ImageFrameDescriptor { TRY(m_context->cmyk_bitmap()->to_low_quality_rgb()), 0 }; + return ImageFrameDescriptor { m_context->bitmap(), 0 }; }