diff --git a/CMakeLists.txt b/CMakeLists.txt
index e2709046..f56da3ec 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12)
-project(SAIL VERSION 0.9.0
+project(SAIL VERSION 0.9.1
DESCRIPTION "Squirrel Abstract Image Library"
LANGUAGES C CXX)
diff --git a/FORMATS.md b/FORMATS.md
index fbd206a0..cfd4192b 100644
--- a/FORMATS.md
+++ b/FORMATS.md
@@ -239,6 +239,21 @@
- 18 |
+ 19 |
XBM |
Bit depth: 1-bit.
diff --git a/README.md b/README.md
index 11f997c7..49ca0115 100644
--- a/README.md
+++ b/README.md
@@ -79,13 +79,14 @@ images with the help of [ksquirrel-libs](FAQ.md#how-old-is-sail), the predecesso
| 8 | [JPEG XL](https://wikipedia.org/wiki/JPEG_XL) | R | libjxl |
| 9 | [PCX](https://wikipedia.org/wiki/PCX) | R | |
| 10 | [PNG](https://wikipedia.org/wiki/Portable_Network_Graphics) | RW | libpng |
-| 11 | [PSD](https://en.wikipedia.org/wiki/Adobe_Photoshop#File_format) | R | |
-| 12 | [QOI](http://qoiformat.org) | RW | |
-| 13 | [SVG](https://wikipedia.org/wiki/Scalable_Vector_Graphics) | R | resvg |
-| 14 | [TGA](https://wikipedia.org/wiki/Truevision_TGA) | R | |
-| 15 | [TIFF](https://wikipedia.org/wiki/TIFF) | RW | libtiff |
| .. | ... | | |
-| 17 | [WEBP](https://wikipedia.org/wiki/WebP) | R | libwebp |
+| 12 | [PSD](https://en.wikipedia.org/wiki/Adobe_Photoshop#File_format) | R | |
+| 13 | [QOI](http://qoiformat.org) | RW | |
+| 14 | [SVG](https://wikipedia.org/wiki/Scalable_Vector_Graphics) | R | resvg |
+| 15 | [TGA](https://wikipedia.org/wiki/Truevision_TGA) | R | |
+| 16 | [TIFF](https://wikipedia.org/wiki/TIFF) | RW | libtiff |
+| .. | ... | | |
+| 18 | [WEBP](https://wikipedia.org/wiki/WebP) | R | libwebp |
| .. | ... | | |
See the full list [here](FORMATS.md). Work to add more image formats is ongoing.
diff --git a/src/sail-codecs/CMakeLists.txt b/src/sail-codecs/CMakeLists.txt
index 2996212c..457f3a69 100644
--- a/src/sail-codecs/CMakeLists.txt
+++ b/src/sail-codecs/CMakeLists.txt
@@ -7,7 +7,7 @@ add_subdirectory(common/bmp)
set(HIGHEST_PRIORITY_CODECS gif jpeg png tiff)
set(HIGH_PRIORITY_CODECS bmp svg)
set(MEDIUM_PRIORITY_CODECS avif jpeg2000 jpegxl webp)
-set(LOW_PRIORITY_CODECS ico pcx psd qoi tga)
+set(LOW_PRIORITY_CODECS ico pcx pnm psd qoi tga)
set(LOWEST_PRIORITY_CODECS wal xbm)
set(CODECS ${HIGHEST_PRIORITY_CODECS}
diff --git a/src/sail-codecs/pnm/CMakeLists.txt b/src/sail-codecs/pnm/CMakeLists.txt
new file mode 100644
index 00000000..632a0f97
--- /dev/null
+++ b/src/sail-codecs/pnm/CMakeLists.txt
@@ -0,0 +1,3 @@
+# Common codec configuration
+#
+sail_codec(NAME pnm SOURCES helpers.h helpers.c pnm.c ICON pnm.png)
diff --git a/src/sail-codecs/pnm/helpers.c b/src/sail-codecs/pnm/helpers.c
new file mode 100644
index 00000000..8148514a
--- /dev/null
+++ b/src/sail-codecs/pnm/helpers.c
@@ -0,0 +1,169 @@
+/* This file is part of SAIL (https://github.com/HappySeaFox/sail)
+
+ Copyright (c) 2023 Dmitry Baryshev
+
+ The MIT License
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "helpers.h"
+
+sail_status_t pnm_private_skip_to_letters_numbers_force_read(struct sail_io *io, char *first_char) {
+
+ char c;
+
+ do {
+ SAIL_TRY(io->strict_read(io->stream, &c, 1));
+
+ if (c == '#') {
+ do {
+ SAIL_TRY(io->strict_read(io->stream, &c, 1));
+ } while(c != '\n');
+ }
+ } while (!isalnum(c));
+
+ *first_char = c;
+
+ return SAIL_OK;
+}
+
+sail_status_t pnm_private_skip_to_letters_numbers(struct sail_io *io, char starting_char, char *first_char) {
+
+ if (isalnum(starting_char)) {
+ *first_char = starting_char;
+ return SAIL_OK;
+ }
+
+ SAIL_TRY(pnm_private_skip_to_letters_numbers_force_read(io, first_char));
+
+ return SAIL_OK;
+}
+
+sail_status_t pnm_private_read_word(struct sail_io *io, char *str, size_t str_size) {
+
+ if (str_size < 2) {
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_INVALID_ARGUMENT);
+ }
+
+ char first_char;
+ SAIL_TRY(pnm_private_skip_to_letters_numbers(io, SAIL_PNM_INVALID_STARTING_CHAR, &first_char));
+
+ unsigned i = 0;
+ char c = first_char;
+
+ bool eof;
+ SAIL_TRY(io->eof(io->stream, &eof));
+
+ sail_status_t saved_status = SAIL_OK;
+
+ while (isalnum(c) && i < str_size - 1 && !eof) {
+ *(str + i++) = c;
+ SAIL_TRY_OR_EXECUTE(io->strict_read(io->stream, &c, 1),
+ /* on error */ saved_status = __sail_status);
+ SAIL_TRY(io->eof(io->stream, &eof));
+
+ if (saved_status != SAIL_OK && !eof) {
+ return saved_status;
+ }
+ }
+
+ /* The buffer is full but no word delimiter found. */
+ if (i == str_size - 1 && !eof) {
+ SAIL_LOG_ERROR("PNM: No word delimiter found");
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_BROKEN_IMAGE);
+ }
+
+ *(str + i) = '\0';
+
+ return SAIL_OK;
+}
+
+sail_status_t pnm_private_read_pixels(struct sail_io *io, struct sail_image *image, unsigned channels, unsigned bpc, double multiplier_to_full_range) {
+
+ for (unsigned row = 0; row < image->height; row++) {
+ uint8_t *scan8 = sail_scan_line(image, row);
+ uint16_t *scan16 = sail_scan_line(image, row);
+
+ for (unsigned column = 0; column < image->width; column++) {
+ for(unsigned channel = 0; channel < channels; channel++) {
+ char buffer[8];
+ SAIL_TRY(pnm_private_read_word(io, buffer, sizeof(buffer)));
+
+ unsigned value;
+ #ifdef _MSC_VER
+ if (sscanf_s(buffer, "%u", &value) != 1) {
+ #else
+ if (sscanf(buffer, "%u", &value) != 1) {
+ #endif
+ SAIL_LOG_ERROR("PNM: Failed to read color value");
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_BROKEN_IMAGE);
+ }
+
+ if (SAIL_LIKELY(bpc == 8)) {
+ *scan8++ = (uint8_t)(value * multiplier_to_full_range);
+ } else {
+ *scan16++ = (uint16_t)(value * multiplier_to_full_range);
+ }
+ }
+ }
+ }
+
+ return SAIL_OK;
+}
+
+enum SailPixelFormat pnm_private_rgb_sail_pixel_format(enum SailPnmVersion pnm_version, unsigned bpc) {
+
+ switch (pnm_version) {
+ case SAIL_PNM_VERSION_P1:
+ case SAIL_PNM_VERSION_P4: return SAIL_PIXEL_FORMAT_BPP1_INDEXED;
+
+ case SAIL_PNM_VERSION_P2:
+ case SAIL_PNM_VERSION_P5: {
+ switch (bpc) {
+ case 8: return SAIL_PIXEL_FORMAT_BPP8_GRAYSCALE;
+ case 16: return SAIL_PIXEL_FORMAT_BPP16_GRAYSCALE;
+
+ default: return SAIL_PIXEL_FORMAT_UNKNOWN;
+ }
+ }
+
+ case SAIL_PNM_VERSION_P3:
+ case SAIL_PNM_VERSION_P6: {
+ switch (bpc) {
+ case 8: return SAIL_PIXEL_FORMAT_BPP24_RGB;
+ case 16: return SAIL_PIXEL_FORMAT_BPP48_RGB;
+
+ default: return SAIL_PIXEL_FORMAT_UNKNOWN;
+ }
+ }
+
+ default: {
+ return SAIL_PIXEL_FORMAT_UNKNOWN;
+ }
+ }
+}
+
diff --git a/src/sail-codecs/pnm/helpers.h b/src/sail-codecs/pnm/helpers.h
new file mode 100644
index 00000000..dcec56e9
--- /dev/null
+++ b/src/sail-codecs/pnm/helpers.h
@@ -0,0 +1,59 @@
+/* This file is part of SAIL (https://github.com/HappySeaFox/sail)
+
+ Copyright (c) 2023 Dmitry Baryshev
+
+ The MIT License
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+#ifndef SAIL_PNM_HELPERS_H
+#define SAIL_PNM_HELPERS_H
+
+#include /* size_t */
+
+#include
+#include
+#include
+
+struct sail_image;
+struct sail_io;
+
+enum SailPnmVersion {
+ SAIL_PNM_VERSION_P1,
+ SAIL_PNM_VERSION_P2,
+ SAIL_PNM_VERSION_P3,
+ SAIL_PNM_VERSION_P4,
+ SAIL_PNM_VERSION_P5,
+ SAIL_PNM_VERSION_P6,
+};
+
+static const char SAIL_PNM_INVALID_STARTING_CHAR = '\0';
+
+SAIL_HIDDEN sail_status_t pnm_private_skip_to_letters_numbers_force_read(struct sail_io *io, char *first_char);
+
+SAIL_HIDDEN sail_status_t pnm_private_skip_to_letters_numbers(struct sail_io *io, char starting_char, char *first_char);
+
+SAIL_HIDDEN sail_status_t pnm_private_read_word(struct sail_io *io, char *str, size_t str_size);
+
+SAIL_HIDDEN sail_status_t pnm_private_read_pixels(struct sail_io *io, struct sail_image *image, unsigned channels, unsigned bpc, double multiplier_to_full_range);
+
+SAIL_HIDDEN enum SailPixelFormat pnm_private_rgb_sail_pixel_format(enum SailPnmVersion pnm_version, unsigned bpc);
+
+#endif
diff --git a/src/sail-codecs/pnm/pnm.c b/src/sail-codecs/pnm/pnm.c
new file mode 100644
index 00000000..886cf459
--- /dev/null
+++ b/src/sail-codecs/pnm/pnm.c
@@ -0,0 +1,331 @@
+/* This file is part of SAIL (https://github.com/HappySeaFox/sail)
+
+ Copyright (c) 2023 Dmitry Baryshev
+
+ The MIT License
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+*/
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include "helpers.h"
+
+static const uint8_t SAIL_PNM_MONO_PALETTE[] = { 255, 255, 255, 0, 0, 0 };
+
+/*
+ * Codec-specific state.
+ */
+struct pnm_state {
+ struct sail_io *io;
+ const struct sail_load_options *load_options;
+ const struct sail_save_options *save_options;
+
+ bool frame_loaded;
+ enum SailPnmVersion version;
+ double multiplier_to_full_range;
+ unsigned bpc;
+};
+
+static sail_status_t alloc_pnm_state(struct sail_io *io,
+ const struct sail_load_options *load_options,
+ const struct sail_save_options *save_options,
+ struct pnm_state **pnm_state) {
+
+ void *ptr;
+ SAIL_TRY(sail_malloc(sizeof(struct pnm_state), &ptr));
+ *pnm_state = ptr;
+
+ **pnm_state = (struct pnm_state) {
+ .io = io,
+ .load_options = load_options,
+ .save_options = save_options,
+
+ .frame_loaded = false,
+
+ .multiplier_to_full_range = 0,
+ .bpc = 0,
+ };
+
+ return SAIL_OK;
+}
+
+static void destroy_pnm_state(struct pnm_state *pnm_state) {
+
+ if (pnm_state == NULL) {
+ return;
+ }
+
+ sail_free(pnm_state);
+}
+
+/*
+ * Decoding functions.
+ */
+
+SAIL_EXPORT sail_status_t sail_codec_load_init_v8_pnm(struct sail_io *io, const struct sail_load_options *load_options, void **state) {
+
+ *state = NULL;
+
+ /* Allocate a new state. */
+ struct pnm_state *pnm_state;
+ SAIL_TRY(alloc_pnm_state(io, load_options, NULL, &pnm_state));
+ *state = pnm_state;
+
+ /* Init decoder. */
+ char str[8];
+ SAIL_TRY(pnm_private_read_word(pnm_state->io, str, sizeof(str)));
+
+ const char pnm = str[1];
+
+ SAIL_LOG_TRACE("PNM: Version '%c'", pnm);
+
+ switch (pnm) {
+ case '1': pnm_state->version = SAIL_PNM_VERSION_P1; break;
+ case '2': pnm_state->version = SAIL_PNM_VERSION_P2; break;
+ case '3': pnm_state->version = SAIL_PNM_VERSION_P3; break;
+ case '4': pnm_state->version = SAIL_PNM_VERSION_P4; break;
+ case '5': pnm_state->version = SAIL_PNM_VERSION_P5; break;
+ case '6': pnm_state->version = SAIL_PNM_VERSION_P6; break;
+
+ default: {
+ SAIL_LOG_ERROR("PNM: Unsupported version '%c'", pnm);
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_UNSUPPORTED_FORMAT);
+ }
+ }
+
+ return SAIL_OK;
+}
+
+SAIL_EXPORT sail_status_t sail_codec_load_seek_next_frame_v8_pnm(void *state, struct sail_image **image) {
+
+ struct pnm_state *pnm_state = state;
+
+ if (pnm_state->frame_loaded) {
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_NO_MORE_FRAMES);
+ }
+
+ pnm_state->frame_loaded = true;
+
+ char buffer[32];
+
+ /* Dimensions. */
+ unsigned w;
+ SAIL_TRY(pnm_private_read_word(pnm_state->io, buffer, sizeof(buffer)));
+
+#ifdef _MSC_VER
+ if (sscanf_s(buffer, "%u", &w) != 1) {
+#else
+ if (sscanf(buffer, "%u", &w) != 1) {
+#endif
+ SAIL_LOG_ERROR("PNM: Failed to read image dimensions");
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_BROKEN_IMAGE);
+ }
+
+ unsigned h;
+ SAIL_TRY(pnm_private_read_word(pnm_state->io, buffer, sizeof(buffer)));
+
+#ifdef _MSC_VER
+ if (sscanf_s(buffer, "%u", &h) != 1) {
+#else
+ if (sscanf(buffer, "%u", &h) != 1) {
+#endif
+ SAIL_LOG_ERROR("PNM: Failed to read image dimensions");
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_BROKEN_IMAGE);
+ }
+
+ /* Maximum color. */
+ if (pnm_state->version == SAIL_PNM_VERSION_P2 ||
+ pnm_state->version == SAIL_PNM_VERSION_P3 ||
+ pnm_state->version == SAIL_PNM_VERSION_P5 ||
+ pnm_state->version == SAIL_PNM_VERSION_P6) {
+
+ SAIL_TRY(pnm_private_read_word(pnm_state->io, buffer, sizeof(buffer)));
+
+ unsigned max_color;
+#ifdef _MSC_VER
+ if (sscanf_s(buffer, "%u", &max_color) != 1) {
+#else
+ if (sscanf(buffer, "%u", &max_color) != 1) {
+#endif
+ SAIL_LOG_ERROR("PNM: Failed to read maximum color value");
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_BROKEN_IMAGE);
+ }
+
+ if (max_color <= 255) {
+ pnm_state->bpc = 8;
+ pnm_state->multiplier_to_full_range = 255.0 / max_color;
+ } else if (max_color <= 65535) {
+ pnm_state->bpc = 16;
+ pnm_state->multiplier_to_full_range = 65535.0 / max_color;
+ } else {
+ SAIL_LOG_ERROR("PNM: BPP more than 16 is not supported");
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_UNSUPPORTED_FORMAT);
+ }
+
+ SAIL_LOG_TRACE("PNM: Max color(%u), scale(%.1f)", max_color, pnm_state->multiplier_to_full_range);
+ } else {
+ pnm_state->multiplier_to_full_range = 1;
+ pnm_state->bpc = 1;
+ }
+
+ enum SailPixelFormat pixel_format = pnm_private_rgb_sail_pixel_format(pnm_state->version, pnm_state->bpc);
+
+ if (pixel_format == SAIL_PIXEL_FORMAT_UNKNOWN) {
+ SAIL_LOG_ERROR("PNM: Unsupported pixel format");
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_UNSUPPORTED_PIXEL_FORMAT);
+ }
+
+ struct sail_image *image_local;
+ SAIL_TRY(sail_alloc_image(&image_local));
+
+ if (pnm_state->load_options->options & SAIL_OPTION_SOURCE_IMAGE) {
+ SAIL_TRY_OR_CLEANUP(sail_alloc_source_image(&image_local->source_image),
+ /* cleanup */ sail_destroy_image(image_local));
+
+ image_local->source_image->pixel_format = pixel_format;
+ image_local->source_image->compression = SAIL_COMPRESSION_NONE;
+ }
+
+ image_local->width = w;
+ image_local->height = h;
+ image_local->pixel_format = pixel_format;
+ image_local->delay = -1;
+ image_local->bytes_per_line = sail_bytes_per_line(image_local->width, image_local->pixel_format);
+
+ if (pixel_format == SAIL_PIXEL_FORMAT_BPP1_INDEXED) {
+ SAIL_TRY_OR_CLEANUP(sail_alloc_palette_for_data(SAIL_PIXEL_FORMAT_BPP24_RGB, 2, &image_local->palette),
+ /* cleanup */ sail_destroy_image(image_local));
+
+ memcpy(image_local->palette->data, SAIL_PNM_MONO_PALETTE, 6);
+ }
+
+ *image = image_local;
+
+ return SAIL_OK;
+}
+
+SAIL_EXPORT sail_status_t sail_codec_load_frame_v8_pnm(void *state, struct sail_image *image) {
+
+ const struct pnm_state *pnm_state = state;
+
+ switch (pnm_state->version) {
+ case SAIL_PNM_VERSION_P1: {
+ for (unsigned row = 0; row < image->height; row++) {
+ uint8_t *scan = sail_scan_line(image, row);
+ unsigned shift = 8;
+
+ for (unsigned column = 0; column < image->width; column++) {
+ char first_char;
+ SAIL_TRY(pnm_private_skip_to_letters_numbers_force_read(pnm_state->io, &first_char));
+
+ const unsigned value = first_char - '0';
+
+ if (value != 0 && value != 1) {
+ SAIL_LOG_ERROR("PNM: Unexpected character '%c'", first_char);
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_BROKEN_IMAGE);
+ }
+
+ if (shift == 8) {
+ *scan = 0;
+ }
+
+ *scan |= (value << --shift);
+
+ if (shift == 0) {
+ scan++;
+ shift = 8;
+ }
+ }
+ }
+ break;
+ }
+ case SAIL_PNM_VERSION_P2: {
+ SAIL_TRY(pnm_private_read_pixels(pnm_state->io, image, 1, pnm_state->bpc, pnm_state->multiplier_to_full_range));
+ break;
+ }
+ case SAIL_PNM_VERSION_P3: {
+ SAIL_TRY(pnm_private_read_pixels(pnm_state->io, image, 3, pnm_state->bpc, pnm_state->multiplier_to_full_range));
+ break;
+ }
+ case SAIL_PNM_VERSION_P4:
+ case SAIL_PNM_VERSION_P5:
+ case SAIL_PNM_VERSION_P6: {
+ for (unsigned row = 0; row < image->height; row++) {
+ SAIL_TRY(pnm_state->io->strict_read(pnm_state->io->stream, sail_scan_line(image, row), image->bytes_per_line));
+ }
+ break;
+ }
+ }
+
+ return SAIL_OK;
+}
+
+SAIL_EXPORT sail_status_t sail_codec_load_finish_v8_pnm(void **state) {
+
+ struct pnm_state *pnm_state = *state;
+
+ *state = NULL;
+
+ destroy_pnm_state(pnm_state);
+
+ return SAIL_OK;
+}
+
+/*
+ * Encoding functions.
+ */
+
+SAIL_EXPORT sail_status_t sail_codec_save_init_v8_pnm(struct sail_io *io, const struct sail_save_options *save_options, void **state) {
+
+ (void)io;
+ (void)save_options;
+ (void)state;
+
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_NOT_IMPLEMENTED);
+}
+
+SAIL_EXPORT sail_status_t sail_codec_save_seek_next_frame_v8_pnm(void *state, const struct sail_image *image) {
+
+ (void)state;
+ (void)image;
+
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_NOT_IMPLEMENTED);
+}
+
+SAIL_EXPORT sail_status_t sail_codec_save_frame_v8_pnm(void *state, const struct sail_image *image) {
+
+ (void)state;
+ (void)image;
+
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_NOT_IMPLEMENTED);
+}
+
+SAIL_EXPORT sail_status_t sail_codec_save_finish_v8_pnm(void **state) {
+
+ (void)state;
+
+ SAIL_LOG_AND_RETURN(SAIL_ERROR_NOT_IMPLEMENTED);
+}
diff --git a/src/sail-codecs/pnm/pnm.codec.info.in b/src/sail-codecs/pnm/pnm.codec.info.in
new file mode 100644
index 00000000..6e4e991a
--- /dev/null
+++ b/src/sail-codecs/pnm/pnm.codec.info.in
@@ -0,0 +1,26 @@
+# PNM codec information
+#
+[codec]
+layout=8
+version=0.6.5
+priority=LOW
+name=PNM
+description=Portable aNy Map
+magic-numbers=50 31;50 32;50 33;50 34;50 35;50 36
+extensions=pbm;pgm;ppm;pnm
+mime-types=image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-portable-anymap
+
+[load-features]
+features=STATIC;SOURCE-IMAGE
+tuning=
+
+[save-features]
+features=
+pixel-formats=
+compressions=
+default-compression=
+compression-level-min=0
+compression-level-max=0
+compression-level-default=0
+compression-level-step=0
+tuning=
diff --git a/src/sail-codecs/pnm/pnm.png b/src/sail-codecs/pnm/pnm.png
new file mode 100644
index 00000000..0f448d2a
Binary files /dev/null and b/src/sail-codecs/pnm/pnm.png differ
diff --git a/src/sail/io_file.h b/src/sail/io_file.h
index 9d6b757a..cfb221c2 100644
--- a/src/sail/io_file.h
+++ b/src/sail/io_file.h
@@ -37,7 +37,7 @@ struct sail_io;
/*
* Opens the specified image file for reading and allocates a new I/O object for it.
- * sail_io.stream is a pointer to a FILE.
+ * sail_io.stream is a pointer to a FILE. fread/fwrite/fseek/feof are used underneath.
*
* Returns SAIL_OK on success.
*/
diff --git a/tests/images/pnm/bpp1-grayscale.ascii.pbm b/tests/images/pnm/bpp1-grayscale.ascii.pbm
new file mode 100644
index 00000000..caa729d6
--- /dev/null
+++ b/tests/images/pnm/bpp1-grayscale.ascii.pbm
@@ -0,0 +1,34 @@
+P1
+32 32
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 11111111
+11111111 11111111 11111111 00000000
+11111111 11111111 11111111 00000000
+11111111 11111111 11111111 00000000
+11111111 11111111 11111111 00000000
+11111111 11111111 11111111 00000000
+11111111 11111111 11111111 00000000
+11111111 11111111 11111111 00000000
+11111111 11111111 11111111 00000000
+00000000 00000000 00000000 00000000
+00000000 00000000 00000000 00000000
+00000000 00000000 00000000 00000000
+00000000 00000000 00000000 00000000
+00000000 00000000 00000000 00000000
+00000000 00000000 00000000 00000000
+00000000 00000000 00000000 00000000
+00000000 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
+11111111 00000000 00000000 00000000
\ No newline at end of file
diff --git a/tests/images/pnm/bpp1-grayscale.raw.pbm b/tests/images/pnm/bpp1-grayscale.raw.pbm
new file mode 100644
index 00000000..8ebfe948
Binary files /dev/null and b/tests/images/pnm/bpp1-grayscale.raw.pbm differ
diff --git a/tests/images/pnm/bpp24-rgb.ascii.pnm b/tests/images/pnm/bpp24-rgb.ascii.pnm
new file mode 100644
index 00000000..6e82e9bd
--- /dev/null
+++ b/tests/images/pnm/bpp24-rgb.ascii.pnm
@@ -0,0 +1,3076 @@
+P3
+# Created by GIMP version 2.10.36 PNM plug-in
+32 32
+255
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+0
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+0
+128
+128
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+192
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+128
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+1
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+0
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+255
+0
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+1
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
+255
diff --git a/tests/images/pnm/bpp24-rgb.raw.pnm b/tests/images/pnm/bpp24-rgb.raw.pnm
new file mode 100644
index 00000000..77c67294
Binary files /dev/null and b/tests/images/pnm/bpp24-rgb.raw.pnm differ
diff --git a/tests/images/pnm/bpp8-grayscale.ascii.pgm b/tests/images/pnm/bpp8-grayscale.ascii.pgm
new file mode 100644
index 00000000..207de6e1
--- /dev/null
+++ b/tests/images/pnm/bpp8-grayscale.ascii.pgm
@@ -0,0 +1,1028 @@
+P2
+# Created by GIMP version 2.10.36 PNM plug-in
+32 32
+255
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+0
+0
+0
+0
+0
+0
+0
+0
+62
+62
+62
+62
+62
+62
+62
+62
+110
+110
+110
+110
+110
+110
+110
+110
+124
+124
+124
+124
+124
+124
+124
+124
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+30
+30
+30
+30
+30
+30
+30
+30
+70
+70
+70
+70
+70
+70
+70
+70
+114
+114
+114
+114
+114
+114
+114
+114
+192
+192
+192
+192
+192
+192
+192
+192
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+128
+128
+128
+128
+128
+128
+128
+128
+130
+130
+130
+130
+130
+130
+130
+130
+220
+220
+220
+220
+220
+220
+220
+220
+248
+248
+248
+248
+248
+248
+248
+248
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
+70
+70
+70
+70
+70
+70
+70
+70
+145
+145
+145
+145
+145
+145
+145
+145
+228
+228
+228
+228
+228
+228
+228
+228
+255
+255
+255
+255
+255
+255
+255
+255
diff --git a/tests/images/pnm/bpp8-grayscale.raw.pgm b/tests/images/pnm/bpp8-grayscale.raw.pgm
new file mode 100644
index 00000000..3dc48079
Binary files /dev/null and b/tests/images/pnm/bpp8-grayscale.raw.pgm differ
diff --git a/tests/images/test-images.h.in b/tests/images/test-images.h.in
index 20092bc0..93b18856 100644
--- a/tests/images/test-images.h.in
+++ b/tests/images/test-images.h.in
@@ -58,6 +58,15 @@ static const char * const SAIL_TEST_IMAGES[] = {
"@SAIL_TEST_IMAGES_PATH@/png/bpp4-indexed.comment.iccp.png",
#endif
+#ifdef SAIL_HAVE_BUILTIN_PNM
+ "@SAIL_TEST_IMAGES_PATH@/pnm/bpp1-grayscale.ascii.pbm",
+ "@SAIL_TEST_IMAGES_PATH@/pnm/bpp1-grayscale.raw.pbm",
+ "@SAIL_TEST_IMAGES_PATH@/pnm/bpp24-rgb.ascii.pnm",
+ "@SAIL_TEST_IMAGES_PATH@/pnm/bpp24-rgb.raw.pnm",
+ "@SAIL_TEST_IMAGES_PATH@/pnm/bpp8-grayscale.ascii.pgm",
+ "@SAIL_TEST_IMAGES_PATH@/pnm/bpp8-grayscale.raw.pgm",
+#endif
+
#ifdef SAIL_HAVE_BUILTIN_PSD
"@SAIL_TEST_IMAGES_PATH@/psd/bpp8-indexed.psd",
"@SAIL_TEST_IMAGES_PATH@/psd/bpp24-rgb.psd",
|