Skip to content

Commit 187862e

Browse files
nicoawesomekling
authored andcommitted
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.
1 parent 0d76a9d commit 187862e

File tree

9 files changed

+173
-9
lines changed

9 files changed

+173
-9
lines changed

Userland/Libraries/LibCore/MimeData.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ static Array const s_registered_mime_type = {
124124
MimeType { .name = "image/webp"sv, .common_extensions = { ".webp"sv }, .description = "WebP image data"sv, .magic_bytes = Vector<u8> { 'W', 'E', 'B', 'P' }, .offset = 8 },
125125
MimeType { .name = "image/x-icon"sv, .common_extensions = { ".ico"sv }, .description = "ICO image data"sv },
126126
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 } },
127+
MimeType { .name = "image/x-portable-arbitrarymap"sv, .common_extensions = { ".pam"sv }, .description = "PAM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x37, 0x0A } },
127128
MimeType { .name = "image/x-portable-bitmap"sv, .common_extensions = { ".pbm"sv }, .description = "PBM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x31, 0x0A } },
128129
MimeType { .name = "image/x-portable-graymap"sv, .common_extensions = { ".pgm"sv }, .description = "PGM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x32, 0x0A } },
129130
MimeType { .name = "image/x-portable-pixmap"sv, .common_extensions = { ".ppm"sv }, .description = "PPM image data"sv, .magic_bytes = Vector<u8> { 0x50, 0x33, 0x0A } },

Userland/Libraries/LibGUI/FileTypeFilter.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ struct FileTypeFilter {
2525

2626
static FileTypeFilter image_files()
2727
{
28-
return FileTypeFilter { "Image Files", Vector<ByteString> { "png", "gif", "bmp", "dip", "pbm", "pgm", "ppm", "ico", "iff", "jpeg", "jpg", "jxl", "dds", "qoi", "tif", "tiff", "webp", "tvg" } };
28+
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" } };
2929
}
3030
};
3131

Userland/Libraries/LibGfx/Bitmap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
__ENUMERATE_IMAGE_FORMAT(jpeg, ".jpg") \
2727
__ENUMERATE_IMAGE_FORMAT(jxl, ".jxl") \
2828
__ENUMERATE_IMAGE_FORMAT(iff, ".lbm") \
29+
__ENUMERATE_IMAGE_FORMAT(pam, ".pam") \
2930
__ENUMERATE_IMAGE_FORMAT(pbm, ".pbm") \
3031
__ENUMERATE_IMAGE_FORMAT(pgm, ".pgm") \
3132
__ENUMERATE_IMAGE_FORMAT(png, ".png") \

Userland/Libraries/LibGfx/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ set(SOURCES
5555
ImageFormats/PNGLoader.cpp
5656
ImageFormats/PNGWriter.cpp
5757
ImageFormats/PortableFormatWriter.cpp
58+
ImageFormats/PAMLoader.cpp
5859
ImageFormats/PPMLoader.cpp
5960
ImageFormats/QOILoader.cpp
6061
ImageFormats/QOIWriter.cpp

Userland/Libraries/LibGfx/ImageFormats/ImageDecoder.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <LibGfx/ImageFormats/ImageDecoder.h>
1414
#include <LibGfx/ImageFormats/JPEGLoader.h>
1515
#include <LibGfx/ImageFormats/JPEGXLLoader.h>
16+
#include <LibGfx/ImageFormats/PAMLoader.h>
1617
#include <LibGfx/ImageFormats/PBMLoader.h>
1718
#include <LibGfx/ImageFormats/PGMLoader.h>
1819
#include <LibGfx/ImageFormats/PNGLoader.h>
@@ -40,6 +41,7 @@ static OwnPtr<ImageDecoderPlugin> probe_and_sniff_for_appropriate_plugin(Readonl
4041
{ ILBMImageDecoderPlugin::sniff, ILBMImageDecoderPlugin::create },
4142
{ JPEGImageDecoderPlugin::sniff, JPEGImageDecoderPlugin::create },
4243
{ JPEGXLImageDecoderPlugin::sniff, JPEGXLImageDecoderPlugin::create },
44+
{ PAMImageDecoderPlugin::sniff, PAMImageDecoderPlugin::create },
4345
{ PBMImageDecoderPlugin::sniff, PBMImageDecoderPlugin::create },
4446
{ PGMImageDecoderPlugin::sniff, PGMImageDecoderPlugin::create },
4547
{ PNGImageDecoderPlugin::sniff, PNGImageDecoderPlugin::create },
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright (c) 2024, the SerenityOS developers.
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include "PAMLoader.h"
8+
#include "PortableImageLoaderCommon.h"
9+
10+
namespace Gfx {
11+
12+
ErrorOr<void> read_image_data(PAMLoadingContext& context)
13+
{
14+
VERIFY(context.type == PAMLoadingContext::Type::RAWBITS);
15+
16+
// FIXME: Technically it's more to spec to check that a known tupl type has a minimum depth and then skip additional channels.
17+
bool is_gray = context.format_details.depth == 1 && context.format_details.tupl_type == "GRAYSCALE"sv;
18+
bool is_gray_alpha = context.format_details.depth == 2 && context.format_details.tupl_type == "GRAYSCALE_ALPHA"sv;
19+
bool is_rgb = context.format_details.depth == 3 && context.format_details.tupl_type == "RGB"sv;
20+
bool is_rgba = context.format_details.depth == 4 && context.format_details.tupl_type == "RGB_ALPHA"sv;
21+
22+
if (!is_gray && !is_gray_alpha && !is_rgb && !is_rgba)
23+
return Error::from_string_view("Unsupported PAM depth"sv);
24+
25+
TRY(create_bitmap(context));
26+
27+
auto& stream = *context.stream;
28+
29+
for (u64 i = 0; i < context.width * context.height; ++i) {
30+
if (is_gray) {
31+
Array<u8, 1> pixel;
32+
TRY(stream.read_until_filled(pixel));
33+
context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[0], pixel[0] });
34+
} else if (is_gray_alpha) {
35+
Array<u8, 2> pixel;
36+
TRY(stream.read_until_filled(pixel));
37+
context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[0], pixel[0], pixel[1] });
38+
} else if (is_rgb) {
39+
Array<u8, 3> pixel;
40+
TRY(stream.read_until_filled(pixel));
41+
context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[1], pixel[2] });
42+
} else if (is_rgba) {
43+
Array<u8, 4> pixel;
44+
TRY(stream.read_until_filled(pixel));
45+
context.bitmap->set_pixel(i % context.width, i / context.width, { pixel[0], pixel[1], pixel[2], pixel[3] });
46+
}
47+
}
48+
49+
context.state = PAMLoadingContext::State::BitmapDecoded;
50+
return {};
51+
}
52+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright (c) 2024, the SerenityOS developers.
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#pragma once
8+
9+
#include <AK/StringView.h>
10+
#include <LibGfx/ImageFormats/ImageDecoder.h>
11+
#include <LibGfx/ImageFormats/PortableImageMapLoader.h>
12+
13+
namespace Gfx {
14+
15+
struct PAM {
16+
static constexpr auto binary_magic_number = '7';
17+
static constexpr StringView image_type = "PAM"sv;
18+
u16 max_val { 0 };
19+
u16 depth { 0 };
20+
String tupl_type {};
21+
};
22+
23+
using PAMLoadingContext = PortableImageMapLoadingContext<PAM>;
24+
25+
template<class Context>
26+
ErrorOr<void> read_pam_header(Context& context)
27+
{
28+
// https://netpbm.sourceforge.net/doc/pam.html
29+
TRY(read_magic_number(context));
30+
31+
Optional<u16> width;
32+
Optional<u16> height;
33+
Optional<u16> depth;
34+
Optional<u16> max_val;
35+
Optional<String> tupltype;
36+
37+
while (true) {
38+
TRY(read_whitespace(context));
39+
40+
auto const token = TRY(read_token(*context.stream));
41+
42+
if (token == "ENDHDR") {
43+
auto newline = TRY(context.stream->template read_value<u8>());
44+
if (newline != '\n')
45+
return Error::from_string_view("PAM ENDHDR not followed by newline"sv);
46+
break;
47+
}
48+
49+
TRY(read_whitespace(context));
50+
if (token == "WIDTH") {
51+
if (width.has_value())
52+
return Error::from_string_view("Duplicate PAM WIDTH field"sv);
53+
width = TRY(read_number(*context.stream));
54+
} else if (token == "HEIGHT") {
55+
if (height.has_value())
56+
return Error::from_string_view("Duplicate PAM HEIGHT field"sv);
57+
height = TRY(read_number(*context.stream));
58+
} else if (token == "DEPTH") {
59+
if (depth.has_value())
60+
return Error::from_string_view("Duplicate PAM DEPTH field"sv);
61+
depth = TRY(read_number(*context.stream));
62+
} else if (token == "MAXVAL") {
63+
if (max_val.has_value())
64+
return Error::from_string_view("Duplicate PAM MAXVAL field"sv);
65+
max_val = TRY(read_number(*context.stream));
66+
} else if (token == "TUPLTYPE") {
67+
// FIXME: tupltype should be all text until the next newline, with leading and trailing space stripped.
68+
// FIXME: If there are multipe TUPLTYPE lines, their values are all appended.
69+
tupltype = TRY(read_token(*context.stream));
70+
} else {
71+
return Error::from_string_view("Unknown PAM token"sv);
72+
}
73+
}
74+
75+
if (!width.has_value() || !height.has_value() || !depth.has_value() || !max_val.has_value())
76+
return Error::from_string_view("Missing PAM header fields"sv);
77+
context.width = *width;
78+
context.height = *height;
79+
context.format_details.depth = *depth;
80+
context.format_details.max_val = *max_val;
81+
if (tupltype.has_value())
82+
context.format_details.tupl_type = *tupltype;
83+
84+
context.state = Context::State::HeaderDecoded;
85+
86+
return {};
87+
}
88+
89+
using PAMImageDecoderPlugin = PortableImageDecoderPlugin<PAMLoadingContext>;
90+
91+
ErrorOr<void> read_image_data(PAMLoadingContext& context);
92+
}

