From 187862ebe0931e362fd5b407f1b976d91c50cdde Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Wed, 24 Jan 2024 21:27:24 -0500 Subject: [PATCH] LibGfx: Add a .pam loader .pam is a "portrable arbitrarymap" as documented at https://netpbm.sourceforge.net/doc/pam.html It's very similar to .pbm, .pgm, and .ppm, so this uses the PortableImageMapLoader framework. The header is slightly different, so this has a custom header parsing function. Also, .pam only exixts in binary form, so the ascii form support becomes optional. --- 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/ImageDecoder.cpp | 2 + .../LibGfx/ImageFormats/PAMLoader.cpp | 52 +++++++++++ .../Libraries/LibGfx/ImageFormats/PAMLoader.h | 92 +++++++++++++++++++ .../ImageFormats/PortableImageLoaderCommon.h | 20 +++- .../ImageFormats/PortableImageMapLoader.h | 11 ++- 9 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 Userland/Libraries/LibGfx/ImageFormats/PAMLoader.cpp create mode 100644 Userland/Libraries/LibGfx/ImageFormats/PAMLoader.h diff --git a/Userland/Libraries/LibCore/MimeData.cpp b/Userland/Libraries/LibCore/MimeData.cpp index 716046bc20..f5a531216d 100644 --- a/Userland/Libraries/LibCore/MimeData.cpp +++ b/Userland/Libraries/LibCore/MimeData.cpp @@ -124,6 +124,7 @@ static Array const s_registered_mime_type = { 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, ".lbm"sv }, .description = "Interleaved bitmap image data"sv, .magic_bytes = Vector { 0x46, 0x4F, 0x52, 0x4F } }, + MimeType { .name = "image/x-portable-arbitrarymap"sv, .common_extensions = { ".pam"sv }, .description = "PAM image data"sv, .magic_bytes = Vector { 0x50, 0x37, 0x0A } }, 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 7cd07f802b..4d4b93079f 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", "iff", "jpeg", "jpg", "jxl", "dds", "qoi", "tif", "tiff", "webp", "tvg" } }; + return FileTypeFilter { "Image Files", Vector { "png", "gif", "bmp", "dip", "pam", "pbm", "pgm", "ppm", "ico", "iff", "jpeg", "jpg", "jxl", "dds", "qoi", "tif", "tiff", "webp", "tvg" } }; } }; diff --git a/Userland/Libraries/LibGfx/Bitmap.h b/Userland/Libraries/LibGfx/Bitmap.h index abfba94033..24f9470087 100644 --- a/Userland/Libraries/LibGfx/Bitmap.h +++ b/Userland/Libraries/LibGfx/Bitmap.h @@ -26,6 +26,7 @@ __ENUMERATE_IMAGE_FORMAT(jpeg, ".jpg") \ __ENUMERATE_IMAGE_FORMAT(jxl, ".jxl") \ __ENUMERATE_IMAGE_FORMAT(iff, ".lbm") \ + __ENUMERATE_IMAGE_FORMAT(pam, ".pam") \ __ENUMERATE_IMAGE_FORMAT(pbm, ".pbm") \ __ENUMERATE_IMAGE_FORMAT(pgm, ".pgm") \ __ENUMERATE_IMAGE_FORMAT(png, ".png") \ diff --git a/Userland/Libraries/LibGfx/CMakeLists.txt b/Userland/Libraries/LibGfx/CMakeLists.txt index 360dc7f657..14bb80a05b 100644 --- a/Userland/Libraries/LibGfx/CMakeLists.txt +++ b/Userland/Libraries/LibGfx/CMakeLists.txt @@ -55,6 +55,7 @@ set(SOURCES ImageFormats/PNGLoader.cpp ImageFormats/PNGWriter.cpp ImageFormats/PortableFormatWriter.cpp + ImageFormats/PAMLoader.cpp ImageFormats/PPMLoader.cpp ImageFormats/QOILoader.cpp ImageFormats/QOIWriter.cpp diff --git a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp index 5d1bd80c70..56d054bfde 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp +++ b/Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ static OwnPtr probe_and_sniff_for_appropriate_plugin(Readonl { ILBMImageDecoderPlugin::sniff, ILBMImageDecoderPlugin::create }, { JPEGImageDecoderPlugin::sniff, JPEGImageDecoderPlugin::create }, { JPEGXLImageDecoderPlugin::sniff, JPEGXLImageDecoderPlugin::create }, + { PAMImageDecoderPlugin::sniff, PAMImageDecoderPlugin::create }, { PBMImageDecoderPlugin::sniff, PBMImageDecoderPlugin::create }, { PGMImageDecoderPlugin::sniff, PGMImageDecoderPlugin::create }, { PNGImageDecoderPlugin::sniff, PNGImageDecoderPlugin::create }, diff --git a/Userland/Libraries/LibGfx/ImageFormats/PAMLoader.cpp b/Userland/Libraries/LibGfx/ImageFormats/PAMLoader.cpp new file mode 100644 index 0000000000..e184a06f47 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/PAMLoader.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "PAMLoader.h" +#include "PortableImageLoaderCommon.h" + +namespace Gfx { + +ErrorOr read_image_data(PAMLoadingContext& context) +{ + VERIFY(context.type == PAMLoadingContext::Type::RAWBITS); + + // FIXME: Technically it's more to spec to check that a known tupl type has a minimum depth and then skip additional channels. + bool is_gray = context.format_details.depth == 1 && context.format_details.tupl_type == "GRAYSCALE"sv; + bool is_gray_alpha = context.format_details.depth == 2 && context.format_details.tupl_type == "GRAYSCALE_ALPHA"sv; + bool is_rgb = context.format_details.depth == 3 && context.format_details.tupl_type == "RGB"sv; + bool is_rgba = context.format_details.depth == 4 && context.format_details.tupl_type == "RGB_ALPHA"sv; + + if (!is_gray && !is_gray_alpha && !is_rgb && !is_rgba) + return Error::from_string_view("Unsupported PAM depth"sv); + + TRY(create_bitmap(context)); + + auto& stream = *context.stream; + + for (u64 i = 0; i < context.width * context.height; ++i) { + if (is_gray) { + Array pixel; + TRY(stream.read_until_filled(pixel)); + context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[0], pixel[0] }); + } else if (is_gray_alpha) { + Array pixel; + TRY(stream.read_until_filled(pixel)); + context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[0], pixel[0], pixel[1] }); + } else if (is_rgb) { + Array pixel; + TRY(stream.read_until_filled(pixel)); + context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[1], pixel[2] }); + } else if (is_rgba) { + Array pixel; + TRY(stream.read_until_filled(pixel)); + context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[1], pixel[2], pixel[3] }); + } + } + + context.state = PAMLoadingContext::State::BitmapDecoded; + return {}; +} +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/PAMLoader.h b/Userland/Libraries/LibGfx/ImageFormats/PAMLoader.h new file mode 100644 index 0000000000..0a674097f3 --- /dev/null +++ b/Userland/Libraries/LibGfx/ImageFormats/PAMLoader.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Gfx { + +struct PAM { + static constexpr auto binary_magic_number = '7'; + static constexpr StringView image_type = "PAM"sv; + u16 max_val { 0 }; + u16 depth { 0 }; + String tupl_type {}; +}; + +using PAMLoadingContext = PortableImageMapLoadingContext; + +template +ErrorOr read_pam_header(Context& context) +{ + // https://netpbm.sourceforge.net/doc/pam.html + TRY(read_magic_number(context)); + + Optional width; + Optional height; + Optional depth; + Optional max_val; + Optional tupltype; + + while (true) { + TRY(read_whitespace(context)); + + auto const token = TRY(read_token(*context.stream)); + + if (token == "ENDHDR") { + auto newline = TRY(context.stream->template read_value()); + if (newline != '\n') + return Error::from_string_view("PAM ENDHDR not followed by newline"sv); + break; + } + + TRY(read_whitespace(context)); + if (token == "WIDTH") { + if (width.has_value()) + return Error::from_string_view("Duplicate PAM WIDTH field"sv); + width = TRY(read_number(*context.stream)); + } else if (token == "HEIGHT") { + if (height.has_value()) + return Error::from_string_view("Duplicate PAM HEIGHT field"sv); + height = TRY(read_number(*context.stream)); + } else if (token == "DEPTH") { + if (depth.has_value()) + return Error::from_string_view("Duplicate PAM DEPTH field"sv); + depth = TRY(read_number(*context.stream)); + } else if (token == "MAXVAL") { + if (max_val.has_value()) + return Error::from_string_view("Duplicate PAM MAXVAL field"sv); + max_val = TRY(read_number(*context.stream)); + } else if (token == "TUPLTYPE") { + // FIXME: tupltype should be all text until the next newline, with leading and trailing space stripped. + // FIXME: If there are multipe TUPLTYPE lines, their values are all appended. + tupltype = TRY(read_token(*context.stream)); + } else { + return Error::from_string_view("Unknown PAM token"sv); + } + } + + if (!width.has_value() || !height.has_value() || !depth.has_value() || !max_val.has_value()) + return Error::from_string_view("Missing PAM header fields"sv); + context.width = *width; + context.height = *height; + context.format_details.depth = *depth; + context.format_details.max_val = *max_val; + if (tupltype.has_value()) + context.format_details.tupl_type = *tupltype; + + context.state = Context::State::HeaderDecoded; + + return {}; +} + +using PAMImageDecoderPlugin = PortableImageDecoderPlugin; + +ErrorOr read_image_data(PAMLoadingContext& context); +} diff --git a/Userland/Libraries/LibGfx/ImageFormats/PortableImageLoaderCommon.h b/Userland/Libraries/LibGfx/ImageFormats/PortableImageLoaderCommon.h index 21c2f43737..7d29798062 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/PortableImageLoaderCommon.h +++ b/Userland/Libraries/LibGfx/ImageFormats/PortableImageLoaderCommon.h @@ -29,7 +29,7 @@ static constexpr Color adjust_color(u16 max_val, Color color) return color; } -static inline ErrorOr read_number(SeekableStream& stream) +inline ErrorOr read_token(SeekableStream& stream) { StringBuilder sb {}; u8 byte {}; @@ -43,7 +43,12 @@ static inline ErrorOr read_number(SeekableStream& stream) sb.append(byte); } - auto const maybe_value = TRY(sb.to_string()).to_number(); + return TRY(sb.to_string()); +} + +static inline ErrorOr read_number(SeekableStream& stream) +{ + auto const maybe_value = TRY(read_token(stream)).to_number(); if (!maybe_value.has_value()) return Error::from_string_literal("Can't convert bytes to a number"); @@ -81,9 +86,11 @@ static ErrorOr read_magic_number(TContext& context) Array magic_number {}; TRY(context.stream->read_until_filled(Bytes { magic_number })); - if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::ascii_magic_number) { - context.type = TContext::Type::ASCII; - return {}; + if constexpr (requires { TContext::FormatDetails::ascii_magic_number; }) { + if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::ascii_magic_number) { + context.type = TContext::Type::ASCII; + return {}; + } } if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::binary_magic_number) { @@ -187,6 +194,9 @@ static ErrorOr read_header(Context& context) return {}; } +template +static ErrorOr read_pam_header(Context& context); + template static ErrorOr decode(TContext& context) { diff --git a/Userland/Libraries/LibGfx/ImageFormats/PortableImageMapLoader.h b/Userland/Libraries/LibGfx/ImageFormats/PortableImageMapLoader.h index ab3b393c08..cbf73334d3 100644 --- a/Userland/Libraries/LibGfx/ImageFormats/PortableImageMapLoader.h +++ b/Userland/Libraries/LibGfx/ImageFormats/PortableImageMapLoader.h @@ -85,7 +85,10 @@ ErrorOr> PortableImageDecoderPlugin: { auto stream = TRY(try_make(data)); auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) PortableImageDecoderPlugin(move(stream)))); - TRY(read_header(*plugin->m_context)); + if constexpr (TContext::FormatDetails::binary_magic_number == '7') + TRY(read_pam_header(*plugin->m_context)); + else + TRY(read_header(*plugin->m_context)); return plugin; } @@ -96,8 +99,10 @@ bool PortableImageDecoderPlugin::sniff(ReadonlyBytes data) if (data.size() < 2) return false; - if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::ascii_magic_number) - return true; + if constexpr (requires { Context::FormatDetails::ascii_magic_number; }) { + if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::ascii_magic_number) + return true; + } if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::binary_magic_number) return true;