From 75caccafa4a184e76f31254fcb35b92191a51fd5 Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Sat, 28 Oct 2023 18:05:26 -0400 Subject: [PATCH] LibGfx: Add a TIFF loader --- AK/Debug.h.in | 4 + Meta/CMake/all_the_debug_macros.cmake | 1 + Userland/Libraries/LibGfx/CMakeLists.txt | 1 + .../LibGfx/ImageFormats/ImageDecoder.cpp | 2 + .../LibGfx/ImageFormats/TIFFLoader.cpp | 547 ++++++++++++++++++ .../LibGfx/ImageFormats/TIFFLoader.h | 35 ++ 6 files changed, 590 insertions(+) create mode 100644 Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp create mode 100644 Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.h diff --git a/AK/Debug.h.in b/AK/Debug.h.in index 58215b9485..5c315c6ab0 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -458,6 +458,10 @@ # cmakedefine01 TEXTEDITOR_DEBUG #endif +#ifndef TIFF_DEBUG +# cmakedefine01 TIFF_DEBUG +#endif + #ifndef TIME_ZONE_DEBUG # cmakedefine01 TIME_ZONE_DEBUG #endif diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index 59e8e070d9..af11c81816 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -183,6 +183,7 @@ set(TERMCAP_DEBUG ON) set(TERMINAL_DEBUG ON) set(TEXTEDITOR_DEBUG ON) set(THREAD_DEBUG ON) +set(TIFF_DEBUG ON) set(TIME_ZONE_DEBUG ON) set(TLS_DEBUG ON) set(TLS_SSL_KEYLOG_DEBUG ON) diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 3566adc252..13f503dc33 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -34,6 +34,7 @@ set(SOURCES ImageFormats/BMPLoader.cpp ImageFormats/BMPWriter.cpp ImageFormats/BooleanDecoder.cpp + ImageFormats/TIFFLoader.cpp ImageFormats/DDSLoader.cpp ImageFormats/GIFLoader.cpp ImageFormats/ICOLoader.cpp diff --git a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp index cbc68c250e..5a5551baac 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -44,6 +45,7 @@ static OwnPtr probe_and_sniff_for_appropriate_plugin(Readonl { PNGImageDecoderPlugin::sniff, PNGImageDecoderPlugin::create }, { PPMImageDecoderPlugin::sniff, PPMImageDecoderPlugin::create }, { QOIImageDecoderPlugin::sniff, QOIImageDecoderPlugin::create }, + { TIFFImageDecoderPlugin::sniff, TIFFImageDecoderPlugin::create }, { TinyVGImageDecoderPlugin::sniff, TinyVGImageDecoderPlugin::create }, { WebPImageDecoderPlugin::sniff, WebPImageDecoderPlugin::create }, }; diff --git a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp new file mode 100644 index 0000000000..8ca592b94e --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.cpp @@ -0,0 +1,547 @@ +/* + * Copyright (c) 2023, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "TIFFLoader.h" +#include +#include +#include + +namespace Gfx { + +class TIFFLoadingContext { +public: + enum class State { + NotDecoded = 0, + Error, + HeaderDecoded, + FrameDecoded, + }; + + template x32> + struct Rational { + using Type = x32; + x32 numerator; + x32 denominator; + }; + + TIFFLoadingContext(NonnullOwnPtr stream) + : m_stream(move(stream)) + { + } + + ErrorOr decode_image_header() + { + TRY(read_image_file_header()); + TRY(read_next_image_file_directory()); + + m_state = State::HeaderDecoded; + return {}; + } + + ErrorOr decode_frame() + { + auto maybe_error = decode_frame_impl(); + + if (maybe_error.is_error()) { + m_state = State::Error; + return maybe_error.release_error(); + } + + return {}; + } + + IntSize size() const + { + return m_size; + } + + State state() const + { + return m_state; + } + + RefPtr bitmap() const + { + return m_bitmap; + } + +private: + enum class ByteOrder { + LittleEndian, + BigEndian, + }; + + enum class Type { + Byte = 1, + ASCII = 2, + UnsignedShort = 3, + UnsignedLong = 4, + UnsignedRational = 5, + Undefined = 7, + SignedLong = 9, + SignedRational = 10, + Float = 11, + Double = 12, + UTF8 = 129, + }; + + using Value = Variant, i32, Rational>; + + // This enum is progessively defined across sections but summarized in: + // Appendix A: TIFF Tags Sorted by Number + enum class Compression { + NoCompression = 1, + CCITT = 2, + Group3Fax = 3, + Group4Fax = 4, + LZW = 5, + JPEG = 6, + PackBits = 32773, + }; + + ErrorOr decode_frame_impl() + { + if (m_compression != Compression::NoCompression) + return Error::from_string_literal("Compressed TIFF are not supported yet :^)"); + + m_bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, m_size)); + + for (u32 strip_index = 0; strip_index < m_strip_offsets.size(); ++strip_index) { + TRY(m_stream->seek(m_strip_offsets[strip_index])); + for (u32 row = 0; row < m_rows_per_strip; row++) { + auto const scanline = row + m_rows_per_strip * strip_index; + if (scanline >= static_cast(m_size.height())) + break; + + for (u32 column = 0; column < static_cast(m_size.width()); ++column) { + Color const color { TRY(read_value()), TRY(read_value()), TRY(read_value()) }; + m_bitmap->set_pixel(column, scanline, color); + } + } + } + return {}; + } + + template + ErrorOr read_value() + { + if (m_byte_order == ByteOrder::LittleEndian) + return TRY(m_stream->read_value>()); + if (m_byte_order == ByteOrder::BigEndian) + return TRY(m_stream->read_value>()); + VERIFY_NOT_REACHED(); + } + + ErrorOr read_next_idf_offset() + { + auto const next_block_position = TRY(read_value()); + + if (next_block_position != 0) + m_next_ifd = Optional { next_block_position }; + else + m_next_ifd = OptionalNone {}; + dbgln_if(TIFF_DEBUG, "Setting image file directory pointer to {}", m_next_ifd); + return {}; + } + + ErrorOr read_image_file_header() + { + // Section 2: TIFF Structure - Image File Header + + auto const byte_order = TRY(m_stream->read_value()); + + switch (byte_order) { + case 0x4949: + m_byte_order = ByteOrder::LittleEndian; + break; + case 0x4D4D: + m_byte_order = ByteOrder::BigEndian; + break; + default: + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid byte order"); + } + + auto const magic_number = TRY(read_value()); + + if (magic_number != 42) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid magic number"); + + TRY(read_next_idf_offset()); + + return {}; + } + + ErrorOr read_next_image_file_directory() + { + // Section 2: TIFF Structure - Image File Directory + + if (!m_next_ifd.has_value()) + return Error::from_string_literal("TIFFImageDecoderPlugin: Missing an Image File Directory"); + + TRY(m_stream->seek(m_next_ifd.value())); + + auto const number_of_field = TRY(read_value()); + + for (u16 i = 0; i < number_of_field; ++i) + TRY(read_tag()); + + TRY(read_next_idf_offset()); + return {}; + } + + ErrorOr read_type() + { + switch (TRY(read_value())) { + case to_underlying(Type::Byte): + return Type::Byte; + case to_underlying(Type::ASCII): + return Type::ASCII; + case to_underlying(Type::UnsignedShort): + return Type::UnsignedShort; + case to_underlying(Type::UnsignedLong): + return Type::UnsignedLong; + case to_underlying(Type::UnsignedRational): + return Type::UnsignedRational; + case to_underlying(Type::Undefined): + return Type::Undefined; + case to_underlying(Type::SignedLong): + return Type::SignedLong; + case to_underlying(Type::SignedRational): + return Type::SignedRational; + case to_underlying(Type::UTF8): + return Type::UTF8; + default: + return Error::from_string_literal("TIFFImageDecoderPlugin: Unknown type"); + } + } + + static constexpr u8 size_of_type(Type type) + { + switch (type) { + case Type::Byte: + return 1; + case Type::ASCII: + return 1; + case Type::UnsignedShort: + return 2; + case Type::UnsignedLong: + return 4; + case Type::UnsignedRational: + return 8; + case Type::Undefined: + return 1; + case Type::SignedLong: + return 4; + case Type::SignedRational: + return 8; + case Type::Float: + return 4; + case Type::Double: + return 8; + case Type::UTF8: + return 1; + default: + VERIFY_NOT_REACHED(); + } + } + + ErrorOr> read_tiff_value(Type type, u32 count, u32 offset) + { + auto const old_offset = TRY(m_stream->tell()); + ScopeGuard reset_offset { [this, old_offset]() { MUST(m_stream->seek(old_offset)); } }; + + TRY(m_stream->seek(offset)); + + if (size_of_type(type) * count > m_stream->remaining()) + return Error::from_string_literal("TIFFImageDecoderPlugin: Tag size claims to be bigger that remaining bytes"); + + auto const read_every_values = [this, count]() -> ErrorOr> { + Vector result {}; + TRY(result.try_ensure_capacity(count)); + if constexpr (IsSpecializationOf) { + for (u32 i = 0; i < count; ++i) + result.empend(T { TRY(read_value()), TRY(read_value()) }); + } else { + for (u32 i = 0; i < count; ++i) + result.empend(TRY(read_value())); + } + return result; + }; + + switch (type) { + case Type::Byte: + case Type::Undefined: + return read_every_values.template operator()(); + case Type::ASCII: + case Type::UTF8: { + Vector result; + auto string_data = TRY(ByteBuffer::create_uninitialized(count)); + TRY(m_stream->read_until_filled(string_data)); + result.empend(TRY(String::from_utf8(StringView { string_data.bytes() }))); + return result; + } + case Type::UnsignedShort: + return read_every_values.template operator()(); + case Type::UnsignedLong: + return read_every_values.template operator()(); + case Type::UnsignedRational: + return read_every_values.template operator()>(); + case Type::SignedLong: + return read_every_values.template operator()(); + ; + case Type::SignedRational: + return read_every_values.template operator()>(); + default: + VERIFY_NOT_REACHED(); + } + } + + ErrorOr read_tag() + { + auto const tag = TRY(read_value()); + auto const type = TRY(read_type()); + auto const count = TRY(read_value()); + + Checked checked_size = size_of_type(type); + checked_size *= count; + + if (checked_size.has_overflow()) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag with too large data"); + + auto const tiff_value = TRY(([=, this]() -> ErrorOr> { + if (checked_size.value() <= 4) { + auto value = TRY(read_tiff_value(type, count, TRY(m_stream->tell()))); + TRY(m_stream->discard(4)); + return value; + } + auto const offset = TRY(read_value()); + return read_tiff_value(type, count, offset); + }())); + + if constexpr (TIFF_DEBUG) { + if (tiff_value.size() == 1) { + tiff_value[0].visit( + [&](auto const& value) { + dbgln("Read tag({}), type({}): {}", tag, to_underlying(type), value); + }); + } else { + dbg("Read tag({}), type({}): [", tag, to_underlying(type)); + for (u32 i = 0; i < tiff_value.size(); ++i) { + tiff_value[i].visit( + [&](auto const& value) { + dbg("{}", value); + }); + if (i != tiff_value.size() - 1) + dbg(", "); + } + dbgln("]"); + } + } + + TRY(handle_tag(tag, type, count, tiff_value)); + + return {}; + } + + ErrorOr handle_tag(u16 tag, Type type, u32 count, Vector const& value) + { + // FIXME: Make that easy to extend + switch (tag) { + case 256: + // ImageWidth + if ((type != Type::UnsignedShort && type != Type::UnsignedLong) || count != 1) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 256"); + + value[0].visit( + [this] T>(T const& width) { + m_size.set_width(width); + }, + [&](auto const&) { + VERIFY_NOT_REACHED(); + }); + break; + + case 257: + // ImageLength + if ((type != Type::UnsignedShort && type != Type::UnsignedLong) || count != 1) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 257"); + + value[0].visit( + [this] T>(T const& width) { + m_size.set_height(width); + }, + [&](auto const&) { + VERIFY_NOT_REACHED(); + }); + break; + + case 258: + // BitsPerSample + if (type != Type::UnsignedShort || count != 3) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 258"); + + for (u8 i = 0; i < m_bits_per_sample.size(); ++i) { + value[i].visit( + [this, i](u16 const& bits_per_sample) { + m_bits_per_sample[i] = bits_per_sample; + }, + [&](auto const&) { + VERIFY_NOT_REACHED(); + }); + } + break; + + case 259: + // Compression + if (type != Type::UnsignedShort || count != 1) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 259"); + + TRY(value[0].visit( + [this](u16 const& compression) -> ErrorOr { + if (compression > 6 && compression != to_underlying(Compression::PackBits)) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid compression value"); + + m_compression = static_cast(compression); + return {}; + }, + [&](auto const&) -> ErrorOr { + VERIFY_NOT_REACHED(); + })); + break; + + case 273: + // StripOffsets + if (type != Type::UnsignedShort && type != Type::UnsignedLong) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 273"); + + TRY(m_strip_offsets.try_ensure_capacity(count)); + for (u32 i = 0; i < count; ++i) { + value[i].visit( + [this] T>(T const& offset) { + m_strip_offsets.append(offset); + }, + [&](auto const&) { + VERIFY_NOT_REACHED(); + }); + } + break; + + case 277: + // SamplesPerPixel + if (type != Type::UnsignedShort || count != 1) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 277"); + TRY(value[0].visit( + [](u16 const& samples_per_pixels) -> ErrorOr { + if (samples_per_pixels != 3) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 277"); + return {}; + }, + [&](auto const&) -> ErrorOr { + VERIFY_NOT_REACHED(); + })); + break; + + case 278: + // RowsPerStrip + if ((type != Type::UnsignedShort && type != Type::UnsignedLong) || count != 1) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 278"); + + value[0].visit( + [this] T>(T const& rows_per_strip) { + m_rows_per_strip = rows_per_strip; + }, + [&](auto const&) { + VERIFY_NOT_REACHED(); + }); + break; + + case 279: + // StripByteCounts + if (type != Type::UnsignedShort && type != Type::UnsignedLong) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid tag 279"); + + TRY(m_strip_bytes_count.try_ensure_capacity(count)); + for (u32 i = 0; i < count; ++i) { + value[i].visit( + [this] T>(T const& offset) { + m_strip_bytes_count.append(offset); + }, + [&](auto const&) { + VERIFY_NOT_REACHED(); + }); + } + break; + default: + dbgln_if(TIFF_DEBUG, "Unknown tag: {}", tag); + } + + return {}; + } + + NonnullOwnPtr m_stream; + IntSize m_size {}; + State m_state {}; + RefPtr m_bitmap {}; + + ByteOrder m_byte_order {}; + Optional m_next_ifd {}; + + Array m_bits_per_sample {}; + Compression m_compression {}; + Vector m_strip_offsets {}; + u32 m_rows_per_strip {}; + Vector m_strip_bytes_count {}; +}; + +TIFFImageDecoderPlugin::TIFFImageDecoderPlugin(NonnullOwnPtr stream) +{ + m_context = make(move(stream)); +} + +bool TIFFImageDecoderPlugin::sniff(ReadonlyBytes bytes) +{ + if (bytes.size() < 4) + return false; + bool const valid_little_endian = bytes[0] == 0x49 && bytes[1] == 0x49 && bytes[2] == 0x2A && bytes[3] == 0x00; + bool const valid_big_endian = bytes[0] == 0x4D && bytes[1] == 0x4D && bytes[2] == 0x00 && bytes[3] == 0x2A; + return valid_little_endian || valid_big_endian; +} + +IntSize TIFFImageDecoderPlugin::size() +{ + return m_context->size(); +} + +ErrorOr> TIFFImageDecoderPlugin::create(ReadonlyBytes data) +{ + auto stream = TRY(try_make(data)); + auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) TIFFImageDecoderPlugin(move(stream)))); + TRY(plugin->m_context->decode_image_header()); + return plugin; +} + +ErrorOr TIFFImageDecoderPlugin::frame(size_t index, Optional) +{ + if (index > 0) + return Error::from_string_literal("TIFFImageDecoderPlugin: Invalid frame index"); + + if (m_context->state() == TIFFLoadingContext::State::Error) + return Error::from_string_literal("TIFFImageDecoderPlugin: Decoding failed"); + + if (m_context->state() < TIFFLoadingContext::State::FrameDecoded) + TRY(m_context->decode_frame()); + + return ImageFrameDescriptor { m_context->bitmap(), 0 }; +} +} + +template +struct AK::Formatter> : Formatter { + ErrorOr format(FormatBuilder& builder, Gfx::TIFFLoadingContext::Rational value) + { + return Formatter::format(builder, "{} ({}/{})"sv, static_cast(value.numerator) / value.denominator, value.numerator, value.denominator); + } +}; diff --git a/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.h b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.h new file mode 100644 index 0000000000..809cc50a03 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/TIFFLoader.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, Lucas Chollet + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +namespace Gfx { + +// https://www.itu.int/itudoc/itu-t/com16/tiff-fx/docs/tiff6.pdf + +class TIFFLoadingContext; + +class TIFFImageDecoderPlugin : public ImageDecoderPlugin { +public: + static bool sniff(ReadonlyBytes); + static ErrorOr> create(ReadonlyBytes); + + virtual ~TIFFImageDecoderPlugin() override = default; + + virtual IntSize size() override; + + virtual ErrorOr frame(size_t index, Optional ideal_size = {}) override; + +private: + TIFFImageDecoderPlugin(NonnullOwnPtr); + + OwnPtr m_context; +}; + +}