1
0
mirror of https://github.com/SerenityOS/serenity synced 2024-07-09 05:57:31 +00:00

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.
This commit is contained in:
Nico Weber 2024-01-24 21:27:24 -05:00 committed by Andreas Kling
parent 0d76a9da17
commit 187862ebe0
9 changed files with 173 additions and 9 deletions

View File

@ -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<u8> { '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<u8> { 0x46, 0x4F, 0x52, 0x4F } },
MimeType { .name = "image/x-portable-arbitrarymap"sv, .common_extensions = { ".pam"sv }, .description = "PAM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x37, 0x0A } },
MimeType { .name = "image/x-portable-bitmap"sv, .common_extensions = { ".pbm"sv }, .description = "PBM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x31, 0x0A } },
MimeType { .name = "image/x-portable-graymap"sv, .common_extensions = { ".pgm"sv }, .description = "PGM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x32, 0x0A } },
MimeType { .name = "image/x-portable-pixmap"sv, .common_extensions = { ".ppm"sv }, .description = "PPM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x33, 0x0A } },

View File

@ -25,7 +25,7 @@ struct FileTypeFilter {
static FileTypeFilter image_files()
{
return FileTypeFilter { "Image Files", Vector<ByteString> { "png", "gif", "bmp", "dip", "pbm", "pgm", "ppm", "ico", "iff", "jpeg", "jpg", "jxl", "dds", "qoi", "tif", "tiff", "webp", "tvg" } };
return FileTypeFilter { "Image Files", Vector<ByteString> { "png", "gif", "bmp", "dip", "pam", "pbm", "pgm", "ppm", "ico", "iff", "jpeg", "jpg", "jxl", "dds", "qoi", "tif", "tiff", "webp", "tvg" } };
}
};

View File

@ -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") \

View File

@ -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

View File

@ -13,6 +13,7 @@
#include <LibGfx/ImageFormats/ImageDecoder.h>
#include <LibGfx/ImageFormats/JPEGLoader.h>
#include <LibGfx/ImageFormats/JPEGXLLoader.h>
#include <LibGfx/ImageFormats/PAMLoader.h>
#include <LibGfx/ImageFormats/PBMLoader.h>
#include <LibGfx/ImageFormats/PGMLoader.h>
#include <LibGfx/ImageFormats/PNGLoader.h>
@ -40,6 +41,7 @@ static OwnPtr<ImageDecoderPlugin> 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 },

View File

@ -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<void> 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<u8, 1> 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<u8, 2> 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<u8, 3> 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<u8, 4> 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 {};
}
}

View File

@ -0,0 +1,92 @@
/*
* Copyright (c) 2024, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/StringView.h>
#include <LibGfx/ImageFormats/ImageDecoder.h>
#include <LibGfx/ImageFormats/PortableImageMapLoader.h>
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<PAM>;
template<class Context>
ErrorOr<void> read_pam_header(Context& context)
{
// https://netpbm.sourceforge.net/doc/pam.html
TRY(read_magic_number(context));
Optional<u16> width;
Optional<u16> height;
Optional<u16> depth;
Optional<u16> max_val;
Optional<String> 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<u8>());
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<PAMLoadingContext>;
ErrorOr<void> read_image_data(PAMLoadingContext& context);
}

View File

@ -29,7 +29,7 @@ static constexpr Color adjust_color(u16 max_val, Color color)
return color;
}
static inline ErrorOr<u16> read_number(SeekableStream& stream)
inline ErrorOr<String> read_token(SeekableStream& stream)
{
StringBuilder sb {};
u8 byte {};
@ -43,7 +43,12 @@ static inline ErrorOr<u16> read_number(SeekableStream& stream)
sb.append(byte);
}
auto const maybe_value = TRY(sb.to_string()).to_number<u16>();
return TRY(sb.to_string());
}
static inline ErrorOr<u16> read_number(SeekableStream& stream)
{
auto const maybe_value = TRY(read_token(stream)).to_number<u16>();
if (!maybe_value.has_value())
return Error::from_string_literal("Can't convert bytes to a number");
@ -81,9 +86,11 @@ static ErrorOr<void> read_magic_number(TContext& context)
Array<u8, 2> 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<void> read_header(Context& context)
return {};
}
template<typename Context>
static ErrorOr<void> read_pam_header(Context& context);
template<typename TContext>
static ErrorOr<void> decode(TContext& context)
{

View File

@ -85,7 +85,10 @@ ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> PortableImageDecoderPlugin<TContext>:
{
auto stream = TRY(try_make<FixedMemoryStream>(data));
auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) PortableImageDecoderPlugin<TContext>(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<TContext>::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;