Userland/Libraries/LibGfx/ImageFormats/PortableImageLoaderCommon.h

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ static constexpr Color adjust_color(u16 max_val, Color color)
2929
return color;
3030
}
3131

32-
static inline ErrorOr<u16> read_number(SeekableStream& stream)
32+
inline ErrorOr<String> read_token(SeekableStream& stream)
3333
{
3434
StringBuilder sb {};
3535
u8 byte {};
@@ -43,7 +43,12 @@ static inline ErrorOr<u16> read_number(SeekableStream& stream)
4343
sb.append(byte);
4444
}
4545

46-
auto const maybe_value = TRY(sb.to_string()).to_number<u16>();
46+
return TRY(sb.to_string());
47+
}
48+
49+
static inline ErrorOr<u16> read_number(SeekableStream& stream)
50+
{
51+
auto const maybe_value = TRY(read_token(stream)).to_number<u16>();
4752
if (!maybe_value.has_value())
4853
return Error::from_string_literal("Can't convert bytes to a number");
4954

@@ -81,9 +86,11 @@ static ErrorOr<void> read_magic_number(TContext& context)
8186
Array<u8, 2> magic_number {};
8287
TRY(context.stream->read_until_filled(Bytes { magic_number }));
8388

84-
if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::ascii_magic_number) {
85-
context.type = TContext::Type::ASCII;
86-
return {};
89+
if constexpr (requires { TContext::FormatDetails::ascii_magic_number; }) {
90+
if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::ascii_magic_number) {
91+
context.type = TContext::Type::ASCII;
92+
return {};
93+
}
8794
}
8895

