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:
parent
0d76a9da17
commit
187862ebe0
Notes:
sideshowbarker
2024-07-17 05:09:48 +09:00
Author: https://github.com/nico Commit: https://github.com/SerenityOS/serenity/commit/187862ebe0 Pull-request: https://github.com/SerenityOS/serenity/pull/22935 Reviewed-by: https://github.com/LucasChollet ✅
9 changed files with 173 additions and 9 deletions
Userland/Libraries
|
@ -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 } },
|
||||
|
|
|
@ -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" } };
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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") \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
52
Userland/Libraries/LibGfx/ImageFormats/PAMLoader.cpp
Normal file
52
Userland/Libraries/LibGfx/ImageFormats/PAMLoader.cpp
Normal 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 {};
|
||||
}
|
||||
}
|
92
Userland/Libraries/LibGfx/ImageFormats/PAMLoader.h
Normal file
92
Userland/Libraries/LibGfx/ImageFormats/PAMLoader.h
Normal 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);
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue