mirror of
https://github.com/SerenityOS/serenity
synced 2024-07-22 02:26:11 +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:
parent
0d76a9da17
commit
187862ebe0
|
@ -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/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-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-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-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-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 } },
|
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()
|
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(jpeg, ".jpg") \
|
||||||
__ENUMERATE_IMAGE_FORMAT(jxl, ".jxl") \
|
__ENUMERATE_IMAGE_FORMAT(jxl, ".jxl") \
|
||||||
__ENUMERATE_IMAGE_FORMAT(iff, ".lbm") \
|
__ENUMERATE_IMAGE_FORMAT(iff, ".lbm") \
|
||||||
|
__ENUMERATE_IMAGE_FORMAT(pam, ".pam") \
|
||||||
__ENUMERATE_IMAGE_FORMAT(pbm, ".pbm") \
|
__ENUMERATE_IMAGE_FORMAT(pbm, ".pbm") \
|
||||||
__ENUMERATE_IMAGE_FORMAT(pgm, ".pgm") \
|
__ENUMERATE_IMAGE_FORMAT(pgm, ".pgm") \
|
||||||
__ENUMERATE_IMAGE_FORMAT(png, ".png") \
|
__ENUMERATE_IMAGE_FORMAT(png, ".png") \
|
||||||
|
|
|
@ -55,6 +55,7 @@ set(SOURCES
|
||||||
ImageFormats/PNGLoader.cpp
|
ImageFormats/PNGLoader.cpp
|
||||||
ImageFormats/PNGWriter.cpp
|
ImageFormats/PNGWriter.cpp
|
||||||
ImageFormats/PortableFormatWriter.cpp
|
ImageFormats/PortableFormatWriter.cpp
|
||||||
|
ImageFormats/PAMLoader.cpp
|
||||||
ImageFormats/PPMLoader.cpp
|
ImageFormats/PPMLoader.cpp
|
||||||
ImageFormats/QOILoader.cpp
|
ImageFormats/QOILoader.cpp
|
||||||
ImageFormats/QOIWriter.cpp
|
ImageFormats/QOIWriter.cpp
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
#include <LibGfx/ImageFormats/ImageDecoder.h>
|
#include <LibGfx/ImageFormats/ImageDecoder.h>
|
||||||
#include <LibGfx/ImageFormats/JPEGLoader.h>
|
#include <LibGfx/ImageFormats/JPEGLoader.h>
|
||||||
#include <LibGfx/ImageFormats/JPEGXLLoader.h>
|
#include <LibGfx/ImageFormats/JPEGXLLoader.h>
|
||||||
|
#include <LibGfx/ImageFormats/PAMLoader.h>
|
||||||
#include <LibGfx/ImageFormats/PBMLoader.h>
|
#include <LibGfx/ImageFormats/PBMLoader.h>
|
||||||
#include <LibGfx/ImageFormats/PGMLoader.h>
|
#include <LibGfx/ImageFormats/PGMLoader.h>
|
||||||
#include <LibGfx/ImageFormats/PNGLoader.h>
|
#include <LibGfx/ImageFormats/PNGLoader.h>
|
||||||
|
@ -40,6 +41,7 @@ static OwnPtr<ImageDecoderPlugin> probe_and_sniff_for_appropriate_plugin(Readonl
|
||||||
{ ILBMImageDecoderPlugin::sniff, ILBMImageDecoderPlugin::create },
|
{ ILBMImageDecoderPlugin::sniff, ILBMImageDecoderPlugin::create },
|
||||||
{ JPEGImageDecoderPlugin::sniff, JPEGImageDecoderPlugin::create },
|
{ JPEGImageDecoderPlugin::sniff, JPEGImageDecoderPlugin::create },
|
||||||
{ JPEGXLImageDecoderPlugin::sniff, JPEGXLImageDecoderPlugin::create },
|
{ JPEGXLImageDecoderPlugin::sniff, JPEGXLImageDecoderPlugin::create },
|
||||||
|
{ PAMImageDecoderPlugin::sniff, PAMImageDecoderPlugin::create },
|
||||||
{ PBMImageDecoderPlugin::sniff, PBMImageDecoderPlugin::create },
|
{ PBMImageDecoderPlugin::sniff, PBMImageDecoderPlugin::create },
|
||||||
{ PGMImageDecoderPlugin::sniff, PGMImageDecoderPlugin::create },
|
{ PGMImageDecoderPlugin::sniff, PGMImageDecoderPlugin::create },
|
||||||
{ PNGImageDecoderPlugin::sniff, PNGImageDecoderPlugin::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;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline ErrorOr<u16> read_number(SeekableStream& stream)
|
inline ErrorOr<String> read_token(SeekableStream& stream)
|
||||||
{
|
{
|
||||||
StringBuilder sb {};
|
StringBuilder sb {};
|
||||||
u8 byte {};
|
u8 byte {};
|
||||||
|
@ -43,7 +43,12 @@ static inline ErrorOr<u16> read_number(SeekableStream& stream)
|
||||||
sb.append(byte);
|
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())
|
if (!maybe_value.has_value())
|
||||||
return Error::from_string_literal("Can't convert bytes to a number");
|
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 {};
|
Array<u8, 2> magic_number {};
|
||||||
TRY(context.stream->read_until_filled(Bytes { magic_number }));
|
TRY(context.stream->read_until_filled(Bytes { magic_number }));
|
||||||
|
|
||||||
if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::ascii_magic_number) {
|
if constexpr (requires { TContext::FormatDetails::ascii_magic_number; }) {
|
||||||
context.type = TContext::Type::ASCII;
|
if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::ascii_magic_number) {
|
||||||
return {};
|
context.type = TContext::Type::ASCII;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::binary_magic_number) {
|
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 {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename Context>
|
||||||
|
static ErrorOr<void> read_pam_header(Context& context);
|
||||||
|
|
||||||
template<typename TContext>
|
template<typename TContext>
|
||||||
static ErrorOr<void> decode(TContext& context)
|
static ErrorOr<void> decode(TContext& context)
|
||||||
{
|
{
|
||||||
|
|
|
@ -85,7 +85,10 @@ ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> PortableImageDecoderPlugin<TContext>:
|
||||||
{
|
{
|
||||||
auto stream = TRY(try_make<FixedMemoryStream>(data));
|
auto stream = TRY(try_make<FixedMemoryStream>(data));
|
||||||
auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) PortableImageDecoderPlugin<TContext>(move(stream))));
|
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;
|
return plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,8 +99,10 @@ bool PortableImageDecoderPlugin<TContext>::sniff(ReadonlyBytes data)
|
||||||
if (data.size() < 2)
|
if (data.size() < 2)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::ascii_magic_number)
|
if constexpr (requires { Context::FormatDetails::ascii_magic_number; }) {
|
||||||
return true;
|
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)
|
if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::binary_magic_number)
|
||||||
return true;
|
return true;
|
||||||
|
|
Loading…
Reference in a new issue