8996
if (magic_number[0] == 'P' && magic_number[1] == TContext::FormatDetails::binary_magic_number) {
@@ -187,6 +194,9 @@ static ErrorOr<void> read_header(Context& context)
187194
return {};
188195
}
189196

197+
template<typename Context>
198+
static ErrorOr<void> read_pam_header(Context& context);
199+
190200
template<typename TContext>
191201
static ErrorOr<void> decode(TContext& context)
192202
{

Userland/Libraries/LibGfx/ImageFormats/PortableImageMapLoader.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ ErrorOr<NonnullOwnPtr<ImageDecoderPlugin>> PortableImageDecoderPlugin<TContext>:
8585
{
8686
auto stream = TRY(try_make<FixedMemoryStream>(data));
8787
auto plugin = TRY(adopt_nonnull_own_or_enomem(new (nothrow) PortableImageDecoderPlugin<TContext>(move(stream))));
88-
TRY(read_header(*plugin->m_context));
88+
if constexpr (TContext::FormatDetails::binary_magic_number == '7')
89+
TRY(read_pam_header(*plugin->m_context));
90+
else
91+
TRY(read_header(*plugin->m_context));
8992
return plugin;
9093
}
9194

@@ -96,8 +99,10 @@ bool PortableImageDecoderPlugin<TContext>::sniff(ReadonlyBytes data)
9699
if (data.size() < 2)
97100
return false;
98101

99-
if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::ascii_magic_number)
100-
return true;
102+
if constexpr (requires { Context::FormatDetails::ascii_magic_number; }) {
103+
if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::ascii_magic_number)
104+
return true;
105+
}
101106

102107
if (data.data()[0] == 'P' && data.data()[1] == Context::FormatDetails::binary_magic_number)
103108
return true;

0 commit comments

Comments
 (0)