From fda5590313a88f3b59afd60fb74c99d7e060fdfd Mon Sep 17 00:00:00 2001 From: Nicolas Ramz Date: Sat, 29 Jul 2023 16:24:01 +0200 Subject: [PATCH] LibGfx/ILBM: Add an IFF-ILBM decoder :) IFF was a generic container fileformat that was popular on the Amiga since it was the only file format supported by Deluxe Paint. ILBM is an image format popular in the late eighties/nineties that uses the IFF container. This is a very first version of the decoder that only supports (byterun) compressed files with bpp <= 8. Only the minimal chunks are decoded: CMAP, BODY, BMHD. I am planning to add support for the following variants: - EHB (32 colours + lighter 32 colours) - HAM6 / HAM8 (special mode that allowed to display the whole Amiga 4096 colours / 262 144 colours palette) - TrueColor (24bit) Things that could be fun to do: - Still images could be animated using color cycle information --- AK/Debug.h.in | 4 + Base/res/apps/ImageViewer.af | 2 +- Meta/CMake/all_the_debug_macros.cmake | 1 + Meta/gn/secondary/AK/BUILD.gn | 1 + .../Userland/Libraries/LibGfx/BUILD.gn | 1 + Userland/Libraries/LibCore/MimeData.cpp | 1 + Userland/Libraries/LibGUI/FileTypeFilter.h | 2 +- Userland/Libraries/LibGfx/Bitmap.h | 1 + Userland/Libraries/LibGfx/CMakeLists.txt | 1 + .../LibGfx/ImageFormats/ILBMLoader.cpp | 333 ++++++++++++++++++ .../LibGfx/ImageFormats/ILBMLoader.h | 32 ++ .../LibGfx/ImageFormats/ImageDecoder.cpp | 2 + 12 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp create mode 100644 Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.h diff --git a/AK/Debug.h.in b/AK/Debug.h.in index d154f601d0..1bada8a86c 100644 --- a/AK/Debug.h.in +++ b/AK/Debug.h.in @@ -206,6 +206,10 @@ # cmakedefine01 ICO_DEBUG #endif +#ifndef ILBM_DEBUG +# cmakedefine01 ILBM_DEBUG +#endif + #ifndef IMAGE_DECODER_DEBUG # cmakedefine01 IMAGE_DECODER_DEBUG #endif diff --git a/Base/res/apps/ImageViewer.af b/Base/res/apps/ImageViewer.af index 690b33390b..2477dbee04 100644 --- a/Base/res/apps/ImageViewer.af +++ b/Base/res/apps/ImageViewer.af @@ -4,4 +4,4 @@ Executable=/bin/ImageViewer Category=Graphics [Launcher] -FileTypes=bmp,dds,gif,ico,jpeg,jpg,jxl,pbm,pgm,png,ppm,qoi,tga,tvg +FileTypes=bmp,dds,gif,ico,iff,jpeg,jpg,jxl,pbm,pgm,png,ppm,qoi,tga,tvg diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake index fbf306bbfa..b93bdc50f3 100644 --- a/Meta/CMake/all_the_debug_macros.cmake +++ b/Meta/CMake/all_the_debug_macros.cmake @@ -72,6 +72,7 @@ set(HTTPJOB_DEBUG ON) set(HUNKS_DEBUG ON) set(ICMP_DEBUG ON) set(ICO_DEBUG ON) +set(ILBM_DEBUG ON) set(IMAGE_DECODER_DEBUG ON) set(IMAGE_LOADER_DEBUG ON) set(IMAP_PARSER_DEBUG ON) diff --git a/Meta/gn/secondary/AK/BUILD.gn b/Meta/gn/secondary/AK/BUILD.gn index 8341dd1b1c..e18c2b043a 100644 --- a/Meta/gn/secondary/AK/BUILD.gn +++ b/Meta/gn/secondary/AK/BUILD.gn @@ -283,6 +283,7 @@ write_cmake_config("ak_debug_gen") { "HTTPJOB_DEBUG=", "HUNKS_DEBUG=", "ICO_DEBUG=", + "ILBM_DEBUG=", "IMAGE_DECODER_DEBUG=", "IMAGE_LOADER_DEBUG=", "ITEM_RECTS_DEBUG=", diff --git a/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn index b1f03f41e5..9e5608d93e 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibGfx/BUILD.gn @@ -41,6 +41,7 @@ shared_library("LibGfx") { "ImageFormats/DDSLoader.cpp", "ImageFormats/GIFLoader.cpp", "ImageFormats/ICOLoader.cpp", + "ImageFormats/ILBMLoader.cpp", "ImageFormats/ImageDecoder.cpp", "ImageFormats/JPEGLoader.cpp", "ImageFormats/JPEGWriter.cpp", diff --git a/Userland/Libraries/LibCore/MimeData.cpp b/Userland/Libraries/LibCore/MimeData.cpp index ce6de165d0..f59c47f1c2 100644 --- a/Userland/Libraries/LibCore/MimeData.cpp +++ b/Userland/Libraries/LibCore/MimeData.cpp @@ -132,6 +132,7 @@ static Array const s_registered_mime_type = { MimeType { .name = "image/vnd.ms-dds"sv, .common_extensions = { ".dds"sv }, .description = "DDS image data"sv, .magic_bytes = Vector { 'D', 'D', 'S', ' ' } }, MimeType { .name = "image/webp"sv, .common_extensions = { ".webp"sv }, .description = "WebP image data"sv, .magic_bytes = Vector { 'W', 'E', 'B', 'P' }, .offset = 8 }, MimeType { .name = "image/x-icon"sv, .common_extensions = { ".ico"sv }, .description = "ICO image data"sv }, + MimeType { .name = "image/x-ilbm"sv, .common_extensions = { ".iff"sv }, .description = "Interleaved bitmap image data"sv, .magic_bytes = Vector { 0x46, 0x4F, 0x52, 0x4F } }, MimeType { .name = "image/x-portable-bitmap"sv, .common_extensions = { ".pbm"sv }, .description = "PBM image data"sv, .magic_bytes = Vector { 0x50, 0x31, 0x0A } }, MimeType { .name = "image/x-portable-graymap"sv, .common_extensions = { ".pgm"sv }, .description = "PGM image data"sv, .magic_bytes = Vector { 0x50, 0x32, 0x0A } }, MimeType { .name = "image/x-portable-pixmap"sv, .common_extensions = { ".ppm"sv }, .description = "PPM image data"sv, .magic_bytes = Vector { 0x50, 0x33, 0x0A } }, diff --git a/Userland/Libraries/LibGUI/FileTypeFilter.h b/Userland/Libraries/LibGUI/FileTypeFilter.h index f101a023a2..c44568e2e2 100644 --- a/Userland/Libraries/LibGUI/FileTypeFilter.h +++ b/Userland/Libraries/LibGUI/FileTypeFilter.h @@ -25,7 +25,7 @@ struct FileTypeFilter { static FileTypeFilter image_files() { - return FileTypeFilter { "Image Files", Vector { "png", "gif", "bmp", "dip", "pbm", "pgm", "ppm", "ico", "jpeg", "jpg", "jxl", "dds", "qoi", "webp", "tvg" } }; + return FileTypeFilter { "Image Files", Vector { "png", "gif", "bmp", "dip", "pbm", "pgm", "ppm", "ico", "iff", "jpeg", "jpg", "jxl", "dds", "qoi", "webp", "tvg" } }; } }; diff --git a/Userland/Libraries/LibGfx/Bitmap.h b/Userland/Libraries/LibGfx/Bitmap.h index 61e641bca9..4e5321a6d2 100644 --- a/Userland/Libraries/LibGfx/Bitmap.h +++ b/Userland/Libraries/LibGfx/Bitmap.h @@ -24,6 +24,7 @@ __ENUMERATE_IMAGE_FORMAT(gif, ".gif") \ __ENUMERATE_IMAGE_FORMAT(bmp, ".bmp") \ __ENUMERATE_IMAGE_FORMAT(ico, ".ico") \ + __ENUMERATE_IMAGE_FORMAT(iff, ".iff") \ __ENUMERATE_IMAGE_FORMAT(jpeg, ".jpg") \ __ENUMERATE_IMAGE_FORMAT(jpeg, ".jpeg") \ __ENUMERATE_IMAGE_FORMAT(jxl, ".jxl") \ diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 4fb078cda6..3566adc252 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -37,6 +37,7 @@ set(SOURCES ImageFormats/DDSLoader.cpp ImageFormats/GIFLoader.cpp ImageFormats/ICOLoader.cpp + ImageFormats/ILBMLoader.cpp ImageFormats/ImageDecoder.cpp ImageFormats/ISOBMFF/Boxes.cpp ImageFormats/ISOBMFF/Reader.cpp diff --git a/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp new file mode 100644 index 0000000000..9b2efdd3cc --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.cpp @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2023, Nicolas Ramz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Gfx { + +struct IFFHeader { + FourCC form; + BigEndian file_size; + FourCC format; +}; + +static_assert(AssertSize()); + +struct Chunk { + FourCC type; + ReadonlyBytes data; +}; + +enum class CompressionType : u8 { + None = 0, + ByteRun = 1 +}; + +enum class MaskType : u8 { + None = 0, + HasMask = 1, + HasTransparentColor = 2, + HasLasso = 3 +}; + +struct ChunkHeader { + FourCC chunk_type; + BigEndian chunk_size; +}; + +struct BMHDHeader { + BigEndian width; + BigEndian height; + BigEndian x; + BigEndian y; + u8 planes; + MaskType mask; + CompressionType compression; + u8 pad; + BigEndian transparent_color; + u8 x_aspect; + u8 y_aspect; + BigEndian page_width; + BigEndian page_height; +}; + +static_assert(sizeof(BMHDHeader) == 20); + +struct ILBMLoadingContext { + enum class State { + NotDecoded = 0, + HeaderDecoded, + BitmapDecoded + }; + State state { State::NotDecoded }; + ReadonlyBytes data; + + // points to current chunk + ReadonlyBytes chunks_cursor; + + // max number of bytes per plane row + u16 pitch; + + FixedArray color_table; + + RefPtr bitmap; + + BMHDHeader bm_header; +}; + +static ErrorOr decode_iff_ilbm_header(ILBMLoadingContext& context) +{ + if (context.state >= ILBMLoadingContext::State::HeaderDecoded) + return {}; + + if (context.data.size() < sizeof(IFFHeader)) + return Error::from_string_literal("Missing IFF header"); + + auto& header = *bit_cast(context.data.data()); + if (header.form != FourCC("FORM") || header.format != FourCC("ILBM")) + return Error::from_string_literal("Invalid IFF-ILBM header"); + + return {}; +} + +static ErrorOr> decode_cmap_chunk(Chunk cmap_chunk) +{ + size_t const size = cmap_chunk.data.size() / 3; + FixedArray color_table = TRY(FixedArray::create(size)); + + for (size_t i = 0; i < size; ++i) { + color_table[i] = Color(cmap_chunk.data[i * 3], cmap_chunk.data[(i * 3) + 1], cmap_chunk.data[(i * 3) + 2]); + } + + return color_table; +} + +static ErrorOr> chunky_to_bitmap(ILBMLoadingContext& context, ByteBuffer const& chunky) +{ + auto const width = context.bm_header.width; + auto const height = context.bm_header.height; + + RefPtr bitmap = TRY(Bitmap::create(BitmapFormat::BGRA8888, { width, height })); + + dbgln_if(ILBM_DEBUG, "created Bitmap {}x{}", width, height); + + for (int row = 0; row < height; ++row) { + for (int col = 0; col < width; ++col) { + u8 index = chunky[(width * row) + col]; + bitmap->set_pixel(col, row, context.color_table[index]); + } + } + + dbgln_if(ILBM_DEBUG, "filled Bitmap"); + + return bitmap; +} + +static ErrorOr planar_to_chunky(ReadonlyBytes bitplanes, ILBMLoadingContext& context) +{ + dbgln_if(ILBM_DEBUG, "planar_to_chunky"); + u16 pitch = context.pitch; + u16 width = context.bm_header.width; + u16 height = context.bm_header.height; + u8 planes = context.bm_header.planes; + auto chunky = TRY(ByteBuffer::create_zeroed(width * height)); + + for (u16 y = 0; y < height; y++) { + for (u8 p = 0; p < planes; p++) { + u8 const plane_mask = 1 << p; + for (u16 i = 0; i < pitch; i++) { + u16 offset = (pitch * planes * y) + (p * pitch) + i; + u8 bit = bitplanes[offset]; + + for (u8 b = 0; b < 8; b++) { + u8 mask = 1 << (7 - b); + // get current plane + if (bit & mask) { + u16 x = (i * 8) + b; + chunky[(y * width) + x] |= plane_mask; + } + } + } + } + } + + return chunky; +} + +static ErrorOr uncompress_byte_run(ReadonlyBytes data, ILBMLoadingContext& context) +{ + auto length = data.size(); + dbgln_if(ILBM_DEBUG, "uncompress_byte_run pitch={} size={}", context.pitch, data.size()); + + auto plane_data = TRY(ByteBuffer::create_uninitialized(context.pitch * context.bm_header.height * context.bm_header.planes)); + + u32 index = 0; + u32 read_bytes = 0; + while (read_bytes < length) { + auto const byte = static_cast(data[read_bytes++]); + if (byte >= -127 && byte <= -1) { + // read next byte + u8 next_byte = data[read_bytes++]; + for (u16 i = 0; i < -byte + 1; ++i) { + plane_data[index++] = next_byte; + } + } else if (byte >= 0) { + for (u16 i = 0; i < byte + 1; ++i) { + plane_data[index] = data[read_bytes]; + read_bytes++; + index++; + } + } + } + + return plane_data; +} + +static ErrorOr decode_body_chunk(Chunk body_chunk, ILBMLoadingContext& context) +{ + dbgln_if(ILBM_DEBUG, "decode_body_chunk {}", body_chunk.data.size()); + + if (context.bm_header.compression == CompressionType::ByteRun) { + // these are the uncompressed interleaved bitmap planes + auto plane_data = TRY(uncompress_byte_run(body_chunk.data, context)); + // that we need to convert to chunky pixel data + auto pixel_data = TRY(planar_to_chunky(plane_data, context)); + + context.bitmap = TRY(chunky_to_bitmap(context, pixel_data)); + } else { + return Error::from_string_literal("Uncompress body not supported yet"); + } + + return {}; +} + +static ErrorOr decode_iff_chunk_header(ReadonlyBytes chunks) +{ + if (chunks.size() < sizeof(ChunkHeader)) + return Error::from_string_literal("Not enough data for IFF chunk header"); + + auto const& header = *bit_cast(chunks.data()); + + if (chunks.size() < sizeof(ChunkHeader) + header.chunk_size) + return Error::from_string_literal("Not enough data for IFF chunk"); + + return Chunk { header.chunk_type, { chunks.data() + sizeof(ChunkHeader), header.chunk_size } }; +} + +static ErrorOr decode_iff_advance_chunk(ReadonlyBytes& chunks) +{ + auto chunk = TRY(decode_iff_chunk_header(chunks)); + + chunks = chunks.slice(sizeof(ChunkHeader) + chunk.data.size()); + + // add padding if needed + if (chunk.data.size() % 2 != 0) { + if (chunks.is_empty()) + return Error::from_string_literal("Missing data for padding byte"); + if (*chunks.data() != 0) + return Error::from_string_literal("Padding byte is not 0"); + chunks = chunks.slice(1); + } + + return chunk; +} + +static ErrorOr decode_iff_chunks(ILBMLoadingContext& context) +{ + auto& chunks = context.chunks_cursor; + + dbgln_if(ILBM_DEBUG, "decode_iff_chunks"); + + while (!chunks.is_empty()) { + auto chunk = TRY(decode_iff_advance_chunk(chunks)); + if (chunk.type == FourCC("CMAP")) { + context.color_table = TRY(decode_cmap_chunk(chunk)); + } else if (chunk.type == FourCC("BODY")) { + TRY(decode_body_chunk(chunk, context)); + context.state = ILBMLoadingContext::State::BitmapDecoded; + } else if (chunk.type == FourCC("CRNG")) { + dbgln_if(ILBM_DEBUG, "Chunk:CRNG"); + } + } + + return {}; +} + +static ErrorOr decode_bmhd_chunk(ILBMLoadingContext& context) +{ + context.chunks_cursor = context.data.slice(sizeof(IFFHeader)); + auto first_chunk = TRY(decode_iff_advance_chunk(context.chunks_cursor)); + + if (first_chunk.type != FourCC("BMHD")) + return Error::from_string_literal("IFFImageDecoderPlugin: Invalid chunk type, expected BMHD"); + + context.bm_header = *bit_cast(first_chunk.data.data()); + context.pitch = ceil_div((u16)context.bm_header.width, (u16)16) * 2; + + context.state = ILBMLoadingContext::State::HeaderDecoded; + + dbgln_if(ILBM_DEBUG, "IFFImageDecoderPlugin: BMHD: {}x{} ({},{}), p={}, m={}, c={}", + context.bm_header.width, + context.bm_header.height, + context.bm_header.x, + context.bm_header.y, + context.bm_header.planes, + to_underlying(context.bm_header.mask), + to_underlying(context.bm_header.compression)); + + return {}; +} + +ILBMImageDecoderPlugin::ILBMImageDecoderPlugin(ReadonlyBytes data, NonnullOwnPtr context) + : m_context(move(context)) +{ + m_context->data = data; +} + +ILBMImageDecoderPlugin::~ILBMImageDecoderPlugin() = default; + +IntSize ILBMImageDecoderPlugin::size() +{ + return IntSize { m_context->bm_header.width, m_context->bm_header.height }; +} + +bool ILBMImageDecoderPlugin::sniff(ReadonlyBytes data) +{ + ILBMLoadingContext context; + context.data = data; + + return !decode_iff_ilbm_header(context).is_error(); +} + +ErrorOr> ILBMImageDecoderPlugin::create(ReadonlyBytes data) +{ + auto context = TRY(try_make()); + auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) ILBMImageDecoderPlugin(data, move(context)))); + TRY(decode_iff_ilbm_header(*plugin->m_context)); + TRY(decode_bmhd_chunk(*plugin->m_context)); + return plugin; +} + +ErrorOr ILBMImageDecoderPlugin::frame(size_t index, Optional) +{ + if (index > 0) + return Error::from_string_literal("ILBMImageDecoderPlugin: frame index must be 0"); + + if (m_context->state < ILBMLoadingContext::State::BitmapDecoded) + TRY(decode_iff_chunks(*m_context)); + + VERIFY(m_context->bitmap); + return ImageFrameDescriptor { m_context->bitmap, 0 }; +} + +ErrorOr> ILBMImageDecoderPlugin::icc_data() +{ + return OptionalNone {}; +} +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.h b/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.h new file mode 100644 index 0000000000..db3a4d5eb7 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/ILBMLoader.h @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023, Nicolas Ramz + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Gfx { + +struct ILBMLoadingContext; + +class ILBMImageDecoderPlugin final : public ImageDecoderPlugin { +public: + static bool sniff(ReadonlyBytes); + static ErrorOr> create(ReadonlyBytes); + + virtual ~ILBMImageDecoderPlugin() override; + + virtual IntSize size() override; + virtual ErrorOr> icc_data() override; + + virtual ErrorOr frame(size_t index, Optional ideal_size = {}) override; + +private: + ILBMImageDecoderPlugin(ReadonlyBytes, NonnullOwnPtr); + NonnullOwnPtr m_context; +}; + +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp index dbcc6ddff7..a04c19421f 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ static OwnPtr probe_and_sniff_for_appropriate_plugin(Readonl { PGMImageDecoderPlugin::sniff, PGMImageDecoderPlugin::create }, { PPMImageDecoderPlugin::sniff, PPMImageDecoderPlugin::create }, { ICOImageDecoderPlugin::sniff, ICOImageDecoderPlugin::create }, + { ILBMImageDecoderPlugin::sniff, ILBMImageDecoderPlugin::create }, { JPEGImageDecoderPlugin::sniff, JPEGImageDecoderPlugin::create }, { JPEGXLImageDecoderPlugin::sniff, JPEGXLImageDecoderPlugin::create }, { DDSImageDecoderPlugin::sniff, DDSImageDecoderPlugin::create },