summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorzlago2024-09-24 20:54:48 +0200
committerzlago2024-09-24 20:54:48 +0200
commitb23a3ab831f91553d34a48f51370ed9525de07ac (patch)
treebd4adf20ba92d17433f386c0b5347a8d8cc9045f /src
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/input.h21
-rw-r--r--src/libplum.c8490
-rw-r--r--src/libplum.h394
-rw-r--r--src/loader.c368
-rw-r--r--src/loader.h16
-rw-r--r--src/main.c178
-rw-r--r--src/main.h3
-rw-r--r--src/tilemap.c125
-rw-r--r--src/tilemap.h5
-rw-r--r--src/util.c66
-rw-r--r--src/util.h2
-rw-r--r--src/zip.c168
-rw-r--r--src/zip.h80
13 files changed, 9916 insertions, 0 deletions
diff --git a/src/input.h b/src/input.h
new file mode 100644
index 0000000..9830a49
--- /dev/null
+++ b/src/input.h
@@ -0,0 +1,21 @@
+#pragma once
+
+enum _inputs {
+	INPUT_UP,
+	INPUT_DOWN,
+	INPUT_LEFT,
+	INPUT_RIGHT,
+	
+	INPUT_A,
+	INPUT_S,
+
+	INPUT_LENGTH // no. of checked inputs
+};
+
+#define input_up(a)    (1 & a >> INPUT_UP)
+#define input_down(a)  (1 & a >> INPUT_DOWN)
+#define input_left(a)  (1 & a >> INPUT_LEFT)
+#define input_right(a) (1 & a >> INPUT_RIGHT)
+
+#define input_a(a)     (1 & a >> INPUT_A)
+#define input_s(a)     (1 & a >> INPUT_S)
diff --git a/src/libplum.c b/src/libplum.c
new file mode 100644
index 0000000..5be1668
--- /dev/null
+++ b/src/libplum.c
@@ -0,0 +1,8490 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdalign.h>
+#include <setjmp.h>
+
+#ifndef PLUM_DEFS
+
+#define PLUM_DEFS
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdalign.h>
+
+#if defined(PLUM_NO_STDINT) || defined(PLUM_NO_ANON_MEMBERS) || defined(PLUM_NO_VLA)
+  #error libplum feature-test macros must not be defined when compiling the library.
+#elif defined(__cplusplus)
+  #error libplum cannot be compiled with a C++ compiler.
+#elif __STDC_VERSION__ < 201710L
+  #error libplum requires C17 or later.
+#elif SIZE_MAX < 0xffffffffu
+  #error libplum requires size_t to be at least 32 bits wide.
+#endif
+
+#ifdef noreturn
+  #undef noreturn
+#endif
+#define noreturn _Noreturn void
+
+#ifdef PLUM_DEBUG
+  #define internal
+#else
+  #define internal static
+#endif
+
+#define alignto(amount) alignas(((amount) < alignof(max_align_t)) ? (amount) : alignof(max_align_t))
+
+#define bytematch(address, ...) (!memcmp((address), (unsigned char []) {__VA_ARGS__}, sizeof (unsigned char []) {__VA_ARGS__}))
+#define bytewrite(address, ...) (memcpy(address, (unsigned char []) {__VA_ARGS__}, sizeof (unsigned char []) {__VA_ARGS__}))
+#define byteoutput(context, ...) (bytewrite(append_output_node((context), sizeof (unsigned char []) {__VA_ARGS__}), __VA_ARGS__))
+#define byteappend(address, ...) (bytewrite(address, __VA_ARGS__), sizeof (unsigned char []) {__VA_ARGS__})
+
+#define swap(T, first, second) do {T temp = first; first = second; second = temp;} while (false)
+
+#endif
+
+#ifndef PLUM_HEADER
+
+#define PLUM_HEADER
+
+#define PLUM_VERSION 10029
+
+#include <stddef.h>
+#ifndef PLUM_NO_STDINT
+#include <stdint.h>
+#endif
+
+#if !defined(__cplusplus) && (__STDC_VERSION__ >= 199901L)
+/* C99 or later, not C++, we can use restrict, and check for VLAs and anonymous struct members (C11) */
+/* indented preprocessor directives and // comments are also allowed here, but we'll avoid them for consistency */
+#define PLUM_RESTRICT restrict
+#define PLUM_ANON_MEMBERS (__STDC_VERSION__ >= 201112L)
+/* protect against really broken preprocessor implementations */
+#if !defined(__STDC_NO_VLA__) || !(__STDC_NO_VLA__ + 0)
+#define PLUM_VLA_SUPPORT 1
+#else
+#define PLUM_VLA_SUPPORT 0
+#endif
+#elif defined(__cplusplus)
+/* C++ allows anonymous unions as struct members, but not restrict or VLAs */
+#define PLUM_RESTRICT
+#define PLUM_ANON_MEMBERS 1
+#define PLUM_VLA_SUPPORT 0
+#else
+/* C89 (or, if we're really unlucky, non-standard C), so don't use any "advanced" C features */
+#define PLUM_RESTRICT
+#define PLUM_ANON_MEMBERS 0
+#define PLUM_VLA_SUPPORT 0
+#endif
+
+#ifdef PLUM_NO_ANON_MEMBERS
+#undef PLUM_ANON_MEMBERS
+#define PLUM_ANON_MEMBERS 0
+#endif
+
+#ifdef PLUM_NO_VLA
+#undef PLUM_VLA_SUPPORT
+#define PLUM_VLA_SUPPORT 0
+#endif
+
+#define PLUM_MODE_FILENAME   ((size_t) -1)
+#define PLUM_MODE_BUFFER     ((size_t) -2)
+#define PLUM_MODE_CALLBACK   ((size_t) -3)
+#define PLUM_MAX_MEMORY_SIZE ((size_t) -4)
+
+/* legacy constants, for compatibility with the v0.4 API */
+#define PLUM_FILENAME PLUM_MODE_FILENAME
+#define PLUM_BUFFER   PLUM_MODE_BUFFER
+#define PLUM_CALLBACK PLUM_MODE_CALLBACK
+
+enum plum_flags {
+  /* color formats */
+  PLUM_COLOR_32     = 0, /* RGBA 8.8.8.8 */
+  PLUM_COLOR_64     = 1, /* RGBA 16.16.16.16 */
+  PLUM_COLOR_16     = 2, /* RGBA 5.5.5.1 */
+  PLUM_COLOR_32X    = 3, /* RGBA 10.10.10.2 */
+  PLUM_COLOR_MASK   = 3,
+  PLUM_ALPHA_INVERT = 4,
+  /* palettes */
+  PLUM_PALETTE_NONE     =     0,
+  PLUM_PALETTE_LOAD     = 0x200,
+  PLUM_PALETTE_GENERATE = 0x400,
+  PLUM_PALETTE_FORCE    = 0x600,
+  PLUM_PALETTE_MASK     = 0x600,
+  /* palette sorting */
+  PLUM_SORT_LIGHT_FIRST =     0,
+  PLUM_SORT_DARK_FIRST  = 0x800,
+  /* other bit flags */
+  PLUM_ALPHA_REMOVE   =  0x100,
+  PLUM_SORT_EXISTING  = 0x1000,
+  PLUM_PALETTE_REDUCE = 0x2000
+};
+
+enum plum_image_types {
+  PLUM_IMAGE_NONE,
+  PLUM_IMAGE_BMP,
+  PLUM_IMAGE_GIF,
+  PLUM_IMAGE_PNG,
+  PLUM_IMAGE_APNG,
+  PLUM_IMAGE_JPEG,
+  PLUM_IMAGE_PNM,
+  PLUM_NUM_IMAGE_TYPES
+};
+
+enum plum_metadata_types {
+  PLUM_METADATA_NONE,
+  PLUM_METADATA_COLOR_DEPTH,
+  PLUM_METADATA_BACKGROUND,
+  PLUM_METADATA_LOOP_COUNT,
+  PLUM_METADATA_FRAME_DURATION,
+  PLUM_METADATA_FRAME_DISPOSAL,
+  PLUM_METADATA_FRAME_AREA,
+  PLUM_NUM_METADATA_TYPES
+};
+
+enum plum_frame_disposal_methods {
+  PLUM_DISPOSAL_NONE,
+  PLUM_DISPOSAL_BACKGROUND,
+  PLUM_DISPOSAL_PREVIOUS,
+  PLUM_DISPOSAL_REPLACE,
+  PLUM_DISPOSAL_BACKGROUND_REPLACE,
+  PLUM_DISPOSAL_PREVIOUS_REPLACE,
+  PLUM_NUM_DISPOSAL_METHODS
+};
+
+enum plum_errors {
+  PLUM_OK,
+  PLUM_ERR_INVALID_ARGUMENTS,
+  PLUM_ERR_INVALID_FILE_FORMAT,
+  PLUM_ERR_INVALID_METADATA,
+  PLUM_ERR_INVALID_COLOR_INDEX,
+  PLUM_ERR_TOO_MANY_COLORS,
+  PLUM_ERR_UNDEFINED_PALETTE,
+  PLUM_ERR_IMAGE_TOO_LARGE,
+  PLUM_ERR_NO_DATA,
+  PLUM_ERR_NO_MULTI_FRAME,
+  PLUM_ERR_FILE_INACCESSIBLE,
+  PLUM_ERR_FILE_ERROR,
+  PLUM_ERR_OUT_OF_MEMORY,
+  PLUM_NUM_ERRORS
+};
+
+#define PLUM_COLOR_VALUE_32(red, green, blue, alpha) ((uint32_t) (((uint32_t) (red) & 0xff) | (((uint32_t) (green) & 0xff) << 8) | \
+                                                                  (((uint32_t) (blue) & 0xff) << 16) | (((uint32_t) (alpha) & 0xff) << 24)))
+#define PLUM_COLOR_VALUE_64(red, green, blue, alpha) ((uint64_t) (((uint64_t) (red) & 0xffffu) | (((uint64_t) (green) & 0xffffu) << 16) | \
+                                                                  (((uint64_t) (blue) & 0xffffu) << 32) | (((uint64_t) (alpha) & 0xffffu) << 48)))
+#define PLUM_COLOR_VALUE_16(red, green, blue, alpha) ((uint16_t) (((uint16_t) (red) & 0x1f) | (((uint16_t) (green) & 0x1f) << 5) | \
+                                                                  (((uint16_t) (blue) & 0x1f) << 10) | (((uint16_t) (alpha) & 1) << 15)))
+#define PLUM_COLOR_VALUE_32X(red, green, blue, alpha) ((uint32_t) (((uint32_t) (red) & 0x3ff) | (((uint32_t) (green) & 0x3ff) << 10) | \
+                                                                   (((uint32_t) (blue) & 0x3ff) << 20) | (((uint32_t) (alpha) & 3) << 30)))
+
+#define PLUM_RED_32(color) ((uint32_t) ((uint32_t) (color) & 0xff))
+#define PLUM_RED_64(color) ((uint64_t) ((uint64_t) (color) & 0xffffu))
+#define PLUM_RED_16(color) ((uint16_t) ((uint16_t) (color) & 0x1f))
+#define PLUM_RED_32X(color) ((uint32_t) ((uint32_t) (color) & 0x3ff))
+#define PLUM_GREEN_32(color) ((uint32_t) (((uint32_t) (color) >> 8) & 0xff))
+#define PLUM_GREEN_64(color) ((uint64_t) (((uint64_t) (color) >> 16) & 0xffffu))
+#define PLUM_GREEN_16(color) ((uint16_t) (((uint16_t) (color) >> 5) & 0x1f))
+#define PLUM_GREEN_32X(color) ((uint32_t) (((uint32_t) (color) >> 10) & 0x3ff))
+#define PLUM_BLUE_32(color) ((uint32_t) (((uint32_t) (color) >> 16) & 0xff))
+#define PLUM_BLUE_64(color) ((uint64_t) (((uint64_t) (color) >> 32) & 0xffffu))
+#define PLUM_BLUE_16(color) ((uint16_t) (((uint16_t) (color) >> 10) & 0x1f))
+#define PLUM_BLUE_32X(color) ((uint32_t) (((uint32_t) (color) >> 20) & 0x3ff))
+#define PLUM_ALPHA_32(color) ((uint32_t) (((uint32_t) (color) >> 24) & 0xff))
+#define PLUM_ALPHA_64(color) ((uint64_t) (((uint64_t) (color) >> 48) & 0xffffu))
+#define PLUM_ALPHA_16(color) ((uint16_t) (((uint16_t) (color) >> 15) & 1))
+#define PLUM_ALPHA_32X(color) ((uint32_t) (((uint32_t) (color) >> 30) & 3))
+
+#define PLUM_RED_MASK_32 ((uint32_t) 0xff)
+#define PLUM_RED_MASK_64 ((uint64_t) 0xffffu)
+#define PLUM_RED_MASK_16 ((uint16_t) 0x1f)
+#define PLUM_RED_MASK_32X ((uint32_t) 0x3ff)
+#define PLUM_GREEN_MASK_32 ((uint32_t) 0xff00u)
+#define PLUM_GREEN_MASK_64 ((uint64_t) 0xffff0000u)
+#define PLUM_GREEN_MASK_16 ((uint16_t) 0x3e0)
+#define PLUM_GREEN_MASK_32X ((uint32_t) 0xffc00u)
+#define PLUM_BLUE_MASK_32 ((uint32_t) 0xff0000u)
+#define PLUM_BLUE_MASK_64 ((uint64_t) 0xffff00000000u)
+#define PLUM_BLUE_MASK_16 ((uint16_t) 0x7c00)
+#define PLUM_BLUE_MASK_32X ((uint32_t) 0x3ff00000u)
+#define PLUM_ALPHA_MASK_32 ((uint32_t) 0xff000000u)
+#define PLUM_ALPHA_MASK_64 ((uint64_t) 0xffff000000000000u)
+#define PLUM_ALPHA_MASK_16 ((uint16_t) 0x8000u)
+#define PLUM_ALPHA_MASK_32X ((uint32_t) 0xc0000000u)
+
+#define PLUM_PIXEL_INDEX(image, col, row, frame) (((size_t) (frame) * (size_t) (image) -> height + (size_t) (row)) * (size_t) (image) -> width + (size_t) (col))
+
+#define PLUM_PIXEL_8(image, col, row, frame) (((uint8_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+#define PLUM_PIXEL_16(image, col, row, frame) (((uint16_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+#define PLUM_PIXEL_32(image, col, row, frame) (((uint32_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+#define PLUM_PIXEL_64(image, col, row, frame) (((uint64_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+
+#if PLUM_VLA_SUPPORT
+#define PLUM_PIXEL_ARRAY_TYPE(image) ((*)[(image) -> height][(image) -> width])
+#define PLUM_PIXEL_ARRAY(declarator, image) ((* (declarator))[(image) -> height][(image) -> width])
+
+#define PLUM_PIXELS_8(image) ((uint8_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#define PLUM_PIXELS_16(image) ((uint16_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#define PLUM_PIXELS_32(image) ((uint32_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#define PLUM_PIXELS_64(image) ((uint64_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#endif
+
+struct plum_buffer {
+  size_t size;
+  void * data;
+};
+
+#ifdef __cplusplus
+extern "C" /* function pointer member requires an explicit extern "C" declaration to be passed safely from C++ to C */
+#endif
+struct plum_callback {
+  int (* callback) (void * userdata, void * buffer, int size);
+  void * userdata;
+};
+
+struct plum_metadata {
+  int type;
+  size_t size;
+  void * data;
+  struct plum_metadata * next;
+};
+
+struct plum_image {
+  uint16_t type;
+  uint8_t max_palette_index;
+  uint8_t color_format;
+  uint32_t frames;
+  uint32_t height;
+  uint32_t width;
+  void * allocator;
+  struct plum_metadata * metadata;
+#if PLUM_ANON_MEMBERS
+  union {
+#endif
+    void * palette;
+#if PLUM_ANON_MEMBERS
+    uint16_t * palette16;
+    uint32_t * palette32;
+    uint64_t * palette64;
+  };
+  union {
+#endif
+    void * data;
+#if PLUM_ANON_MEMBERS
+    uint8_t * data8;
+    uint16_t * data16;
+    uint32_t * data32;
+    uint64_t * data64;
+  };
+#endif
+  void * userdata;
+#ifdef __cplusplus
+inline uint8_t & pixel8 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint8_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint8_t & pixel8 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint8_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint16_t & pixel16 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint16_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint16_t & pixel16 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint16_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint32_t & pixel32 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint32_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint32_t & pixel32 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint32_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint64_t & pixel64 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint64_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint64_t & pixel64 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint64_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint16_t & color16 (uint8_t index) {
+  return ((uint16_t *) this -> palette)[index];
+}
+
+inline const uint16_t & color16 (uint8_t index) const {
+  return ((const uint16_t *) this -> palette)[index];
+}
+
+inline uint32_t & color32 (uint8_t index) {
+  return ((uint32_t *) this -> palette)[index];
+}
+
+inline const uint32_t & color32 (uint8_t index) const {
+  return ((const uint32_t *) this -> palette)[index];
+}
+
+inline uint64_t & color64 (uint8_t index) {
+  return ((uint64_t *) this -> palette)[index];
+}
+
+inline const uint64_t & color64 (uint8_t index) const {
+  return ((const uint64_t *) this -> palette)[index];
+}
+#endif
+};
+
+struct plum_rectangle {
+  uint32_t left;
+  uint32_t top;
+  uint32_t width;
+  uint32_t height;
+};
+
+/* keep declarations readable: redefine the "restrict" keyword, and undefine it later
+   (note that, if this expands to "#define restrict restrict", that will NOT expand recursively) */
+#define restrict PLUM_RESTRICT
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct plum_image * plum_new_image(void);
+struct plum_image * plum_copy_image(const struct plum_image * image);
+void plum_destroy_image(struct plum_image * image);
+struct plum_image * plum_load_image(const void * restrict buffer, size_t size_mode, unsigned flags, unsigned * restrict error);
+struct plum_image * plum_load_image_limited(const void * restrict buffer, size_t size_mode, unsigned flags, size_t limit, unsigned * restrict error);
+size_t plum_store_image(const struct plum_image * image, void * restrict buffer, size_t size_mode, unsigned * restrict error);
+unsigned plum_validate_image(const struct plum_image * image);
+const char * plum_get_error_text(unsigned error);
+const char * plum_get_file_format_name(unsigned format);
+uint32_t plum_get_version_number(void);
+int plum_check_valid_image_size(uint32_t width, uint32_t height, uint32_t frames);
+int plum_check_limited_image_size(uint32_t width, uint32_t height, uint32_t frames, size_t limit);
+size_t plum_color_buffer_size(size_t size, unsigned flags);
+size_t plum_pixel_buffer_size(const struct plum_image * image);
+size_t plum_palette_buffer_size(const struct plum_image * image);
+unsigned plum_rotate_image(struct plum_image * image, unsigned count, int flip);
+void plum_convert_colors(void * restrict destination, const void * restrict source, size_t count, unsigned to, unsigned from);
+uint64_t plum_convert_color(uint64_t color, unsigned from, unsigned to);
+void plum_remove_alpha(struct plum_image * image);
+unsigned plum_sort_palette(struct plum_image * image, unsigned flags);
+unsigned plum_sort_palette_custom(struct plum_image * image, uint64_t (* callback) (void *, uint64_t), void * argument, unsigned flags);
+unsigned plum_reduce_palette(struct plum_image * image);
+const uint8_t * plum_validate_palette_indexes(const struct plum_image * image);
+int plum_get_highest_palette_index(const struct plum_image * image);
+int plum_convert_colors_to_indexes(uint8_t * restrict destination, const void * restrict source, void * restrict palette, size_t count, unsigned flags);
+void plum_convert_indexes_to_colors(void * restrict destination, const uint8_t * restrict source, const void * restrict palette, size_t count, unsigned flags);
+void plum_sort_colors(const void * restrict colors, uint8_t max_index, unsigned flags, uint8_t * restrict result);
+void * plum_malloc(struct plum_image * image, size_t size);
+void * plum_calloc(struct plum_image * image, size_t size);
+void * plum_realloc(struct plum_image * image, void * buffer, size_t size);
+void plum_free(struct plum_image * image, void * buffer);
+struct plum_metadata * plum_allocate_metadata(struct plum_image * image, size_t size);
+unsigned plum_append_metadata(struct plum_image * image, int type, const void * data, size_t size);
+struct plum_metadata * plum_find_metadata(const struct plum_image * image, int type);
+
+#ifdef __cplusplus
+}
+#endif
+
+#undef restrict
+
+/* if PLUM_UNPREFIXED_MACROS is defined, include shorter, unprefixed alternatives for some common macros */
+/* this requires an explicit opt-in because it violates the principle of a library prefix as a namespace */
+#ifdef PLUM_UNPREFIXED_MACROS
+#define PIXEL(image, col, row, frame) PLUM_PIXEL_INDEX(image, col, row, frame)
+
+#define PIXEL8(image, col, row, frame) PLUM_PIXEL_8(image, col, row, frame)
+#define PIXEL16(image, col, row, frame) PLUM_PIXEL_16(image, col, row, frame)
+#define PIXEL32(image, col, row, frame) PLUM_PIXEL_32(image, col, row, frame)
+#define PIXEL64(image, col, row, frame) PLUM_PIXEL_64(image, col, row, frame)
+
+#if PLUM_VLA_SUPPORT
+#define PIXARRAY_T(image) PLUM_PIXEL_ARRAY_TYPE(image)
+#define PIXARRAY(declarator, image) PLUM_PIXEL_ARRAY(declarator, image)
+
+#define PIXELS8(image) PLUM_PIXELS_8(image)
+#define PIXELS16(image) PLUM_PIXELS_16(image)
+#define PIXELS32(image) PLUM_PIXELS_32(image)
+#define PIXELS64(image) PLUM_PIXELS_64(image)
+#endif
+
+#define COLOR32(red, green, blue, alpha) PLUM_COLOR_VALUE_32(red, green, blue, alpha)
+#define COLOR64(red, green, blue, alpha) PLUM_COLOR_VALUE_64(red, green, blue, alpha)
+#define COLOR16(red, green, blue, alpha) PLUM_COLOR_VALUE_16(red, green, blue, alpha)
+#define COLOR32X(red, green, blue, alpha) PLUM_COLOR_VALUE_32X(red, green, blue, alpha)
+
+#define RED32(color) PLUM_RED_32(color)
+#define RED64(color) PLUM_RED_64(color)
+#define RED16(color) PLUM_RED_16(color)
+#define RED32X(color) PLUM_RED_32X(color)
+#define GREEN32(color) PLUM_GREEN_32(color)
+#define GREEN64(color) PLUM_GREEN_64(color)
+#define GREEN16(color) PLUM_GREEN_16(color)
+#define GREEN32X(color) PLUM_GREEN_32X(color)
+#define BLUE32(color) PLUM_BLUE_32(color)
+#define BLUE64(color) PLUM_BLUE_64(color)
+#define BLUE16(color) PLUM_BLUE_16(color)
+#define BLUE32X(color) PLUM_BLUE_32X(color)
+#define ALPHA32(color) PLUM_ALPHA_32(color)
+#define ALPHA64(color) PLUM_ALPHA_64(color)
+#define ALPHA16(color) PLUM_ALPHA_16(color)
+#define ALPHA32X(color) PLUM_ALPHA_32X(color)
+#endif
+
+#endif
+
+#include <stdio.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdalign.h>
+#include <setjmp.h>
+
+
+struct allocator_node {
+  struct allocator_node * previous;
+  struct allocator_node * next;
+  alignas(max_align_t) unsigned char data[];
+};
+
+struct data_node {
+  union {
+    struct {
+      size_t size;
+      struct data_node * previous;
+      struct data_node * next;
+    };
+    max_align_t alignment;
+  };
+  unsigned char data[];
+};
+
+struct context {
+  unsigned status;
+  size_t size;
+  union {
+    const unsigned char * data;
+    struct data_node * output; // reverse order: top of the list is the LAST node
+  };
+  struct allocator_node * allocator;
+  union {
+    struct plum_image * image;
+    const struct plum_image * source;
+  };
+  FILE * file;
+  jmp_buf target;
+};
+
+struct pair {
+  size_t value;
+  size_t index;
+};
+
+struct compressed_GIF_code {
+  alignas(uint32_t) int16_t reference; // align the first member to align the struct
+  unsigned char value;
+  unsigned char type;
+};
+
+struct PNG_chunk_locations {
+  // includes APNG chunks; IHDR and IEND omitted because IHDR has a fixed offset and IEND contains no data
+  size_t palette; // PLTE
+  size_t bits; // sBIT
+  size_t background; // bKGD
+  size_t transparency; // tRNS
+  size_t animation; // acTL
+  size_t * data; // IDAT
+  size_t * frameinfo; // fcTL
+  size_t ** framedata; // fdAT
+};
+
+struct compressed_PNG_code {
+  unsigned datacode:   9;
+  unsigned dataextra:  5;
+  unsigned distcode:   5;
+  unsigned distextra: 13;
+};
+
+struct JPEG_marker_layout {
+  unsigned char * frametype; // 0-15
+  size_t * frames;
+  size_t ** framescans;
+  size_t *** framedata; // for each frame, for each scan, for each restart interval: offset, size
+  unsigned char * markertype; // same as the follow-up byte from the marker itself
+  size_t * markers; // for some markers only (DHT, DAC, DQT, DNL, DRI, EXP)
+  size_t hierarchical; // DHP marker, if present
+  size_t JFIF;
+  size_t Exif;
+  size_t Adobe;
+};
+
+struct JPEG_decoder_tables {
+  short * Huffman[8]; // 4 DC, 4 AC
+  unsigned short * quantization[4];
+  unsigned char arithmetic[8]; // conditioning values: 4 DC, 4 AC
+  uint16_t restart;
+};
+
+struct JPEG_component_info {
+  unsigned index:   8;
+  unsigned tableQ:  8;
+  unsigned tableDC: 4;
+  unsigned tableAC: 4;
+  unsigned scaleH:  4;
+  unsigned scaleV:  4;
+};
+
+struct JPEG_decompressor_state {
+  union {
+    int16_t (* restrict current_block[4])[64];
+    uint16_t * restrict current_value[4];
+  };
+  size_t last_size;
+  size_t restart_count;
+  uint16_t row_skip_index;
+  uint16_t row_skip_count;
+  uint16_t column_skip_index;
+  uint16_t column_skip_count;
+  uint16_t row_offset[4];
+  uint16_t unit_row_offset[4];
+  uint8_t unit_offset[4];
+  uint16_t restart_size;
+  unsigned char component_count;
+  unsigned char MCU[81];
+};
+
+enum JPEG_MCU_control_codes {
+  MCU_ZERO_COORD = 0xfd,
+  MCU_NEXT_ROW   = 0xfe,
+  MCU_END_LIST   = 0xff
+};
+
+struct JPEG_arithmetic_decoder_state {
+  unsigned probability: 15;
+  bool switch_MPS:       1;
+  unsigned next_MPS:     8;
+  unsigned next_LPS:     8;
+};
+
+struct JPEG_encoded_value {
+  unsigned code:   8;
+  unsigned type:   1; // 0 for DC codes, 1 for AC codes
+  unsigned bits:   7;
+  unsigned value: 16;
+};
+
+struct PNM_image_header {
+  uint8_t type; // 1-6: PNM header types, 7: unknown PAM, 11-13: PAM without alpha (B/W, grayscale, RGB), 14-16: PAM with alpha
+  uint16_t maxvalue;
+  uint32_t width;
+  uint32_t height;
+  size_t datastart;
+  size_t datalength;
+};
+
+#include <stddef.h>
+#include <stdint.h>
+
+
+// JPEG block coordinates in zig-zag order (mapping cell indexes to (x, y) coordinates)
+static const alignto(64) uint8_t JPEG_zigzag_rows[] = {
+  0, 0, 1, 2, 1, 0, 0, 1, 2, 3, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3,
+  4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 4, 5, 6, 7, 7, 6, 5, 6, 7, 7
+};
+static const alignto(64) uint8_t JPEG_zigzag_columns[] = {
+  0, 1, 0, 0, 1, 2, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0, 0, 1, 2, 3, 4, 5, 6, 7, 6, 5, 4,
+  3, 2, 1, 0, 1, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 3, 2, 3, 4, 5, 6, 7, 7, 6, 5, 4, 5, 6, 7, 7, 6, 7
+};
+
+// code lengths for default Huffman table used by PNG compression (entries 0x000 - 0x11f: data and length tree; entries 0x120 - 0x13f: distance tree)
+static const uint8_t default_PNG_Huffman_table_lengths[] = {
+   //         00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
+   /* 0x000 */ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+   /* 0x020 */ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+   /* 0x040 */ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+   /* 0x060 */ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+   /* 0x080 */ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+   /* 0x0a0 */ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+   /* 0x0c0 */ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+   /* 0x0e0 */ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
+   /* 0x100 */ 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8,
+   /* 0x120 */ 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5
+};
+
+// bitmasks used to extract the alpha channel out of a color value for each color format
+static const uint64_t alpha_component_masks[] = {0xff000000u, 0xffff000000000000u, 0x8000u, 0xc0000000u};
+
+// start and step for each interlace pass for interlaced PNG images; vertical coordinates use entries 0-6 and horizontal coordinates use entries 1-7
+static const uint8_t interlaced_PNG_pass_start[] = {0, 0, 4, 0, 2, 0, 1, 0};
+static const uint8_t interlaced_PNG_pass_step[] = {8, 8, 8, 4, 4, 2, 2, 1};
+
+// bytes per channel for each image type that the PNG writer can generate; 0 indicates that pixels are bitpacked (less than one byte per pixel)
+static const uint8_t bytes_per_channel_PNG[] = {0, 0, 0, 1, 3, 4, 6, 8};
+
+// encoding/decoding parameters for the PNG compressor; the base length and distance arrays contain one extra entry (with a value out of range)
+static const uint16_t compressed_PNG_base_lengths[] = {
+  3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 259
+};
+static const uint16_t compressed_PNG_base_distances[] = {
+  1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 32769
+};
+static const uint8_t compressed_PNG_length_bits[] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
+static const uint8_t compressed_PNG_distance_bits[] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
+static const uint8_t compressed_PNG_code_table_order[] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
+
+// number of channels per pixel that a PNG image has, based on its image type (as encoded in its header); 0 = invalid
+static const uint8_t channels_per_pixel_PNG[] = {1, 0, 3, 1, 2, 0, 4};
+
+#include <stdint.h>
+
+static inline uint16_t read_le16_unaligned (const unsigned char * data) {
+  return (uint16_t) *data | ((uint16_t) data[1] << 8);
+}
+
+static inline uint32_t read_le32_unaligned (const unsigned char * data) {
+  return (uint32_t) *data | ((uint32_t) data[1] << 8) | ((uint32_t) data[2] << 16) | ((uint32_t) data[3] << 24);
+}
+
+static inline uint16_t read_be16_unaligned (const unsigned char * data) {
+  return (uint16_t) data[1] | ((uint16_t) *data << 8);
+}
+
+static inline uint32_t read_be32_unaligned (const unsigned char * data) {
+  return (uint32_t) data[3] | ((uint32_t) data[2] << 8) | ((uint32_t) data[1] << 16) | ((uint32_t) *data << 24);
+}
+
+static inline void write_le16_unaligned (unsigned char * restrict buffer, uint16_t value) {
+  bytewrite(buffer, value, value >> 8);
+}
+
+static inline void write_le32_unaligned (unsigned char * restrict buffer, uint32_t value) {
+  bytewrite(buffer, value, value >> 8, value >> 16, value >> 24);
+}
+
+static inline void write_be16_unaligned (unsigned char * restrict buffer, uint32_t value) {
+  bytewrite(buffer, value >> 8, value);
+}
+
+static inline void write_be32_unaligned (unsigned char * restrict buffer, uint32_t value) {
+  bytewrite(buffer, value >> 24, value >> 16, value >> 8, value);
+}
+
+// allocator.c
+internal void * attach_allocator_node(struct allocator_node **, struct allocator_node *);
+internal void * allocate(struct allocator_node **, size_t);
+internal void * clear_allocate(struct allocator_node **, size_t);
+internal void deallocate(struct allocator_node **, void *);
+internal void * reallocate(struct allocator_node **, void *, size_t);
+internal void destroy_allocator_list(struct allocator_node *);
+
+// bmpread.c
+internal void load_BMP_data(struct context *, unsigned, size_t);
+internal uint8_t load_BMP_palette(struct context *, size_t, unsigned, uint64_t * restrict);
+internal void load_BMP_bitmasks(struct context *, size_t, uint8_t * restrict, unsigned);
+internal uint8_t * load_monochrome_BMP(struct context *, size_t, bool);
+internal uint8_t * load_halfbyte_BMP(struct context *, size_t, bool);
+internal uint8_t * load_byte_BMP(struct context *, size_t, bool);
+internal uint8_t * load_halfbyte_compressed_BMP(struct context *, size_t, bool);
+internal uint8_t * load_byte_compressed_BMP(struct context *, size_t, bool);
+internal uint64_t * load_BMP_pixels(struct context *, size_t, bool, size_t, uint64_t (*) (const unsigned char *, const void *), const void *);
+internal uint64_t load_BMP_halfword_pixel(const unsigned char *, const void *);
+internal uint64_t load_BMP_word_pixel(const unsigned char *, const void *);
+internal uint64_t load_BMP_RGB_pixel(const unsigned char *, const void *);
+internal uint64_t load_BMP_bitmasked_pixel(uint_fast32_t, const uint8_t *);
+
+// bmpwrite.c
+internal void generate_BMP_data(struct context *);
+internal void generate_BMP_bitmasked_data(struct context *, uint32_t, unsigned char *);
+internal void generate_BMP_palette_halfbyte_data(struct context *, unsigned char *);
+internal void generate_BMP_palette_byte_data(struct context *, unsigned char *);
+internal size_t try_compress_BMP(struct context *, size_t, size_t (*) (uint8_t * restrict, const uint8_t * restrict, size_t));
+internal size_t compress_BMP_halfbyte_row(uint8_t * restrict, const uint8_t * restrict, size_t);
+internal unsigned emit_BMP_compressed_halfbyte_remainder(uint8_t * restrict, const uint8_t * restrict, unsigned);
+internal size_t compress_BMP_byte_row(uint8_t * restrict, const uint8_t * restrict, size_t);
+internal void append_BMP_palette(struct context *);
+internal void generate_BMP_RGB_data(struct context *, unsigned char *);
+
+// checksum.c
+internal uint32_t compute_PNG_CRC(const unsigned char *, size_t);
+internal uint32_t compute_Adler32_checksum(const unsigned char *, size_t);
+
+// color.c
+internal bool image_has_transparency(const struct plum_image *);
+internal uint32_t get_color_depth(const struct plum_image *);
+internal uint32_t get_true_color_depth(const struct plum_image *);
+
+// framebounds.c
+internal struct plum_rectangle * get_frame_boundaries(struct context *, bool);
+internal void adjust_frame_boundaries(const struct plum_image *, struct plum_rectangle * restrict);
+internal bool image_rectangles_have_transparency(const struct plum_image *, const struct plum_rectangle *);
+
+// framebuffer.c
+internal void validate_image_size(struct context *, size_t);
+internal void allocate_framebuffers(struct context *, unsigned, bool);
+internal void write_framebuffer_to_image(struct plum_image *, const uint64_t * restrict, uint32_t, unsigned);
+internal void write_palette_framebuffer_to_image(struct context *, const uint8_t * restrict, const uint64_t * restrict, uint32_t, unsigned, uint8_t);
+internal void write_palette_to_image(struct context *, const uint64_t * restrict, unsigned);
+internal void rotate_frame_8(uint8_t * restrict, uint8_t * restrict, size_t, size_t, size_t (*) (size_t, size_t, size_t, size_t));
+internal void rotate_frame_16(uint16_t * restrict, uint16_t * restrict, size_t, size_t, size_t (*) (size_t, size_t, size_t, size_t));
+internal void rotate_frame_32(uint32_t * restrict, uint32_t * restrict, size_t, size_t, size_t (*) (size_t, size_t, size_t, size_t));
+internal void rotate_frame_64(uint64_t * restrict, uint64_t * restrict, size_t, size_t, size_t (*) (size_t, size_t, size_t, size_t));
+internal size_t rotate_left_coordinate(size_t, size_t, size_t, size_t);
+internal size_t rotate_right_coordinate(size_t, size_t, size_t, size_t);
+internal size_t rotate_both_coordinate(size_t, size_t, size_t, size_t);
+internal size_t flip_coordinate(size_t, size_t, size_t, size_t);
+internal size_t rotate_left_flip_coordinate(size_t, size_t, size_t, size_t);
+internal size_t rotate_right_flip_coordinate(size_t, size_t, size_t, size_t);
+internal size_t rotate_both_flip_coordinate(size_t, size_t, size_t, size_t);
+
+// frameduration.c
+internal uint64_t adjust_frame_duration(uint64_t, int64_t * restrict);
+internal void update_frame_duration_remainder(uint64_t, uint64_t, int64_t * restrict);
+internal void calculate_frame_duration_fraction(uint64_t, uint32_t, uint32_t * restrict, uint32_t * restrict);
+
+// gifcompress.c
+internal unsigned char * compress_GIF_data(struct context *, const unsigned char * restrict, size_t, size_t *, unsigned);
+internal void decompress_GIF_data(struct context *, unsigned char * restrict, const unsigned char * restrict, size_t, size_t, unsigned);
+internal void initialize_GIF_compression_codes(struct compressed_GIF_code * restrict, unsigned);
+internal uint8_t find_leading_GIF_code(const struct compressed_GIF_code * restrict, unsigned);
+internal void emit_GIF_data(struct context *, const struct compressed_GIF_code * restrict, unsigned, unsigned char **, unsigned char *);
+
+// gifread.c
+internal void load_GIF_data(struct context *, unsigned, size_t);
+internal uint64_t ** load_GIF_palettes_and_frame_count(struct context *, unsigned, size_t * restrict, uint64_t * restrict);
+internal void load_GIF_palette(struct context *, uint64_t * restrict, size_t * restrict, unsigned);
+internal void * load_GIF_data_blocks(struct context *, size_t * restrict, size_t * restrict);
+internal void skip_GIF_data_blocks(struct context *, size_t * restrict);
+internal void load_GIF_frame(struct context *, size_t * restrict, unsigned, uint32_t, const uint64_t * restrict, uint64_t, uint64_t * restrict, uint8_t * restrict,
+                             struct plum_rectangle * restrict);
+
+// gifwrite.c
+internal void generate_GIF_data(struct context *);
+internal void generate_GIF_data_with_palette(struct context *, unsigned char *);
+internal void generate_GIF_data_from_raw(struct context *, unsigned char *);
+internal void generate_GIF_frame_data(struct context *, uint32_t * restrict, unsigned char * restrict, uint32_t, const struct plum_metadata *,
+                                      const struct plum_metadata *, int64_t * restrict, const struct plum_rectangle *);
+internal int_fast32_t get_GIF_background_color(struct context *);
+internal void write_GIF_palette(struct context *, const uint32_t * restrict, unsigned);
+internal void write_GIF_loop_info(struct context *);
+internal void write_GIF_frame(struct context *, const unsigned char * restrict, const uint32_t * restrict, unsigned, int, uint32_t, unsigned, unsigned, unsigned,
+                              unsigned, const struct plum_metadata *, const struct plum_metadata *, int64_t * restrict);
+internal void write_GIF_data_blocks(struct context *, const unsigned char * restrict, size_t);
+
+// huffman.c
+internal void generate_Huffman_tree(struct context *, const size_t * restrict, unsigned char * restrict, size_t, unsigned char);
+internal void generate_Huffman_codes(unsigned short * restrict, size_t, const unsigned char * restrict, bool);
+
+// jpegarithmetic.c
+internal void decompress_JPEG_arithmetic_scan(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_decoder_tables *, size_t,
+                                              const struct JPEG_component_info *, const size_t * restrict, unsigned, unsigned char, unsigned char, bool);
+internal void decompress_JPEG_arithmetic_bit_scan(struct context *, struct JPEG_decompressor_state * restrict, size_t, const struct JPEG_component_info *,
+                                                  const size_t * restrict, unsigned, unsigned char, unsigned char);
+internal void decompress_JPEG_arithmetic_lossless_scan(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_decoder_tables *, size_t,
+                                                       const struct JPEG_component_info *, const size_t * restrict, unsigned char, unsigned);
+internal void initialize_JPEG_arithmetic_counters(struct context *, size_t * restrict, size_t * restrict, uint32_t * restrict);
+internal int16_t next_JPEG_arithmetic_value(struct context *, size_t * restrict, size_t * restrict, uint32_t * restrict, uint16_t * restrict,
+                                            unsigned char * restrict, signed char * restrict, unsigned, unsigned, unsigned char);
+internal unsigned char classify_JPEG_arithmetic_value(uint16_t, unsigned char);
+internal bool next_JPEG_arithmetic_bit(struct context *, size_t * restrict, size_t * restrict, signed char * restrict, uint32_t * restrict, uint16_t * restrict,
+                                       unsigned char * restrict);
+
+// jpegcomponents.c
+internal uint32_t determine_JPEG_components(struct context *, size_t);
+internal unsigned get_JPEG_component_count(uint32_t);
+internal void (* get_JPEG_component_transfer_function(struct context *, const struct JPEG_marker_layout *, uint32_t))
+               (uint64_t * restrict, size_t, unsigned, const double **);
+internal void append_JPEG_color_depth_metadata(struct context *, void (*) (uint64_t * restrict, size_t, unsigned, const double **), unsigned);
+internal void JPEG_transfer_RGB(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_BGR(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_ABGR(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_grayscale(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_alpha_grayscale(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_YCbCr(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_CbYCr(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_YCbCrK(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_CbKYCr(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_ACbYCr(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_CMYK(uint64_t * restrict, size_t, unsigned, const double **);
+internal void JPEG_transfer_CKMY(uint64_t * restrict, size_t, unsigned, const double **);
+
+// jpegcompress.c
+internal struct JPEG_encoded_value * generate_JPEG_luminance_data_stream(struct context *, double (* restrict)[64], size_t, const uint8_t [restrict static 64],
+                                                                         size_t * restrict);
+internal struct JPEG_encoded_value * generate_JPEG_chrominance_data_stream(struct context *, double (* restrict)[64], double (* restrict)[64], size_t,
+                                                                           const uint8_t [restrict static 64], size_t * restrict);
+internal double generate_JPEG_data_unit(struct JPEG_encoded_value *, size_t * restrict, const double [restrict static 64], const uint8_t [restrict static 64],
+                                        double);
+internal void encode_JPEG_value(struct JPEG_encoded_value *, int16_t, unsigned, unsigned char);
+internal size_t generate_JPEG_Huffman_table(struct context *, const struct JPEG_encoded_value *, size_t, unsigned char * restrict,
+                                            unsigned char [restrict static 0x100], unsigned char);
+internal void encode_JPEG_scan(struct context *, const struct JPEG_encoded_value *, size_t, const unsigned char [restrict static 0x200]);
+
+// jpegdct.c
+internal double apply_JPEG_DCT(int16_t [restrict static 64], const double [restrict static 64], const uint8_t [restrict static 64], double);
+internal void apply_JPEG_inverse_DCT(double [restrict static 64], const int16_t [restrict static 64], const uint16_t [restrict static 64]);
+
+// jpegdecompress.c
+internal void initialize_JPEG_decompressor_state(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_component_info *,
+                                                 const unsigned char *, size_t * restrict, size_t, size_t, size_t, unsigned char, unsigned char,
+                                                 const struct JPEG_decoder_tables *, const size_t * restrict, int16_t (* restrict *)[64]);
+internal void initialize_JPEG_decompressor_state_lossless(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_component_info *,
+                                                          const unsigned char *, size_t * restrict, size_t, size_t, size_t, unsigned char, unsigned char,
+                                                          const struct JPEG_decoder_tables *, const size_t * restrict, uint16_t * restrict *);
+internal void initialize_JPEG_decompressor_state_common(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_component_info *,
+                                                        const unsigned char *, size_t * restrict, size_t, size_t, size_t, unsigned char, unsigned char,
+                                                        const struct JPEG_decoder_tables *, const size_t * restrict, unsigned char);
+internal uint16_t predict_JPEG_lossless_sample(const uint16_t *, ptrdiff_t, bool, bool, unsigned, unsigned);
+
+// jpeghierarchical.c
+internal unsigned load_hierarchical_JPEG(struct context *, const struct JPEG_marker_layout *, uint32_t, double **);
+internal void expand_JPEG_component_horizontally(struct context *, double * restrict, size_t, size_t, size_t, double * restrict);
+internal void expand_JPEG_component_vertically(struct context *, double * restrict, size_t, size_t, size_t, double * restrict);
+internal void normalize_JPEG_component(double * restrict, size_t, double);
+
+// jpeghuffman.c
+internal void decompress_JPEG_Huffman_scan(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_decoder_tables *, size_t,
+                                           const struct JPEG_component_info *, const size_t * restrict, unsigned, unsigned char, unsigned char, bool);
+internal void decompress_JPEG_Huffman_bit_scan(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_decoder_tables *, size_t,
+                                               const struct JPEG_component_info *, const size_t * restrict, unsigned, unsigned char, unsigned char);
+internal void decompress_JPEG_Huffman_lossless_scan(struct context *, struct JPEG_decompressor_state * restrict, const struct JPEG_decoder_tables *, size_t,
+                                                    const struct JPEG_component_info *, const size_t * restrict, unsigned char, unsigned);
+internal unsigned char next_JPEG_Huffman_value(struct context *, const unsigned char **, size_t * restrict, uint32_t * restrict, uint8_t * restrict,
+                                               const short * restrict);
+
+// jpegread.c
+internal void load_JPEG_data(struct context *, unsigned, size_t);
+internal struct JPEG_marker_layout * load_JPEG_marker_layout(struct context *);
+internal unsigned get_JPEG_rotation(struct context *, size_t);
+internal unsigned load_single_frame_JPEG(struct context *, const struct JPEG_marker_layout *, uint32_t, double **);
+internal unsigned char process_JPEG_metadata_until_offset(struct context *, const struct JPEG_marker_layout *, struct JPEG_decoder_tables *, size_t * restrict,
+                                                          size_t);
+
+// jpegreadframe.c
+internal void load_JPEG_DCT_frame(struct context *, const struct JPEG_marker_layout *, uint32_t, size_t, struct JPEG_decoder_tables *, size_t * restrict,
+                                  double **, unsigned, size_t, size_t);
+internal void load_JPEG_lossless_frame(struct context *, const struct JPEG_marker_layout *, uint32_t, size_t, struct JPEG_decoder_tables *, size_t * restrict,
+                                       double **, unsigned, size_t, size_t);
+internal unsigned get_JPEG_component_info(struct context *, const unsigned char *, struct JPEG_component_info * restrict, uint32_t);
+internal const unsigned char * get_JPEG_scan_components(struct context *, size_t, struct JPEG_component_info * restrict, unsigned, unsigned char * restrict);
+internal void unpack_JPEG_component(double * restrict, double * restrict, size_t, size_t, size_t, size_t, unsigned char, unsigned char, unsigned char,
+                                    unsigned char);
+
+// jpegtables.c
+internal void initialize_JPEG_decoder_tables(struct context *, struct JPEG_decoder_tables *, const struct JPEG_marker_layout *);
+internal short * process_JPEG_Huffman_table(struct context *, const unsigned char ** restrict, uint16_t * restrict);
+internal void load_default_JPEG_Huffman_tables(struct context *, struct JPEG_decoder_tables * restrict);
+
+// jpegwrite.c
+internal void generate_JPEG_data(struct context *);
+internal void calculate_JPEG_quantization_tables(struct context *, uint8_t [restrict static 64], uint8_t [restrict static 64]);
+internal void convert_JPEG_components_to_YCbCr(struct context *, double (* restrict)[64], double (* restrict)[64], double (* restrict)[64]);
+internal void convert_JPEG_colors_to_YCbCr(const void * restrict, size_t, unsigned char, double * restrict, double * restrict, double * restrict,
+                                           uint64_t * restrict);
+internal void subsample_JPEG_component(double (* restrict)[64], double (* restrict)[64], size_t, size_t);
+
+// load.c
+internal void load_image_buffer_data(struct context *, unsigned, size_t);
+internal void prepare_image_buffer_data(struct context *, const void * restrict, size_t);
+internal void load_file(struct context *, const char *);
+internal void load_from_callback(struct context *, const struct plum_callback *);
+internal void * resize_read_buffer(struct context *, void *, size_t * restrict);
+internal void update_loaded_palette(struct context *, unsigned);
+
+// metadata.c
+internal void add_color_depth_metadata(struct context *, unsigned, unsigned, unsigned, unsigned, unsigned);
+internal void add_background_color_metadata(struct context *, uint64_t, unsigned);
+internal void add_loop_count_metadata(struct context *, uint32_t);
+internal void add_animation_metadata(struct context *, uint64_t ** restrict, uint8_t ** restrict);
+internal struct plum_rectangle * add_frame_area_metadata(struct context *);
+internal uint64_t get_empty_color(const struct plum_image *);
+
+// newstruct.c
+internal struct context * create_context(void);
+
+// palette.c
+internal void generate_palette(struct context *, unsigned);
+internal void remove_palette(struct context *);
+internal void sort_palette(struct plum_image *, unsigned);
+internal void apply_sorted_palette(struct plum_image *, unsigned, const uint8_t *);
+internal void reduce_palette(struct plum_image *);
+internal unsigned check_image_palette(const struct plum_image *);
+internal uint64_t get_color_sorting_score(uint64_t, unsigned);
+
+// pngcompress.c
+internal unsigned char * compress_PNG_data(struct context *, const unsigned char * restrict, size_t, size_t, size_t * restrict);
+internal struct compressed_PNG_code * generate_compressed_PNG_block(struct context *, const unsigned char * restrict, size_t, size_t, uint16_t * restrict,
+                                                                    size_t * restrict, size_t * restrict, bool);
+internal size_t compute_uncompressed_PNG_block_size(const unsigned char * restrict, size_t, size_t, uint16_t * restrict);
+internal unsigned find_PNG_reference(const unsigned char * restrict, const uint16_t * restrict, size_t, size_t, size_t * restrict);
+internal void append_PNG_reference(const unsigned char * restrict, size_t, uint16_t * restrict);
+internal uint16_t compute_PNG_reference_key(const unsigned char * data);
+internal void emit_PNG_code(struct context *, struct compressed_PNG_code **, size_t * restrict, size_t * restrict, int, unsigned);
+internal unsigned char * emit_PNG_compressed_block(struct context *, const struct compressed_PNG_code * restrict, size_t, bool, size_t * restrict,
+                                                   uint32_t * restrict, uint8_t * restrict);
+internal unsigned char * generate_PNG_Huffman_trees(struct context *, uint32_t * restrict, uint8_t * restrict, size_t * restrict,
+                                                    const size_t [restrict static 0x120], const size_t [restrict static 0x20],
+                                                    unsigned char [restrict static 0x120], unsigned char [restrict static 0x20]);
+
+// pngdecompress.c
+internal void * decompress_PNG_data(struct context *, const unsigned char *, size_t, size_t);
+internal void extract_PNG_code_table(struct context *, const unsigned char **, size_t * restrict, unsigned char [restrict static 0x140], uint32_t * restrict,
+                                     uint8_t * restrict);
+internal void decompress_PNG_block(struct context *, const unsigned char **, unsigned char * restrict, size_t * restrict, size_t * restrict, size_t,
+                                   uint32_t * restrict, uint8_t * restrict, const unsigned char [restrict static 0x140]);
+internal short * decode_PNG_Huffman_tree(struct context *, const unsigned char *, unsigned);
+internal uint16_t next_PNG_Huffman_code(struct context *, const short * restrict, const unsigned char **, size_t * restrict, uint32_t * restrict,
+                                        uint8_t * restrict);
+
+// pngread.c
+internal void load_PNG_data(struct context *, unsigned, size_t);
+internal struct PNG_chunk_locations * load_PNG_chunk_locations(struct context *);
+internal void append_PNG_chunk_location(struct context *, size_t **, size_t, size_t * restrict);
+internal void sort_PNG_animation_chunks(struct context *, struct PNG_chunk_locations * restrict, const size_t * restrict, size_t, size_t);
+internal uint8_t load_PNG_palette(struct context *, const struct PNG_chunk_locations * restrict, uint8_t, uint64_t * restrict);
+internal void add_PNG_bit_depth_metadata(struct context *, const struct PNG_chunk_locations *, uint8_t, uint8_t);
+internal uint64_t add_PNG_background_metadata(struct context *, const struct PNG_chunk_locations *, const uint64_t *, uint8_t, uint8_t, uint8_t, unsigned);
+internal uint64_t load_PNG_transparent_color(struct context *, size_t, uint8_t, uint8_t);
+internal bool check_PNG_reduced_frames(struct context *, const struct PNG_chunk_locations *);
+internal bool load_PNG_animation_frame_metadata(struct context *, size_t, uint64_t * restrict, uint8_t * restrict);
+
+// pngreadframe.c
+internal void load_PNG_frame(struct context *, const size_t *, uint32_t, const uint64_t *, uint8_t, uint8_t, uint8_t, bool, uint64_t, uint64_t);
+internal void * load_PNG_frame_part(struct context *, const size_t *, int, uint8_t, uint8_t, bool, uint32_t, uint32_t, size_t);
+internal uint8_t * load_PNG_palette_frame(struct context *, const void *, size_t, uint32_t, uint32_t, uint8_t, uint8_t, bool);
+internal uint64_t * load_PNG_raw_frame(struct context *, const void *, size_t, uint32_t, uint32_t, uint8_t, uint8_t, bool);
+internal void load_PNG_raw_frame_pass(struct context *, unsigned char * restrict, uint64_t * restrict, uint32_t, uint32_t, uint32_t, uint8_t, uint8_t,
+                                      unsigned char, unsigned char, unsigned char, unsigned char, size_t);
+internal void expand_bitpacked_PNG_data(unsigned char * restrict, const unsigned char * restrict, size_t, uint8_t);
+internal void remove_PNG_filter(struct context *, unsigned char * restrict, uint32_t, uint32_t, uint8_t, uint8_t);
+
+// pngwrite.c
+internal void generate_PNG_data(struct context *);
+internal void generate_APNG_data(struct context *);
+internal unsigned generate_PNG_header(struct context *, struct plum_rectangle * restrict);
+internal void append_PNG_header_chunks(struct context *, unsigned, uint32_t);
+internal void append_PNG_palette_data(struct context *, bool);
+internal void append_PNG_background_chunk(struct context *, const void * restrict, unsigned);
+internal void append_PNG_image_data(struct context *, const void * restrict, unsigned, uint32_t * restrict, const struct plum_rectangle *);
+internal void append_APNG_frame_header(struct context *, uint64_t, uint8_t, uint8_t, uint32_t * restrict, int64_t * restrict, const struct plum_rectangle *);
+internal void output_PNG_chunk(struct context *, uint32_t, uint32_t, const void * restrict);
+internal unsigned char * generate_PNG_frame_data(struct context *, const void * restrict, unsigned, size_t * restrict, const struct plum_rectangle *);
+internal void generate_PNG_row_data(struct context *, const void * restrict, unsigned char * restrict, size_t, unsigned);
+internal void filter_PNG_rows(unsigned char * restrict, const unsigned char * restrict, size_t, unsigned);
+internal unsigned char select_PNG_filtered_row(const unsigned char *, size_t);
+
+// pnmread.c
+internal void load_PNM_data(struct context *, unsigned, size_t);
+internal void load_PNM_header(struct context *, size_t, struct PNM_image_header * restrict);
+internal void load_PAM_header(struct context *, size_t, struct PNM_image_header * restrict);
+internal void skip_PNM_whitespace(struct context *, size_t * restrict);
+internal void skip_PNM_line(struct context *, size_t * restrict);
+internal unsigned next_PNM_token_length(struct context *, size_t);
+internal void read_PNM_numbers(struct context *, size_t * restrict, uint32_t * restrict, size_t);
+internal void add_PNM_bit_depth_metadata(struct context *, const struct PNM_image_header *);
+internal void load_PNM_frame(struct context *, const struct PNM_image_header * restrict, uint64_t * restrict);
+internal void load_PNM_bit_frame(struct context *, size_t, size_t, size_t, uint64_t * restrict);
+
+// pnmwrite.c
+internal void generate_PNM_data(struct context *);
+internal uint32_t * get_true_PNM_frame_sizes(struct context *);
+internal void generate_PPM_data(struct context *, const uint32_t * restrict, unsigned, uint64_t * restrict);
+internal void generate_PPM_header(struct context *, uint32_t, uint32_t, unsigned);
+internal void generate_PAM_data(struct context *, const uint32_t * restrict, unsigned, uint64_t * restrict);
+internal void generate_PAM_header(struct context *, uint32_t, uint32_t, unsigned);
+internal size_t write_PNM_number(unsigned char * restrict, uint32_t);
+internal void generate_PNM_frame_data(struct context *, const uint64_t *, uint32_t, uint32_t, unsigned, bool);
+internal void generate_PNM_frame_data_from_palette(struct context *, const uint8_t *, const uint64_t *, uint32_t, uint32_t, unsigned, bool);
+
+// sort.c
+internal void sort_values(uint64_t * restrict, uint64_t);
+internal void quicksort_values(uint64_t * restrict, uint64_t);
+internal void merge_sorted_values(uint64_t * restrict, uint64_t, uint64_t * restrict);
+internal void sort_pairs(struct pair * restrict, uint64_t);
+internal void quicksort_pairs(struct pair * restrict, uint64_t);
+internal void merge_sorted_pairs(struct pair * restrict, uint64_t, struct pair * restrict);
+
+// store.c
+internal void write_generated_image_data_to_file(struct context *, const char *);
+internal void write_generated_image_data_to_callback(struct context *, const struct plum_callback *);
+internal void write_generated_image_data(void * restrict, const struct data_node *);
+internal size_t get_total_output_size(struct context *);
+
+static inline noreturn throw (struct context * context, unsigned error) {
+  context -> status = error;
+  longjmp(context -> target, 1);
+}
+
+static inline struct allocator_node * get_allocator_node (void * buffer) {
+  return (struct allocator_node *) ((char *) buffer - offsetof(struct allocator_node, data));
+}
+
+static inline void * ctxmalloc (struct context * context, size_t size) {
+  void * result = allocate(&(context -> allocator), size);
+  if (!result) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  return result;
+}
+
+static inline void * ctxcalloc (struct context * context, size_t size) {
+  void * result = clear_allocate(&(context -> allocator), size);
+  if (!result) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  return result;
+}
+
+static inline void * ctxrealloc (struct context * context, void * buffer, size_t size) {
+  void * result = reallocate(&(context -> allocator), buffer, size);
+  if (!result) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  return result;
+}
+
+static inline void ctxfree (struct context * context, void * buffer) {
+  deallocate(&(context -> allocator), buffer);
+}
+
+static inline uintmax_t bitnegate (uintmax_t value) {
+  // ensure that the value is negated correctly, without accidental unsigned-to-signed conversions getting in the way
+  return ~value;
+}
+
+static inline uint16_t bitextend16 (uint16_t value, unsigned width) {
+  uint_fast32_t result = value;
+  while (width < 16) {
+    result |= result << width;
+    width <<= 1;
+  }
+  return result >> (width - 16);
+}
+
+static inline void * append_output_node (struct context * context, size_t size) {
+  struct data_node * node = ctxmalloc(context, sizeof *node + size);
+  *node = (struct data_node) {.size = size, .previous = context -> output, .next = NULL};
+  if (context -> output) context -> output -> next = node;
+  context -> output = node;
+  return node -> data;
+}
+
+static inline bool bit_depth_less_than (uint32_t depth, uint32_t target) {
+  // formally "less than or equal to", but that would be a very long name
+  return !((target - depth) & 0x80808080u);
+}
+
+static inline int absolute_value (int value) {
+  return (value < 0) ? -value : value;
+}
+
+static inline uint32_t shift_in_left (struct context * context, unsigned count, uint32_t * restrict dataword, uint8_t * restrict bits,
+                                      const unsigned char ** data, size_t * restrict size) {
+  while (*bits < count) {
+    if (!*size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    *dataword |= (uint32_t) **data << *bits;
+    ++ *data;
+    -- *size;
+    *bits += 8;
+  }
+  uint32_t result;
+  if (count < 32) {
+    result = *dataword & (((uint32_t) 1 << count) - 1);
+    *dataword >>= count;
+  } else {
+    result = *dataword;
+    *dataword = 0;
+  }
+  *bits -= count;
+  return result;
+}
+
+static inline uint32_t shift_in_right_JPEG (struct context * context, unsigned count, uint32_t * restrict dataword, uint8_t * restrict bits,
+                                            const unsigned char ** data, size_t * restrict size) {
+  // unlike shift_in_left above, this function has to account for stuffed bytes (any number of 0xFF followed by a single 0x00)
+  while (*bits < count) {
+    if (!*size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    *dataword = (*dataword << 8) | **data;
+    *bits += 8;
+    while (**data == 0xff) {
+      ++ *data;
+      -- *size;
+      if (!*size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+    ++ *data;
+    -- *size;
+  }
+  *bits -= count;
+  uint32_t result = *dataword >> *bits;
+  *dataword &= ((uint32_t) 1 << *bits) - 1;
+  return result;
+}
+
+static inline uint64_t color_from_floats (double red, double green, double blue, double alpha) {
+  uint64_t outred = (red >= 0) ? red + 0.5 : 0;
+  if (outred >= 0x10000u) outred = 0xffffu;
+  uint64_t outgreen = (green >= 0) ? green + 0.5 : 0;
+  if (outgreen >= 0x10000u) outgreen = 0xffffu;
+  uint64_t outblue = (blue >= 0) ? blue + 0.5 : 0;
+  if (outblue >= 0x10000u) outblue = 0xffffu;
+  uint64_t outalpha = (alpha >= 0) ? alpha + 0.5 : 0;
+  if (outalpha >= 0x10000u) outalpha = 0xffffu;
+  return (outalpha << 48) | (outblue << 32) | (outgreen << 16) | outred;
+}
+
+static inline int16_t make_signed_16 (uint16_t value) {
+  // this is a no-op (since int16_t must use two's complement), but it's necessary to avoid undefined behavior
+  return (value >= 0x8000u) ? -(int16_t) bitnegate(value) - 1 : value;
+}
+
+static inline unsigned bit_width (uintmax_t value) {
+  unsigned result;
+  for (result = 0; value; result ++) value >>= 1;
+  return result;
+}
+
+static inline bool is_whitespace (unsigned char value) {
+  // checks if value is 0 or isspace(value), but independent of current locale and system encoding
+  return !value || (value >= 9 && value <= 13) || value == 32;
+}
+
+void * attach_allocator_node (struct allocator_node ** list, struct allocator_node * node) {
+  if (!node) return NULL;
+  node -> previous = NULL;
+  node -> next = *list;
+  if (node -> next) node -> next -> previous = node;
+  *list = node;
+  return node -> data;
+}
+
+void * allocate (struct allocator_node ** list, size_t size) {
+  if (size >= (size_t) -sizeof(struct allocator_node)) return NULL;
+  return attach_allocator_node(list, malloc(sizeof(struct allocator_node) + size));
+}
+
+void * clear_allocate (struct allocator_node ** list, size_t size) {
+  if (size >= (size_t) -sizeof(struct allocator_node)) return NULL;
+  return attach_allocator_node(list, calloc(1, sizeof(struct allocator_node) + size));
+}
+
+void deallocate (struct allocator_node ** list, void * item) {
+  if (!item) return;
+  struct allocator_node * node = get_allocator_node(item);
+  if (node -> previous)
+    node -> previous -> next = node -> next;
+  else
+    *list = node -> next;
+  if (node -> next) node -> next -> previous = node -> previous;
+  free(node);
+}
+
+void * reallocate (struct allocator_node ** list, void * item, size_t size) {
+  if (size >= (size_t) -sizeof(struct allocator_node)) return NULL;
+  if (!item) return allocate(list, size);
+  struct allocator_node * node = get_allocator_node(item);
+  node = realloc(node, sizeof *node + size);
+  if (!node) return NULL;
+  if (node -> previous)
+    node -> previous -> next = node;
+  else
+    *list = node;
+  if (node -> next) node -> next -> previous = node;
+  return node -> data;
+}
+
+void destroy_allocator_list (struct allocator_node * list) {
+  while (list) {
+    struct allocator_node * node = list;
+    list = node -> next;
+    free(node);
+  }
+}
+
+void * plum_malloc (struct plum_image * image, size_t size) {
+  if (!image) return NULL;
+  struct allocator_node * list = image -> allocator;
+  void * result = allocate(&list, size);
+  image -> allocator = list;
+  return result;
+}
+
+void * plum_calloc (struct plum_image * image, size_t size) {
+  if (!image) return NULL;
+  struct allocator_node * list = image -> allocator;
+  void * result = clear_allocate(&list, size);
+  image -> allocator = list;
+  return result;
+}
+
+void * plum_realloc (struct plum_image * image, void * buffer, size_t size) {
+  if (!image) return NULL;
+  struct allocator_node * list = image -> allocator;
+  void * result = reallocate(&list, buffer, size);
+  if (result) image -> allocator = list;
+  return result;
+}
+
+void plum_free (struct plum_image * image, void * buffer) {
+  if (image) {
+    struct allocator_node * list = image -> allocator;
+    deallocate(&list, buffer);
+    image -> allocator = list;
+  } else
+    free(buffer); // special compatibility mode for bad runtimes without access to C libraries
+}
+
+void load_BMP_data (struct context * context, unsigned flags, size_t limit) {
+  if (context -> size < 54) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint_fast32_t subheader = read_le32_unaligned(context -> data + 14);
+  if (subheader < 40 || subheader >= 0xffffffe6u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  context -> image -> type = PLUM_IMAGE_BMP;
+  context -> image -> frames = 1;
+  context -> image -> width = read_le32_unaligned(context -> data + 18);
+  context -> image -> height = read_le32_unaligned(context -> data + 22);
+  if (context -> image -> width > 0x7fffffffu || context -> image -> height == 0x80000000u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  bool inverted = true;
+  if (context -> image -> height > 0x7fffffffu) {
+    context -> image -> height = -context -> image -> height;
+    inverted = false;
+  }
+  validate_image_size(context, limit);
+  uint_fast32_t dataoffset = read_le32_unaligned(context -> data + 10);
+  if (dataoffset < subheader + 14 || dataoffset >= context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (read_le16_unaligned(context -> data + 26) != 1) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint_fast16_t bits = read_le16_unaligned(context -> data + 28);
+  uint_fast32_t compression = read_le32_unaligned(context -> data + 30);
+  if (bits > 32 || compression > 3) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  allocate_framebuffers(context, flags, bits <= 8);
+  void * frame;
+  uint8_t bitmasks[8];
+  uint64_t palette[256];
+  switch (bits | (compression << 8)) {
+    case 1: // palette-based, first pixel in MSB
+      context -> image -> max_palette_index = load_BMP_palette(context, (size_t) 14 + subheader, 2, palette);
+      frame = load_monochrome_BMP(context, dataoffset, inverted);
+      write_palette_to_image(context, palette, flags);
+      write_palette_framebuffer_to_image(context, frame, palette, 0, flags, context -> image -> max_palette_index);
+      break;
+    case 4: // palette-based, first pixel in upper half
+      context -> image -> max_palette_index = load_BMP_palette(context, (size_t) 14 + subheader, 16, palette);
+      frame = load_halfbyte_BMP(context, dataoffset, inverted);
+      write_palette_to_image(context, palette, flags);
+      write_palette_framebuffer_to_image(context, frame, palette, 0, flags, context -> image -> max_palette_index);
+      break;
+    case 0x204: // 4-bit RLE
+      context -> image -> max_palette_index = load_BMP_palette(context, (size_t) 14 + subheader, 16, palette);
+      frame = load_halfbyte_compressed_BMP(context, dataoffset, inverted);
+      write_palette_to_image(context, palette, flags);
+      write_palette_framebuffer_to_image(context, frame, palette, 0, flags, context -> image -> max_palette_index);
+      break;
+    case 8: // palette-based
+      context -> image -> max_palette_index = load_BMP_palette(context, (size_t) 14 + subheader, 256, palette);
+      frame = load_byte_BMP(context, dataoffset, inverted);
+      write_palette_to_image(context, palette, flags);
+      write_palette_framebuffer_to_image(context, frame, palette, 0, flags, context -> image -> max_palette_index);
+      break;
+    case 0x108: // 8-bit RLE
+      context -> image -> max_palette_index = load_BMP_palette(context, (size_t) 14 + subheader, 256, palette);
+      frame = load_byte_compressed_BMP(context, dataoffset, inverted);
+      write_palette_to_image(context, palette, flags);
+      write_palette_framebuffer_to_image(context, frame, palette, 0, flags, context -> image -> max_palette_index);
+      break;
+    case 16: // mask 0x7c00 red, 0x03e0 green, 0x001f blue
+      add_color_depth_metadata(context, 5, 5, 5, 0, 0);
+      frame = load_BMP_pixels(context, dataoffset, inverted, 2, &load_BMP_halfword_pixel, (const uint8_t []) {10, 5, 5, 5, 0, 5, 0, 0});
+      write_framebuffer_to_image(context -> image, frame, 0, flags);
+      break;
+    case 0x310: // 16-bit bitfield-based
+      load_BMP_bitmasks(context, subheader, bitmasks, 16);
+      add_color_depth_metadata(context, bitmasks[1], bitmasks[3], bitmasks[5], bitmasks[7], 0);
+      frame = load_BMP_pixels(context, dataoffset, inverted, 2, &load_BMP_halfword_pixel, bitmasks);
+      write_framebuffer_to_image(context -> image, frame, 0, flags);
+      break;
+    case 24: // blue, green, red
+      add_color_depth_metadata(context, 8, 8, 8, 0, 0);
+      frame = load_BMP_pixels(context, dataoffset, inverted, 3, &load_BMP_RGB_pixel, NULL);
+      write_framebuffer_to_image(context -> image, frame, 0, flags);
+      break;
+    case 32: // blue, green, red, ignored
+      add_color_depth_metadata(context, 8, 8, 8, 0, 0);
+      frame = load_BMP_pixels(context, dataoffset, inverted, 4, &load_BMP_word_pixel, (const uint8_t []) {16, 8, 8, 8, 0, 8, 0, 0});
+      write_framebuffer_to_image(context -> image, frame, 0, flags);
+      break;
+    case 0x320: // 32-bit bitfield-based
+      load_BMP_bitmasks(context, subheader, bitmasks, 32);
+      add_color_depth_metadata(context, bitmasks[1], bitmasks[3], bitmasks[5], bitmasks[7], 0);
+      frame = load_BMP_pixels(context, dataoffset, inverted, 4, &load_BMP_word_pixel, bitmasks);
+      write_framebuffer_to_image(context -> image, frame, 0, flags);
+      break;
+    default:
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  ctxfree(context, frame);
+}
+
+uint8_t load_BMP_palette (struct context * context, size_t offset, unsigned max_count, uint64_t * restrict palette) {
+  uint_fast32_t count = read_le32_unaligned(context -> data + 46);
+  if (!count || count > max_count) count = max_count;
+  size_t end = offset + count * 4;
+  if (end < offset || end > context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  for (unsigned p = 0; p < count; p ++, offset += 4)
+    palette[p] = (((uint64_t) context -> data[offset] << 32) | ((uint64_t) context -> data[offset + 1] << 16) | (uint64_t) context -> data[offset + 2]) * 0x101;
+  add_color_depth_metadata(context, 8, 8, 8, 0, 0);
+  return count - 1;
+}
+
+void load_BMP_bitmasks (struct context * context, size_t headersize, uint8_t * restrict bitmasks, unsigned maxbits) {
+  bool valid = false;
+  const uint8_t * bp;
+  unsigned count;
+  if (headersize >= 56) {
+    bp = context -> data + 54;
+    count = 4;
+  } else {
+    if (context -> size <= headersize + 26) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    bp = context -> data + 14 + headersize;
+    count = 3;
+    bitmasks[6] = bitmasks[7] = 0;
+  }
+  while (count --) {
+    uint_fast32_t mask = read_le32_unaligned(bp);
+    *bitmasks = bitmasks[1] = 0;
+    if (mask) {
+      while (!(mask & 1)) {
+        ++ *bitmasks;
+        mask >>= 1;
+      }
+      while (mask & 1) {
+        bitmasks[1] ++;
+        mask >>= 1;
+      }
+      if (mask) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      if (bitmasks[1] > 16) {
+        *bitmasks += bitmasks[1] - 16;
+        bitmasks[1] = 16;
+      }
+      if (*bitmasks + bitmasks[1] > maxbits) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      valid = true; // at least one mask is not empty
+    }
+    bp += 4;
+    bitmasks += 2;
+  }
+  if (!valid) throw(context, PLUM_ERR_NO_DATA);
+}
+
+uint8_t * load_monochrome_BMP (struct context * context, size_t offset, bool inverted) {
+  size_t rowsize = ((context -> image -> width + 31) >> 3) & bitnegate(3);
+  size_t imagesize = rowsize * context -> image -> height;
+  if (imagesize > context -> size - offset) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint8_t * frame = ctxmalloc(context, (size_t) context -> image -> width * context -> image -> height);
+  const unsigned char * rowdata = context -> data + offset + (inverted ? rowsize * (context -> image -> height - 1) : 0);
+  size_t cell = 0;
+  for (uint_fast32_t row = 0; row < context -> image -> height; row ++) {
+    const unsigned char * pixeldata = rowdata;
+    for (uint_fast32_t pos = (context -> image -> width >> 3); pos; pos --, pixeldata ++) {
+      frame[cell ++] = !!(*pixeldata & 0x80);
+      frame[cell ++] = !!(*pixeldata & 0x40);
+      frame[cell ++] = !!(*pixeldata & 0x20);
+      frame[cell ++] = !!(*pixeldata & 0x10);
+      frame[cell ++] = !!(*pixeldata & 8);
+      frame[cell ++] = !!(*pixeldata & 4);
+      frame[cell ++] = !!(*pixeldata & 2);
+      frame[cell ++] = *pixeldata & 1;
+    }
+    if (context -> image -> width & 7) {
+      unsigned char remainder = *pixeldata;
+      for (uint_fast8_t pos = context -> image -> width & 7; pos; pos --, remainder <<= 1) frame[cell ++] = !!(remainder & 0x80);
+    }
+    if (inverted)
+      rowdata -= rowsize;
+    else
+      rowdata += rowsize;
+  }
+  return frame;
+}
+
+uint8_t * load_halfbyte_BMP (struct context * context, size_t offset, bool inverted) {
+  size_t rowsize = ((context -> image -> width + 7) >> 1) & bitnegate(3);
+  size_t imagesize = rowsize * context -> image -> height;
+  if (imagesize > context -> size - offset) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint8_t * frame = ctxmalloc(context, (size_t) context -> image -> width * context -> image -> height);
+  const unsigned char * rowdata = context -> data + offset + (inverted ? rowsize * (context -> image -> height - 1) : 0);
+  size_t cell = 0;
+  for (uint_fast32_t row = 0; row < context -> image -> height; row ++) {
+    const unsigned char * pixeldata = rowdata;
+    for (uint_fast32_t pos = context -> image -> width >> 1; pos; pos --) {
+      frame[cell ++] = *pixeldata >> 4;
+      frame[cell ++] = *(pixeldata ++) & 15;
+    }
+    if (context -> image -> width & 1) frame[cell ++] = *pixeldata >> 4;
+    if (inverted)
+      rowdata -= rowsize;
+    else
+      rowdata += rowsize;
+  }
+  return frame;
+}
+
+uint8_t * load_byte_BMP (struct context * context, size_t offset, bool inverted) {
+  size_t rowsize = (context -> image -> width + 3) & bitnegate(3);
+  size_t imagesize = rowsize * context -> image -> height;
+  if (imagesize > context -> size - offset) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint8_t * frame = ctxmalloc(context, (size_t) context -> image -> width * context -> image -> height);
+  if (inverted || (context -> image -> width & 3))
+    for (uint_fast32_t row = 0; row < context -> image -> height; row ++)
+      memcpy(frame + context -> image -> width * row,
+             context -> data + offset + rowsize * (inverted ? context -> image -> height - 1 - row : row),
+             context -> image -> width);
+  else
+    memcpy(frame, context -> data + offset, imagesize);
+  return frame;
+}
+
+uint8_t * load_halfbyte_compressed_BMP (struct context * context, size_t offset, bool inverted) {
+  if (!inverted) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  const unsigned char * data = context -> data + offset;
+  size_t remaining = context -> size - offset;
+  uint8_t * frame = ctxcalloc(context, (size_t) context -> image -> width * context -> image -> height);
+  uint_fast32_t row = context -> image -> height - 1, col = 0;
+  while (remaining >= 2) {
+    unsigned char length = *(data ++);
+    unsigned char databyte = *(data ++);
+    remaining -= 2;
+    if (length) {
+      if (col + length > context -> image -> width) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      uint8_t * framedata = frame + (size_t) row * context -> image -> width + col;
+      col += length;
+      while (length) {
+        *(framedata ++) = databyte >> 4;
+        databyte = (databyte >> 4) | (databyte << 4);
+        length --;
+      }
+    } else switch (databyte) {
+      case 0:
+        if (row) {
+          row --;
+          col = 0;
+          break;
+        }
+      case 1:
+        return frame;
+      case 2:
+        if (remaining < 2 || col + *data > context -> image -> width || data[1] > row) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        col += *(data ++);
+        row -= *(data ++);
+        remaining -= 2;
+        break;
+      default: {
+        if (col + databyte > context -> image -> width) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (remaining < (((databyte + 3) & ~3u) >> 1)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        uint8_t * framedata = frame + (size_t) row * context -> image -> width + col;
+        uint_fast8_t pos;
+        for (pos = 0; pos < (databyte >> 1); pos ++) {
+          *(framedata ++) = data[pos] >> 4;
+          *(framedata ++) = data[pos] & 15;
+        }
+        if (databyte & 1) *framedata = data[pos] >> 4;
+        col += databyte;
+        data += ((databyte + 3) & ~3u) >> 1;
+        remaining -= ((databyte + 3) & ~3u) >> 1;
+      }
+    }
+  }
+  throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+}
+
+uint8_t * load_byte_compressed_BMP (struct context * context, size_t offset, bool inverted) {
+  if (!inverted) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  const unsigned char * data = context -> data + offset;
+  size_t remaining = context -> size - offset;
+  uint8_t * frame = ctxcalloc(context, (size_t) context -> image -> width * context -> image -> height);
+  uint_fast32_t row = context -> image -> height - 1, col = 0;
+  while (remaining >= 2) {
+    unsigned char length = *(data ++);
+    unsigned char databyte = *(data ++);
+    remaining -= 2;
+    if (length) {
+      if (col + length > context -> image -> width) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      memset(frame + (size_t) row * context -> image -> width + col, databyte, length);
+      col += length;
+    } else switch (databyte) {
+      case 0:
+        if (row) {
+          row --;
+          col = 0;
+          break;
+        }
+      case 1:
+        return frame;
+      case 2:
+        if (remaining < 2 || col + *data > context -> image -> width || data[1] > row) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        col += *(data ++);
+        row -= *(data ++);
+        remaining -= 2;
+        break;
+      default:
+        if (col + databyte > context -> image -> width) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (remaining < ((databyte + 1) & ~1u)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        memcpy(frame + (size_t) row * context -> image -> width + col, data, databyte);
+        col += databyte;
+        data += (databyte + 1) & ~1u;
+        remaining -= (databyte + 1) & ~1u;
+    }
+  }
+  throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+}
+
+uint64_t * load_BMP_pixels (struct context * context, size_t offset, bool inverted, size_t bytes,
+                            uint64_t (* loader) (const unsigned char *, const void *), const void * loaderdata) {
+  size_t rowsize = (context -> image -> width * bytes + 3) & bitnegate(3);
+  size_t imagesize = rowsize * context -> image -> height;
+  if (imagesize > context -> size - offset) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  const unsigned char * rowdata = context -> data + offset + (inverted ? rowsize * (context -> image -> height - 1) : 0);
+  size_t cell = 0;
+  uint64_t * frame = ctxmalloc(context, sizeof *frame * context -> image -> width * context -> image -> height);
+  for (uint_fast32_t row = 0; row < context -> image -> height; row ++) {
+    const unsigned char * pixeldata = rowdata;
+    for (uint_fast32_t col = 0; col < context -> image -> width; col ++) {
+      frame[cell ++] = loader(pixeldata, loaderdata);
+      pixeldata += bytes;
+    }
+    if (inverted)
+      rowdata -= rowsize;
+    else
+      rowdata += rowsize;
+  }
+  return frame;
+}
+
+uint64_t load_BMP_halfword_pixel (const unsigned char * data, const void * bitmasks) {
+  return load_BMP_bitmasked_pixel(read_le16_unaligned(data), bitmasks);
+}
+
+uint64_t load_BMP_word_pixel (const unsigned char * data, const void * bitmasks) {
+  return load_BMP_bitmasked_pixel(read_le32_unaligned(data), bitmasks);
+}
+
+uint64_t load_BMP_RGB_pixel (const unsigned char * data, const void * bitmasks) {
+  (void) bitmasks;
+  return (((uint64_t) *data << 32) | ((uint64_t) data[1] << 16) | (uint64_t) data[2]) * 0x101;
+}
+
+uint64_t load_BMP_bitmasked_pixel (uint_fast32_t pixel, const uint8_t * bitmasks) {
+  uint64_t result = 0;
+  if (bitmasks[1]) result |= bitextend16((pixel >> *bitmasks) & (((uint64_t) 1 << bitmasks[1]) - 1), bitmasks[1]);
+  if (bitmasks[3]) result |= (uint64_t) bitextend16((pixel >> bitmasks[2]) & (((uint64_t) 1 << bitmasks[3]) - 1), bitmasks[3]) << 16;
+  if (bitmasks[5]) result |= (uint64_t) bitextend16((pixel >> bitmasks[4]) & (((uint64_t) 1 << bitmasks[5]) - 1), bitmasks[5]) << 32;
+  if (bitmasks[7]) result |= (~(uint64_t) bitextend16((pixel >> bitmasks[6]) & (((uint64_t) 1 << bitmasks[7]) - 1), bitmasks[7])) << 48;
+  return result;
+}
+
+void generate_BMP_data (struct context * context) {
+  if (context -> source -> frames > 1) throw(context, PLUM_ERR_NO_MULTI_FRAME);
+  if (context -> source -> width > 0x7fffffffu || context -> source -> height > 0x7fffffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  unsigned char * header = append_output_node(context, 14);
+  bytewrite(header, 0x42, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00);
+  uint32_t depth = get_true_color_depth(context -> source);
+  if (depth >= 0x1000000u)
+    generate_BMP_bitmasked_data(context, depth, header + 10);
+  else if (context -> source -> palette)
+    if (context -> source -> max_palette_index < 16)
+      generate_BMP_palette_halfbyte_data(context, header + 10);
+    else
+      generate_BMP_palette_byte_data(context, header + 10);
+  else if (bit_depth_less_than(depth, 0x80808u))
+    generate_BMP_RGB_data(context, header + 10);
+  else
+    generate_BMP_bitmasked_data(context, depth, header + 10);
+  size_t filesize = get_total_output_size(context);
+  if (filesize <= 0x7fffffffu) write_le32_unaligned(header + 2, filesize);
+}
+
+void generate_BMP_bitmasked_data (struct context * context, uint32_t depth, unsigned char * offset_pointer) {
+  uint8_t reddepth = depth, greendepth = depth >> 8, bluedepth = depth >> 16, alphadepth = depth >> 24;
+  uint_fast8_t totaldepth = reddepth + greendepth + bluedepth + alphadepth;
+  if (totaldepth > 32) {
+    reddepth = ((reddepth << 6) / totaldepth + 1) >> 1;
+    greendepth = ((greendepth << 6) / totaldepth + 1) >> 1;
+    bluedepth = ((bluedepth << 6) / totaldepth + 1) >> 1;
+    alphadepth = ((alphadepth << 6) / totaldepth + 1) >> 1;
+    totaldepth = reddepth + greendepth + bluedepth + alphadepth;
+    while (totaldepth > 32) {
+      if (alphadepth > 2) totaldepth --, alphadepth --;
+      if (bluedepth > 2 && totaldepth > 32) totaldepth --, bluedepth --;
+      if (reddepth > 2 && totaldepth > 32) totaldepth --, reddepth --;
+      if (greendepth > 2 && totaldepth > 32) totaldepth --, greendepth --;
+    }
+  }
+  uint8_t blueshift = reddepth + greendepth, alphashift = blueshift + bluedepth;
+  unsigned char * attributes = append_output_node(context, 108);
+  memset(attributes, 0, 108);
+  write_le32_unaligned(offset_pointer, 122);
+  *attributes = 108;
+  write_le32_unaligned(attributes + 4, context -> source -> width);
+  write_le32_unaligned(attributes + 8, context -> source -> height);
+  attributes[12] = 1;
+  attributes[14] = (totaldepth <= 16) ? 16 : 32;
+  attributes[16] = 3;
+  write_le32_unaligned(attributes + 40, ((uint32_t) 1 << reddepth) - 1);
+  write_le32_unaligned(attributes + 44, (((uint32_t) 1 << greendepth) - 1) << reddepth);
+  write_le32_unaligned(attributes + 48, (((uint32_t) 1 << bluedepth) - 1) << blueshift);
+  write_le32_unaligned(attributes + 52, alphadepth ? (((uint32_t) 1 << alphadepth) - 1) << alphashift : 0);
+  write_le32_unaligned(attributes + 56, 0x73524742u); // 'sRGB'
+  size_t rowsize = (size_t) context -> source -> width * (attributes[14] >> 3);
+  if (totaldepth <= 16 && (context -> source -> width & 1)) rowsize += 2;
+  size_t imagesize = rowsize * context -> source -> height;
+  if (imagesize <= 0x7fffffffu) write_le32_unaligned(attributes + 20, imagesize);
+  unsigned char * data = append_output_node(context, imagesize);
+  uint_fast32_t row = context -> source -> height - 1;
+  do {
+    size_t pos = (size_t) row * context -> source -> width;
+    for (uint_fast32_t p = 0; p < context -> source -> width; p ++) {
+      uint64_t color;
+      const void * colordata = context -> source -> data;
+      size_t index = pos ++;
+      if (context -> source -> palette) {
+        index = context -> source -> data8[index];
+        colordata = context -> source -> palette;
+      }
+      switch (context -> source -> color_format & PLUM_COLOR_MASK) {
+        case PLUM_COLOR_16: color = index[(const uint16_t *) colordata]; break;
+        case PLUM_COLOR_64: color = index[(const uint64_t *) colordata]; break;
+        default: color = index[(const uint32_t *) colordata];
+      }
+      color = plum_convert_color(color, context -> source -> color_format, PLUM_COLOR_64 | PLUM_ALPHA_INVERT);
+      uint_fast32_t out = ((color & 0xffffu) >> (16 - reddepth)) | ((color & 0xffff0000u) >> (32 - greendepth) << reddepth) |
+                          ((color & 0xffff00000000u) >> (48 - bluedepth) << blueshift);
+      if (alphadepth) out |= color >> (64 - alphadepth) << alphashift;
+      if (totaldepth <= 16) {
+        write_le16_unaligned(data, out);
+        data += 2;
+      } else {
+        write_le32_unaligned(data, out);
+        data += 4;
+      }
+    }
+    if (totaldepth <= 16 && (context -> source -> width & 1)) data += byteappend(data, 0x00, 0x00);
+  } while (row --);
+}
+
+void generate_BMP_palette_halfbyte_data (struct context * context, unsigned char * offset_pointer) {
+  unsigned char * attributes = append_output_node(context, 40);
+  write_le32_unaligned(offset_pointer, 58 + 4 * context -> source -> max_palette_index);
+  memset(attributes, 0, 40);
+  *attributes = 40;
+  write_le32_unaligned(attributes + 4, context -> source -> width);
+  write_le32_unaligned(attributes + 8, context -> source -> height);
+  attributes[12] = 1;
+  attributes[14] = 4;
+  write_le32_unaligned(attributes + 32, context -> source -> max_palette_index + 1);
+  append_BMP_palette(context);
+  size_t rowsize = ((context -> source -> width + 7) & ~7u) >> 1;
+  if (context -> source -> max_palette_index < 2) rowsize = ((rowsize >> 2) + 3) & bitnegate(3);
+  size_t imagesize = rowsize * context -> source -> height;
+  unsigned char * data = append_output_node(context, imagesize);
+  size_t compressed = try_compress_BMP(context, imagesize, &compress_BMP_halfbyte_row);
+  if (compressed) {
+    attributes[16] = 2;
+    if (compressed <= 0x7fffffffu) write_le32_unaligned(attributes + 20, compressed);
+    context -> output -> size = compressed;
+    return;
+  }
+  uint_fast32_t row = context -> source -> height - 1;
+  uint_fast8_t padding = 3u & ~((context -> source -> width - 1) >> ((context -> source -> max_palette_index < 2) ? 3 : 1));
+  do {
+    const uint8_t * source = context -> source -> data8 + (size_t) row * context -> source -> width;
+    if (context -> source -> max_palette_index < 2) {
+      uint_fast8_t value = 0;
+      for (uint_fast32_t p = 0; p < context -> source -> width; p ++) {
+        value = (value << 1) | source[p];
+        if ((p & 7) == 7) {
+          *(data ++) = value;
+          value = 0;
+        }
+      }
+      if (context -> source -> width & 7) *(data ++) = value << (8 - (context -> source -> width & 7));
+      attributes[14] = 1;
+    } else {
+      for (uint_fast32_t p = 0; p < context -> source -> width - 1; p += 2) *(data ++) = (source[p] << 4) | source[p + 1];
+      if (context -> source -> width & 1) *(data ++) = source[context -> source -> width - 1] << 4;
+    }
+    for (uint_fast8_t value = 0; value < padding; value ++) *(data ++) = 0;
+  } while (row --);
+}
+
+void generate_BMP_palette_byte_data (struct context * context, unsigned char * offset_pointer) {
+  unsigned char * attributes = append_output_node(context, 40);
+  write_le32_unaligned(offset_pointer, 58 + 4 * context -> source -> max_palette_index);
+  memset(attributes, 0, 40);
+  *attributes = 40;
+  write_le32_unaligned(attributes + 4, context -> source -> width);
+  write_le32_unaligned(attributes + 8, context -> source -> height);
+  attributes[12] = 1;
+  attributes[14] = 8;
+  write_le32_unaligned(attributes + 32, context -> source -> max_palette_index + 1);
+  append_BMP_palette(context);
+  size_t rowsize = (context -> source -> width + 3) & bitnegate(3), imagesize = rowsize * context -> source -> height;
+  unsigned char * data = append_output_node(context, imagesize);
+  size_t compressed = try_compress_BMP(context, imagesize, &compress_BMP_byte_row);
+  if (compressed) {
+    attributes[16] = 1;
+    if (compressed <= 0x7fffffffu) write_le32_unaligned(attributes + 20, compressed);
+    context -> output -> size = compressed;
+    return;
+  }
+  uint_fast32_t row = context -> source -> height - 1;
+  do {
+    memcpy(data, context -> source -> data8 + row * context -> source -> width, context -> source -> width);
+    if (rowsize != context -> source -> width) memset(data + context -> source -> width, 0, rowsize - context -> source -> width);
+    data += rowsize;
+  } while (row --);
+}
+
+size_t try_compress_BMP (struct context * context, size_t size, size_t (* rowhandler) (uint8_t * restrict, const uint8_t * restrict, size_t)) {
+  uint8_t * rowdata = ctxmalloc(context, size * ((context -> source -> max_palette_index < 2) ? 8 : 2) + 2);
+  uint8_t * output = context -> output -> data;
+  size_t cumulative = 0;
+  uint_fast32_t row = context -> source -> height - 1;
+  do {
+    size_t rowsize = rowhandler(rowdata, context -> source -> data8 + (size_t) row * context -> source -> width, context -> source -> width);
+    cumulative += rowsize;
+    if (cumulative >= size) {
+      ctxfree(context, rowdata);
+      return 0;
+    }
+    if (!row) rowdata[rowsize - 1] = 1; // convert a 0x00, 0x00 (EOL) into 0x00, 0x01 (EOF)
+    memcpy(output, rowdata, rowsize);
+    output += rowsize;
+  } while (row --);
+  ctxfree(context, rowdata);
+  return cumulative;
+}
+
+size_t compress_BMP_halfbyte_row (uint8_t * restrict result, const uint8_t * restrict data, size_t count) {
+  size_t size = 2; // account for the terminator
+  while (count > 3)
+    if (*data == data[2] && data[1] == data[3]) {
+      uint_fast8_t length;
+      for (length = 4; length < 0xff && length < count && data[length] == data[length - 2]; length ++);
+      result += byteappend(result, length, (*data << 4) | data[1]);
+      size += 2;
+      data += length;
+      count -= length;
+    } else {
+      size_t length;
+      uint_fast8_t matches = 0;
+      for (length = 2; length < count; length ++) {
+        if (data[length] == data[length - 2])
+          matches ++;
+        else
+          matches = 0;
+        if (matches >= 5) {
+          length -= matches;
+          break;
+        }
+      }
+      while (length > 2) {
+        uint_fast8_t block = (length > 0xff) ? 0xfc : length;
+        result += byteappend(result, 0, block);
+        size += (block + 7) >> 2 << 1;
+        length -= block;
+        count -= block;
+        while (block >= 4) {
+          result += byteappend(result, (*data << 4) | data[1], (data[2] << 4) | data[3]);
+          data += 4;
+          block -= 4;
+        }
+        switch (block) {
+          case 1: result += byteappend(result, *data << 4, 0); break;
+          case 2: result += byteappend(result, (*data << 4) | data[1], 0); break;
+          case 3: result += byteappend(result, (*data << 4) | data[1], data[2] << 4);
+        }
+        data += block;
+      }
+      matches = emit_BMP_compressed_halfbyte_remainder(result, data, length);
+      result += matches;
+      size += matches;
+      data += length;
+      count -= length;
+    }
+  count = emit_BMP_compressed_halfbyte_remainder(result, data, count);
+  result[count] = result[count + 1] = 0;
+  return size + count;
+}
+
+unsigned emit_BMP_compressed_halfbyte_remainder (uint8_t * restrict result, const uint8_t * restrict data, unsigned count) {
+  switch (count) {
+    case 1:
+      bytewrite(result, 1, *data << 4);
+      return 2;
+    case 2:
+      bytewrite(result, 2, (*data << 4) | data[1]);
+      return 2;
+    case 3:
+      result += byteappend(result, 2 + (*data == data[2]), (*data << 4) | data[1]);
+      if (*data == data[2]) return 2;
+      bytewrite(result, 1, data[2] << 4);
+      return 4;
+    default:
+      return 0;
+  }
+}
+
+size_t compress_BMP_byte_row (uint8_t * restrict result, const uint8_t * restrict data, size_t count) {
+  size_t size = 2; // account for the terminator
+  while (count > 1)
+    if (*data == data[1]) {
+      uint_fast8_t length;
+      for (length = 2; length < 0xff && length < count && *data == data[length]; length ++);
+      result += byteappend(result, length, *data);
+      size += 2;
+      data += length;
+      count -= length;
+    } else {
+      size_t length;
+      uint_fast8_t matches = 0;
+      for (length = 1; length < count; length ++) {
+        if (data[length] == data[length - 1])
+          matches ++;
+        else
+          matches = 0;
+        if (matches >= 2) {
+          length -= matches;
+          break;
+        }
+      }
+      while (length > 2) {
+        uint_fast8_t block = (length > 0xff) ? 0xfe : length;
+        result += byteappend(result, 0, block);
+        memcpy(result, data, block);
+        result += block;
+        data += block;
+        size += 2 + block;
+        length -= block;
+        count -= block;
+        if (block & 1) {
+          *(result ++) = 0;
+          size ++;
+        }
+      }
+      if (length == 2) {
+        matches = 1 + (*data == data[1]);
+        result += byteappend(result, matches, *data);
+        size += 2;
+        data += matches;
+        count -= matches;
+        length -= matches;
+      }
+      if (length == 1) {
+        result += byteappend(result, 1, *data);
+        data ++;
+        size += 2;
+        count --;
+      }
+    }
+  if (count == 1) {
+    result += byteappend(result, 1, *data);
+    size += 2;
+  }
+  bytewrite(result, 0, 0);
+  return size;
+}
+
+void append_BMP_palette (struct context * context) {
+  unsigned char * data = append_output_node(context, 4 * (context -> source -> max_palette_index + 1));
+  uint32_t * colors = ctxmalloc(context, sizeof *colors * (context -> source -> max_palette_index + 1));
+  plum_convert_colors(colors, context -> source -> palette, context -> source -> max_palette_index + 1, PLUM_COLOR_32, context -> source -> color_format);
+  for (unsigned p = 0; p <= context -> source -> max_palette_index; p ++) data += byteappend(data, colors[p] >> 16, colors[p] >> 8, colors[p], 0);
+  ctxfree(context, colors);
+}
+
+void generate_BMP_RGB_data (struct context * context, unsigned char * offset_pointer) {
+  unsigned char * attributes = append_output_node(context, 40);
+  write_le32_unaligned(offset_pointer, 54);
+  memset(attributes, 0, 40);
+  *attributes = 40;
+  write_le32_unaligned(attributes + 4, context -> source -> width);
+  write_le32_unaligned(attributes + 8, context -> source -> height);
+  attributes[12] = 1;
+  attributes[14] = 24;
+  uint32_t * data;
+  if ((context -> source -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_32)
+    data = context -> source -> data;
+  else {
+    data = ctxmalloc(context, sizeof *data * context -> source -> width * context -> source -> height);
+    plum_convert_colors(data, context -> source -> data, (size_t) context -> source -> width * context -> source -> height,
+                        PLUM_COLOR_32, context -> source -> color_format);
+  }
+  size_t rowsize = (size_t) context -> source -> width * 3, padding = 0;
+  if (rowsize & 3) {
+    padding = 4 - (rowsize & 3);
+    rowsize += padding;
+  }
+  unsigned char * out = append_output_node(context, rowsize * context -> source -> height);
+  uint_fast32_t row = context -> source -> height - 1;
+  do {
+    size_t pos = (size_t) row * context -> source -> width;
+    for (uint_fast32_t remaining = context -> source -> width; remaining; pos ++, remaining --)
+      out += byteappend(out, data[pos] >> 16, data[pos] >> 8, data[pos]);
+    for (uint_fast32_t p = 0; p < padding; p ++) *(out ++) = 0;
+  } while (row --);
+  if (data != context -> source -> data) ctxfree(context, data);
+}
+
+uint32_t compute_PNG_CRC (const unsigned char * data, size_t size) {
+  static const uint32_t table[] = {
+    /* 0x00 */ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
+    /* 0x08 */ 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
+    /* 0x10 */ 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+    /* 0x18 */ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
+    /* 0x20 */ 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
+    /* 0x28 */ 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+    /* 0x30 */ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
+    /* 0x38 */ 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
+    /* 0x40 */ 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+    /* 0x48 */ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
+    /* 0x50 */ 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
+    /* 0x58 */ 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+    /* 0x60 */ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
+    /* 0x68 */ 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
+    /* 0x70 */ 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+    /* 0x78 */ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
+    /* 0x80 */ 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
+    /* 0x88 */ 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+    /* 0x90 */ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
+    /* 0x98 */ 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
+    /* 0xa0 */ 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+    /* 0xa8 */ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
+    /* 0xb0 */ 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
+    /* 0xb8 */ 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+    /* 0xc0 */ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
+    /* 0xc8 */ 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
+    /* 0xd0 */ 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+    /* 0xd8 */ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
+    /* 0xe0 */ 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
+    /* 0xe8 */ 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+    /* 0xf0 */ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
+    /* 0xf8 */ 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
+  };
+  uint32_t checksum = 0xffffffff;
+  while (size --) checksum = (checksum >> 8) ^ table[(uint8_t) checksum ^ *(data ++)];
+  return ~checksum;
+}
+
+uint32_t compute_Adler32_checksum (const unsigned char * data, size_t size) {
+  uint_fast32_t first = 1, second = 0;
+  while (size --) {
+    first += *(data ++);
+    if (first >= 65521) first -= 65521;
+    second += first;
+    if (second >= 65521) second -= 65521;
+  }
+  return (second << 16) | first;
+}
+
+void plum_convert_colors (void * restrict destination, const void * restrict source, size_t count, unsigned to, unsigned from) {
+  if (!(source && destination && count)) return;
+  if ((from & (PLUM_COLOR_MASK | PLUM_ALPHA_INVERT)) == (to & (PLUM_COLOR_MASK | PLUM_ALPHA_INVERT))) {
+    memcpy(destination, source, plum_color_buffer_size(count, to));
+    return;
+  }
+  #define convert(sp) do                                                                                       \
+    if ((to & PLUM_COLOR_MASK) == PLUM_COLOR_16)                                                               \
+      for (uint16_t * dp = destination; count; count --) *(dp ++) = plum_convert_color(*(sp ++), from, to);    \
+    else if ((to & PLUM_COLOR_MASK) == PLUM_COLOR_64)                                                          \
+      for (uint64_t * dp = destination; count; count --) *(dp ++) = plum_convert_color(*(sp ++), from, to);    \
+    else                                                                                                       \
+      for (uint32_t * dp = destination; count; count --) *(dp ++) = plum_convert_color(*(sp ++), from, to);    \
+  while (false)
+  if ((from & PLUM_COLOR_MASK) == PLUM_COLOR_16) {
+    const uint16_t * sp = source;
+    convert(sp);
+  } else if ((from & PLUM_COLOR_MASK) == PLUM_COLOR_64) {
+    const uint64_t * sp = source;
+    convert(sp);
+  } else {
+    const uint32_t * sp = source;
+    convert(sp);
+  }
+  #undef convert
+}
+
+uint64_t plum_convert_color (uint64_t color, unsigned from, unsigned to) {
+  // here be dragons
+  uint64_t result;
+  if ((from & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    from &= 0xffffu;
+  else if ((from & PLUM_COLOR_MASK) != PLUM_COLOR_64)
+    from &= 0xffffffffu;
+  #define formatpair(from, to) (((from) << 2) & (PLUM_COLOR_MASK << 2) | (to) & PLUM_COLOR_MASK)
+  switch (formatpair(from, to)) {
+    case formatpair(PLUM_COLOR_32, PLUM_COLOR_32):
+    case formatpair(PLUM_COLOR_64, PLUM_COLOR_64):
+    case formatpair(PLUM_COLOR_16, PLUM_COLOR_16):
+    case formatpair(PLUM_COLOR_32X, PLUM_COLOR_32X):
+      result = color;
+      break;
+    case formatpair(PLUM_COLOR_32, PLUM_COLOR_64):
+      result = ((color & 0xff) | ((color << 8) & 0xff0000u) | ((color << 16) & 0xff00000000u) | ((color << 24) & 0xff000000000000u)) * 0x101;
+      break;
+    case formatpair(PLUM_COLOR_32, PLUM_COLOR_16):
+      result = ((color >> 3) & 0x1f) | ((color >> 6) & 0x3e0) | ((color >> 9) & 0x7c00) | ((color >> 16) & 0x8000u);
+      break;
+    case formatpair(PLUM_COLOR_32, PLUM_COLOR_32X):
+      result = ((color << 2) & 0x3fc) | ((color << 4) & 0xff000u) | ((color << 6) & 0x3fc00000u) | (color & 0xc0000000u) |
+               ((color >> 6) & 3) | ((color >> 4) & 0xc00) | ((color >> 2) & 0x300000u);
+      break;
+    case formatpair(PLUM_COLOR_64, PLUM_COLOR_32):
+      result = ((color >> 8) & 0xff) | ((color >> 16) & 0xff00u) | ((color >> 24) & 0xff0000u) | ((color >> 32) & 0xff000000u);
+      break;
+    case formatpair(PLUM_COLOR_64, PLUM_COLOR_16):
+      result = ((color >> 11) & 0x1f) | ((color >> 22) & 0x3e0) | ((color >> 33) & 0x7c00) | ((color >> 48) & 0x8000u);
+      break;
+    case formatpair(PLUM_COLOR_64, PLUM_COLOR_32X):
+      result = ((color >> 6) & 0x3ff) | ((color >> 12) & 0xffc00u) | ((color >> 18) & 0x3ff00000u) | ((color >> 32) & 0xc0000000u);
+      break;
+    case formatpair(PLUM_COLOR_16, PLUM_COLOR_32):
+      result = ((color << 3) & 0xf8) | ((color << 6) & 0xf800u) | ((color << 9) & 0xf80000u) | ((color & 0x8000u) ? 0xff000000u : 0) |
+               ((color >> 2) & 7) | ((color << 1) & 0x700) | ((color << 4) & 0x70000u);
+      break;
+    case formatpair(PLUM_COLOR_16, PLUM_COLOR_64):
+      result = (((color & 0x1f) | ((color << 11) & 0x1f0000u) | ((color << 22) & 0x1f00000000u)) * 0x842) | ((color & 0x8000u) ? 0xffff000000000000u : 0) |
+               ((color >> 4) & 1) | ((color << 7) & 0x10000u) | ((color << 18) & 0x100000000u);
+      break;
+    case formatpair(PLUM_COLOR_16, PLUM_COLOR_32X):
+      result = (((color & 0x1f) | ((color << 5) & 0x7c00) | ((color << 10) & 0x1f00000u)) * 0x21) | ((color & 0x8000u) ? 0xc0000000u : 0);
+      break;
+    case formatpair(PLUM_COLOR_32X, PLUM_COLOR_32):
+      result = ((color >> 2) & 0xff) | ((color >> 4) & 0xff00u) | ((color >> 6) & 0xff0000u) | ((color >> 30) * 0x55000000u);
+      break;
+    case formatpair(PLUM_COLOR_32X, PLUM_COLOR_64):
+      result = ((color << 6) & 0xffc0u) | ((color << 12) & 0xffc00000u) | ((color << 18) & 0xffc000000000u) | ((color >> 30) * 0x5555000000000000u) |
+               ((color >> 4) & 0x3f) | ((color << 2) & 0x3f0000u) | ((color << 8) & 0x3f00000000u);
+      break;
+    case formatpair(PLUM_COLOR_32X, PLUM_COLOR_16):
+      result = ((color >> 5) & 0x1f) | ((color >> 10) & 0x3e0) | ((color >> 15) & 0x7c00) | ((color >> 16) & 0x8000u);
+  }
+  #undef formatpair
+  if ((to ^ from) & PLUM_ALPHA_INVERT) result ^= alpha_component_masks[to & PLUM_COLOR_MASK];
+  return result;
+}
+
+void plum_remove_alpha (struct plum_image * image) {
+  if (!(image && image -> data && plum_check_valid_image_size(image -> width, image -> height, image -> frames))) return;
+  void * colordata;
+  size_t count;
+  if (image -> palette) {
+    colordata = image -> palette;
+    count = image -> max_palette_index + 1;
+  } else {
+    colordata = image -> data;
+    count = (size_t) image -> width * image -> height * image -> frames;
+  }
+  switch (image -> color_format & PLUM_COLOR_MASK) {
+    case PLUM_COLOR_32: {
+      uint32_t * color = colordata;
+      if (image -> color_format & PLUM_ALPHA_INVERT)
+        while (count --) *(color ++) |= 0xff000000u;
+      else
+        while (count --) *(color ++) &= 0xffffffu;
+    } break;
+    case PLUM_COLOR_64: {
+      uint64_t * color = colordata;
+      if (image -> color_format & PLUM_ALPHA_INVERT)
+        while (count --) *(color ++) |= 0xffff000000000000u;
+      else
+        while (count --) *(color ++) &= 0xffffffffffffu;
+    } break;
+    case PLUM_COLOR_16: {
+      uint16_t * color = colordata;
+      if (image -> color_format & PLUM_ALPHA_INVERT)
+        while (count --) *(color ++) |= 0x8000u;
+      else
+        while (count --) *(color ++) &= 0x7fffu;
+    } break;
+    case PLUM_COLOR_32X: {
+      uint32_t * color = colordata;
+      if (image -> color_format & PLUM_ALPHA_INVERT)
+        while (count --) *(color ++) |= 0xc0000000u;
+      else
+        while (count --) *(color ++) &= 0x3fffffffu;
+    }
+  }
+}
+
+bool image_has_transparency (const struct plum_image * image) {
+  size_t count = image -> palette ? image -> max_palette_index + 1 : ((size_t) image -> width * image -> height * image -> frames);
+  #define checkcolors(bits) do {                                                                 \
+    const uint ## bits ## _t * color = image -> palette ? image -> palette : image -> data;      \
+    uint ## bits ## _t mask = alpha_component_masks[image -> color_format & PLUM_COLOR_MASK];    \
+    if (image -> color_format & PLUM_ALPHA_INVERT) {                                             \
+      while (count --) if (*(color ++) < mask) return true;                                      \
+    } else {                                                                                     \
+      mask = ~mask;                                                                              \
+      while (count --) if (*(color ++) > mask) return true;                                      \
+    }                                                                                            \
+  } while (false)
+  switch (image -> color_format & PLUM_COLOR_MASK) {
+    case PLUM_COLOR_64: checkcolors(64); break;
+    case PLUM_COLOR_16: checkcolors(16); break;
+    default: checkcolors(32);
+  }
+  #undef checkcolors
+  return false;
+}
+
+uint32_t get_color_depth (const struct plum_image * image) {
+  uint_fast32_t red, green, blue, alpha;
+  switch (image -> color_format & PLUM_COLOR_MASK) {
+    case PLUM_COLOR_32:
+      red = green = blue = alpha = 8;
+      break;
+    case PLUM_COLOR_64:
+      red = green = blue = alpha = 16;
+      break;
+    case PLUM_COLOR_16:
+      red = green = blue = 5;
+      alpha = 1;
+      break;
+    case PLUM_COLOR_32X:
+      red = green = blue = 10;
+      alpha = 2;
+  }
+  const struct plum_metadata * colorinfo = plum_find_metadata(image, PLUM_METADATA_COLOR_DEPTH);
+  if (colorinfo) {
+    const unsigned char * data = colorinfo -> data;
+    if (*data || data[1] || data[2]) {
+      if (*data) red = *data;
+      if (data[1]) green = data[1];
+      if (data[2]) blue = data[2];
+    } else if (colorinfo -> size >= 5 && data[4])
+      red = green = blue = data[4];
+    if (colorinfo -> size >= 4 && data[3]) alpha = data[3];
+  }
+  if (red > 16) red = 16;
+  if (green > 16) green = 16;
+  if (blue > 16) blue = 16;
+  if (alpha > 16) alpha = 16;
+  return red | (green << 8) | (blue << 16) | (alpha << 24);
+}
+
+uint32_t get_true_color_depth (const struct plum_image * image) {
+  uint32_t result = get_color_depth(image);
+  if (!image_has_transparency(image)) result &= 0xffffffu;
+  return result;
+}
+
+struct plum_rectangle * get_frame_boundaries (struct context * context, bool anchor_corner) {
+  const struct plum_metadata * metadata = plum_find_metadata(context -> source, PLUM_METADATA_FRAME_AREA);
+  if (!metadata) return NULL;
+  struct plum_rectangle * result = ctxmalloc(context, sizeof *result * context -> source -> frames);
+  uint_fast32_t frames = (context -> source -> frames > metadata -> size / sizeof *result) ? metadata -> size / sizeof *result : context -> source -> frames;
+  if (frames) {
+    memcpy(result, metadata -> data, sizeof *result * frames);
+    if (anchor_corner)
+      for (uint_fast32_t frame = 0; frame < frames; frame ++) {
+        result[frame].width += result[frame].left;
+        result[frame].height += result[frame].top;
+        result[frame].top = result[frame].left = 0;
+      }
+  }
+  for (uint_fast32_t frame = frames; frame < context -> source -> frames; frame ++)
+    result[frame] = (struct plum_rectangle) {.left = 0, .top = 0, .width = context -> source -> width, .height = context -> source -> height};
+  return result;
+}
+
+void adjust_frame_boundaries (const struct plum_image * image, struct plum_rectangle * restrict boundaries) {
+  uint64_t empty_color = get_empty_color(image);
+  if (image -> palette) {
+    bool empty[256];
+    switch (image -> color_format & PLUM_COLOR_MASK) {
+      case PLUM_COLOR_64:
+        for (size_t p = 0; p <= image -> max_palette_index; p ++) empty[p] = image -> palette64[p] == empty_color;
+        break;
+      case PLUM_COLOR_16:
+        for (size_t p = 0; p <= image -> max_palette_index; p ++) empty[p] = image -> palette16[p] == empty_color;
+        break;
+      default:
+        for (size_t p = 0; p <= image -> max_palette_index; p ++) empty[p] = image -> palette32[p] == empty_color;
+    }
+    size_t index = 0;
+    for (uint_fast32_t frame = 0; frame < image -> frames; frame ++) {
+      bool adjust = true;
+      for (size_t remaining = (size_t) boundaries[frame].top * image -> width; remaining; remaining --)
+        if (!empty[image -> data8[index ++]]) goto paldone;
+      if (boundaries[frame].left || boundaries[frame].width != image -> width)
+        for (uint_fast32_t row = 0; row < boundaries[frame].height; row ++) {
+          for (uint_fast32_t col = 0; col < boundaries[frame].left; col ++) if (!empty[image -> data8[index ++]]) goto paldone;
+          index += boundaries[frame].width;
+          for (uint_fast32_t col = boundaries[frame].left + boundaries[frame].width; col < image -> width; col ++)
+            if (!empty[image -> data8[index ++]]) goto paldone;
+        }
+      else
+        index += (size_t) boundaries[frame].height * image -> width;
+      for (size_t remaining = (size_t) (image -> height - boundaries[frame].top - boundaries[frame].height) * image -> width; remaining; remaining --)
+        if (!empty[image -> data8[index ++]]) goto paldone;
+      adjust = false;
+      paldone:
+      if (adjust) boundaries[frame] = (struct plum_rectangle) {.left = 0, .top = 0, .width = image -> width, .height = image -> height};
+    }
+  } else {
+    size_t index = 0;
+    #define checkframe(bits) do                                                                                                                             \
+      for (uint_fast32_t frame = 0; frame < image -> frames; frame ++) {                                                                                    \
+        bool adjust = true;                                                                                                                                 \
+        for (size_t remaining = (size_t) boundaries[frame].top * image -> width; remaining; remaining --)                                                   \
+          if (image -> data ## bits[index ++] != empty_color) goto done ## bits;                                                                            \
+        if (boundaries[frame].left || boundaries[frame].width != image -> width)                                                                            \
+          for (uint_fast32_t row = 0; row < boundaries[frame].height; row ++) {                                                                             \
+            for (uint_fast32_t col = 0; col < boundaries[frame].left; col ++) if (image -> data ## bits[index ++] != empty_color) goto done ## bits;        \
+            index += boundaries[frame].width;                                                                                                               \
+            for (uint_fast32_t col = boundaries[frame].left + boundaries[frame].width; col < image -> width; col ++)                                        \
+              if (image -> data ## bits[index ++] != empty_color) goto done ## bits;                                                                        \
+          }                                                                                                                                                 \
+        else                                                                                                                                                \
+          index += (size_t) boundaries[frame].height * image -> width;                                                                                      \
+        for (size_t remaining = (size_t) (image -> height - boundaries[frame].top - boundaries[frame].height) * image -> width; remaining; remaining --)    \
+          if (image -> data ## bits[index ++] != empty_color) goto done ## bits;                                                                            \
+        adjust = false;                                                                                                                                     \
+        done ## bits:                                                                                                                                       \
+        if (adjust) boundaries[frame] = (struct plum_rectangle) {.left = 0, .top = 0, .width = image -> width, .height = image -> height};                  \
+      }                                                                                                                                                     \
+    while (false)
+    switch (image -> color_format & PLUM_COLOR_MASK) {
+      case PLUM_COLOR_16: checkframe(16); break;
+      case PLUM_COLOR_64: checkframe(64); break;
+      default: checkframe(32);
+    }
+    #undef checkframe
+  }
+}
+
+bool image_rectangles_have_transparency (const struct plum_image * image, const struct plum_rectangle * rectangles) {
+  size_t framesize = (size_t) image -> width * image -> height;
+  if (image -> palette) {
+    bool transparent[256];
+    uint_fast64_t mask = alpha_component_masks[image -> color_format & PLUM_COLOR_MASK], match = (image -> color_format & PLUM_ALPHA_INVERT) ? mask : 0;
+    switch (image -> color_format & PLUM_COLOR_MASK) {
+      case PLUM_COLOR_64:
+        for (size_t p = 0; p <= image -> max_palette_index; p ++) transparent[p] = (image -> palette64[p] & mask) != match;
+        break;
+      case PLUM_COLOR_16:
+        for (size_t p = 0; p <= image -> max_palette_index; p ++) transparent[p] = (image -> palette16[p] & mask) != match;
+        break;
+      default:
+        for (size_t p = 0; p <= image -> max_palette_index; p ++) transparent[p] = (image -> palette32[p] & mask) != match;
+    }
+    for (uint_fast32_t frame = 0; frame < image -> frames; frame ++) for (uint_fast32_t row = 0; row < rectangles[frame].height; row ++) {
+      const uint8_t * rowdata = image -> data8 + framesize * frame + (size_t) image -> width * (row + rectangles[frame].top) + rectangles[frame].left;
+      for (uint_fast32_t col = 0; col < rectangles[frame].width; col ++) if (transparent[rowdata[col]]) return true;
+    }
+  } else {
+    #define checkcolors(bits, address, count) do {                                                 \
+      const uint ## bits ## _t * color = (const void *) address;                                   \
+      size_t remaining = count;                                                                    \
+      uint ## bits ## _t mask = alpha_component_masks[image -> color_format & PLUM_COLOR_MASK];    \
+      if (image -> color_format & PLUM_ALPHA_INVERT) {                                             \
+        while (remaining --) if (*(color ++) < mask) return true;                                  \
+      } else {                                                                                     \
+        mask = ~mask;                                                                              \
+        while (remaining --) if (*(color ++) > mask) return true;                                  \
+      }                                                                                            \
+    } while (false)
+    for (uint_fast32_t frame = 0; frame < image -> frames; frame ++) {
+      size_t frameoffset = framesize * frame + (size_t) rectangles[frame].top * image -> width + rectangles[frame].left;
+      const uint8_t * framedata = image -> data8 + plum_color_buffer_size(frameoffset, image -> color_format);
+      if (rectangles[frame].left || rectangles[frame].width != image -> width) {
+        size_t rowsize = plum_color_buffer_size(image -> width, image -> color_format);
+        for (uint_fast32_t row = 0; row < rectangles[frame].height; row ++, framedata += rowsize)
+          switch (image -> color_format & PLUM_COLOR_MASK) {
+            case PLUM_COLOR_64: checkcolors(64, framedata, rectangles[frame].width); break;
+            case PLUM_COLOR_16: checkcolors(16, framedata, rectangles[frame].width); break;
+            default: checkcolors(32, framedata, rectangles[frame].width);
+          }
+      } else {
+        size_t count = (size_t) rectangles[frame].height * image -> width;
+        switch (image -> color_format & PLUM_COLOR_MASK) {
+          case PLUM_COLOR_64: checkcolors(64, framedata, count); break;
+          case PLUM_COLOR_16: checkcolors(16, framedata, count); break;
+          default: checkcolors(32, framedata, count);
+        }
+      }
+    }
+    #undef checkcolors
+  }
+  return false;
+}
+
+void validate_image_size (struct context * context, size_t limit) {
+  if (!(context -> image -> width && context -> image -> height && context -> image -> frames)) throw(context, PLUM_ERR_NO_DATA);
+  if (!plum_check_limited_image_size(context -> image -> width, context -> image -> height, context -> image -> frames, limit))
+    throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+}
+
+int plum_check_valid_image_size (uint32_t width, uint32_t height, uint32_t frames) {
+  return plum_check_limited_image_size(width, height, frames, SIZE_MAX);
+}
+
+int plum_check_limited_image_size (uint32_t width, uint32_t height, uint32_t frames, size_t limit) {
+  if (!(width && height && frames)) return 0;
+  size_t p = width;
+  if (limit > SIZE_MAX / sizeof(uint64_t)) limit = SIZE_MAX / sizeof(uint64_t);
+  if (p * height / height != p) return 0;
+  p *= height;
+  if (p * frames / frames != p) return 0;
+  p *= frames;
+  return p <= limit;
+}
+
+size_t plum_color_buffer_size (size_t size, unsigned flags) {
+  if (size > SIZE_MAX / sizeof(uint64_t)) return 0;
+  if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    return size * sizeof(uint64_t);
+  else if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    return size * sizeof(uint16_t);
+  else
+    return size * sizeof(uint32_t);
+}
+
+size_t plum_pixel_buffer_size (const struct plum_image * image) {
+  if (!image) return 0;
+  if (!plum_check_valid_image_size(image -> width, image -> height, image -> frames)) return 0;
+  size_t count = (size_t) image -> width * image -> height * image -> frames;
+  return image -> palette ? count : plum_color_buffer_size(count, image -> color_format);
+}
+
+size_t plum_palette_buffer_size (const struct plum_image * image) {
+  if (!image) return 0;
+  return plum_color_buffer_size(image -> max_palette_index + 1, image -> color_format);
+}
+
+void allocate_framebuffers (struct context * context, unsigned flags, bool palette) {
+  size_t size = (size_t) context -> image -> width * context -> image -> height * context -> image -> frames;
+  if (!palette) size = plum_color_buffer_size(size, flags);
+  if (!(context -> image -> data = plum_malloc(context -> image, size))) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  context -> image -> color_format = flags & (PLUM_COLOR_MASK | PLUM_ALPHA_INVERT);
+}
+
+void write_framebuffer_to_image (struct plum_image * image, const uint64_t * restrict framebuffer, uint32_t frame, unsigned flags) {
+  size_t pixels = (size_t) image -> width * image -> height, framesize = plum_color_buffer_size(pixels, flags);
+  plum_convert_colors(image -> data8 + framesize * frame, framebuffer, pixels, flags, PLUM_COLOR_64);
+}
+
+void write_palette_framebuffer_to_image (struct context * context, const uint8_t * restrict framebuffer, const uint64_t * restrict palette, uint32_t frame,
+                                         unsigned flags, uint8_t max_palette_index) {
+  size_t framesize = (size_t) context -> image -> width * context -> image -> height;
+  if (max_palette_index < 0xff)
+    for (size_t pos = 0; pos < framesize; pos ++) if (framebuffer[pos] > max_palette_index) throw(context, PLUM_ERR_INVALID_COLOR_INDEX);
+  if (context -> image -> palette) {
+    memcpy(context -> image -> data8 + framesize * frame, framebuffer, framesize);
+    return;
+  }
+  void * converted = ctxmalloc(context, plum_color_buffer_size(max_palette_index + 1, flags));
+  plum_convert_colors(converted, palette, max_palette_index + 1, flags, PLUM_COLOR_64);
+  plum_convert_indexes_to_colors(context -> image -> data8 + plum_color_buffer_size(framesize, flags) * frame, framebuffer, converted, framesize, flags);
+  ctxfree(context, converted);
+}
+
+void write_palette_to_image (struct context * context, const uint64_t * restrict palette, unsigned flags) {
+  size_t size = plum_color_buffer_size(context -> image -> max_palette_index + 1, flags);
+  if (!(context -> image -> palette = plum_malloc(context -> image, size))) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  plum_convert_colors(context -> image -> palette, palette, context -> image -> max_palette_index + 1, flags, PLUM_COLOR_64);
+}
+
+unsigned plum_rotate_image (struct plum_image * image, unsigned count, int flip) {
+  unsigned error = plum_validate_image(image);
+  if (error) return error;
+  count &= 3;
+  if (!(count || flip)) return 0;
+  size_t framesize = (size_t) image -> width * image -> height;
+  void * buffer;
+  if (image -> palette)
+    buffer = malloc(framesize);
+  else
+    buffer = malloc(plum_color_buffer_size(framesize, image -> color_format));
+  if (!buffer) return PLUM_ERR_OUT_OF_MEMORY;
+  if (count & 1) {
+    uint_fast32_t temp = image -> width;
+    image -> width = image -> height;
+    image -> height = temp;
+  }
+  size_t (* coordinate) (size_t, size_t, size_t, size_t);
+  switch (count) {
+    case 0: coordinate = flip_coordinate; break; // we know flip has to be enabled because null rotations were excluded already
+    case 1: coordinate = flip ? rotate_right_flip_coordinate : rotate_right_coordinate; break;
+    case 2: coordinate = flip ? rotate_both_flip_coordinate : rotate_both_coordinate; break;
+    case 3: coordinate = flip ? rotate_left_flip_coordinate : rotate_left_coordinate;
+  }
+  if (image -> palette)
+    for (uint_fast32_t frame = 0; frame < image -> frames; frame ++)
+      rotate_frame_8(image -> data8 + framesize * frame, buffer, image -> width, image -> height, coordinate);
+  else if ((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    for (uint_fast32_t frame = 0; frame < image -> frames; frame ++)
+      rotate_frame_64(image -> data64 + framesize * frame, buffer, image -> width, image -> height, coordinate);
+  else if ((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    for (uint_fast32_t frame = 0; frame < image -> frames; frame ++)
+      rotate_frame_16(image -> data16 + framesize * frame, buffer, image -> width, image -> height, coordinate);
+  else
+    for (uint_fast32_t frame = 0; frame < image -> frames; frame ++)
+      rotate_frame_32(image -> data32 + framesize * frame, buffer, image -> width, image -> height, coordinate);
+  free(buffer);
+  return 0;
+}
+
+#define ROTATE_FRAME_FUNCTION(bits) \
+void rotate_frame_ ## bits (uint ## bits ## _t * restrict frame, uint ## bits ## _t * restrict buffer, size_t width, size_t height, \
+                            size_t (* coordinate) (size_t, size_t, size_t, size_t)) {                                               \
+  for (size_t row = 0; row < height; row ++) for (size_t col = 0; col < width; col ++)                                              \
+    buffer[row * width + col] = frame[coordinate(row, col, width, height)];                                                         \
+  memcpy(frame, buffer, sizeof *frame * width * height);                                                                            \
+}
+
+ROTATE_FRAME_FUNCTION(8)
+ROTATE_FRAME_FUNCTION(16)
+ROTATE_FRAME_FUNCTION(32)
+ROTATE_FRAME_FUNCTION(64)
+
+#undef ROTATE_FRAME_FUNCTION
+
+size_t rotate_left_coordinate (size_t row, size_t col, size_t width, size_t height) {
+  (void) width;
+  return (col + 1) * height - (row + 1);
+}
+
+size_t rotate_right_coordinate (size_t row, size_t col, size_t width, size_t height) {
+  return (width - 1 - col) * height + row;
+}
+
+size_t rotate_both_coordinate (size_t row, size_t col, size_t width, size_t height) {
+  return height * width - 1 - (row * width + col);
+}
+
+size_t flip_coordinate (size_t row, size_t col, size_t width, size_t height) {
+  return (height - 1 - row) * width + col;
+}
+
+size_t rotate_left_flip_coordinate (size_t row, size_t col, size_t width, size_t height) {
+  (void) width;
+  return col * height + row;
+}
+
+size_t rotate_right_flip_coordinate (size_t row, size_t col, size_t width, size_t height) {
+  return height * width - 1 - (col * height + row);
+}
+
+size_t rotate_both_flip_coordinate (size_t row, size_t col, size_t width, size_t height) {
+  (void) height;
+  return (row + 1) * width - (col + 1);
+}
+
+uint64_t adjust_frame_duration (uint64_t duration, int64_t * restrict remainder) {
+  if (*remainder < 0)
+    if (duration < -*remainder) {
+      *remainder += duration;
+      return 0;
+    } else {
+      duration += (uint64_t) *remainder;
+      *remainder = 0;
+      return duration;
+    }
+  else {
+    duration += *remainder;
+    if (duration < *remainder) duration = UINT64_MAX;
+    *remainder = 0;
+    return duration;
+  }
+}
+
+void update_frame_duration_remainder (uint64_t actual, uint64_t computed, int64_t * restrict remainder) {
+  if (actual < computed) {
+    uint64_t difference = computed - actual;
+    if (difference > INT64_MAX) difference = INT64_MAX;
+    if (*remainder >= 0 || difference - *remainder <= (uint64_t) INT64_MIN)
+      *remainder -= (int64_t) difference;
+    else
+      *remainder = INT64_MIN;
+  } else {
+    uint64_t difference = actual - computed;
+    if (difference > INT64_MAX) difference = INT64_MAX;
+    if (*remainder < 0 || difference + *remainder <= (uint64_t) INT64_MAX)
+      *remainder += (int64_t) difference;
+    else
+      *remainder = INT64_MAX;
+  }
+}
+
+void calculate_frame_duration_fraction (uint64_t duration, uint32_t limit, uint32_t * restrict numerator, uint32_t * restrict denominator) {
+  // if the number is too big to be represented at all, just fail early and return infinity
+  if (duration >= 1000000000u * ((uint64_t) limit + 1)) {
+    *numerator = 1;
+    *denominator = 0;
+    return;
+  }
+  // if the number can be represented exactly, do that
+  *denominator = 1000000000u;
+  while (!((duration | *denominator) & 1)) {
+    duration >>= 1;
+    *denominator >>= 1;
+  }
+  while (!(duration % 5 || *denominator % 5)) {
+    duration /= 5;
+    *denominator /= 5;
+  }
+  if (duration <= limit && *denominator <= limit) {
+    *numerator = duration;
+    return;
+  }
+  // otherwise, calculate the coefficients of the value's continued fraction representation until the represented fraction exceeds the range limit
+  // this will necessarily stop before running out of coefficients because we know at this stage that the exact value doesn't fit
+  uint64_t cumulative_numerator = duration / *denominator, cumulative_denominator = 1, previous_numerator = 1, previous_denominator = 0;
+  uint32_t coefficient, original_denominator = *denominator;
+  *numerator = duration % *denominator;
+  while (true) {
+    coefficient = *denominator / *numerator;
+    uint64_t partial_numerator = *denominator % *numerator;
+    *denominator = *numerator;
+    *numerator = partial_numerator;
+    if (cumulative_numerator > cumulative_denominator) {
+      uint64_t partial_cumulative_numerator = cumulative_numerator * coefficient + previous_numerator;
+      if (partial_cumulative_numerator > limit) break;
+      previous_numerator = cumulative_numerator;
+      cumulative_numerator = partial_cumulative_numerator;
+      uint64_t partial_cumulative_denominator = cumulative_denominator * coefficient + previous_denominator;
+      previous_denominator = cumulative_denominator;
+      cumulative_denominator = partial_cumulative_denominator;
+    } else {
+      uint64_t partial_cumulative_denominator = cumulative_denominator * coefficient + previous_denominator;
+      if (partial_cumulative_denominator > limit) break;
+      previous_denominator = cumulative_denominator;
+      cumulative_denominator = partial_cumulative_denominator;
+      uint64_t partial_cumulative_numerator = cumulative_numerator * coefficient + previous_numerator;
+      previous_numerator = cumulative_numerator;
+      cumulative_numerator = partial_cumulative_numerator;
+    }
+  }
+  // the current coefficient would be too large to fit, so try reducing it until it fits; if a good coefficient is found, use it
+  uint64_t threshold = coefficient / 2 + 1;
+  if (cumulative_numerator > cumulative_denominator) {
+    while (-- coefficient >= threshold) if (cumulative_numerator * coefficient + previous_numerator <= limit) break;
+  } else
+    while (-- coefficient >= threshold) if (cumulative_denominator * coefficient + previous_denominator <= limit) break;
+  if (coefficient >= threshold) {
+    *numerator = cumulative_numerator * coefficient + previous_numerator;
+    *denominator = cumulative_denominator * coefficient + previous_denominator;
+    return;
+  }
+  // reducing the coefficient to half its original value may or may not improve accuracy, so this must be tested
+  // if it doesn't, return the previous step's values; if it does, return the improved values
+  *numerator = cumulative_numerator;
+  *denominator = cumulative_denominator;
+  if (coefficient) {
+    cumulative_numerator = cumulative_numerator * coefficient + previous_numerator;
+    cumulative_denominator = cumulative_denominator * coefficient + previous_denominator;
+    if (cumulative_numerator > limit || cumulative_denominator > limit) return;
+    /* The exact value, old approximation and new approximation are respectively proportional to the products of three quantities:
+       exact value       ~ *denominator * duration * cumulative_denominator
+       old approximation ~ *numerator * original_denominator * cumulative_denominator
+       new approximation ~ *denominator * original_denominator * cumulative_numerator
+       The problem is that these quantities are 96 bits wide, and thus some care must be exercised when computing them and comparing them. */
+    uint32_t exact_low, old_low, new_low; // bits 0-31
+    uint64_t exact_high, old_high, new_high; // bits 32-95
+    uint64_t partial_exact = *denominator * cumulative_denominator;
+    exact_high = (partial_exact >> 32) * duration + (duration >> 32) * partial_exact;
+    partial_exact = (partial_exact & 0xffffffffu) * (duration & 0xffffffffu);
+    exact_high += partial_exact >> 32;
+    exact_low = partial_exact;
+    uint64_t partial_old = *numerator * (uint64_t) original_denominator;
+    old_high = (partial_old >> 32) * cumulative_denominator;
+    partial_old = (partial_old & 0xffffffffu) * cumulative_denominator;
+    old_high += partial_old >> 32;
+    old_low = partial_old;
+    uint64_t partial_new = *denominator * (uint64_t) original_denominator;
+    new_high = (partial_new >> 32) * cumulative_numerator;
+    partial_new = (partial_new & 0xffffffffu) * cumulative_numerator;
+    new_high += partial_new >> 32;
+    new_low = partial_new;
+    // do the subtractions, and abuse two's complement to deal with negative results
+    old_high -= exact_high;
+    uint64_t old_diff = (uint64_t) old_low - exact_low;
+    old_low = old_diff;
+    if (old_diff & 0xffffffff00000000u) old_high --;
+    if (old_high & 0x8000000000000000u)
+      if (old_low) {
+        old_high = ~old_high;
+        old_low = -old_low;
+      } else
+        old_high = -old_high;
+    new_high -= exact_high;
+    uint64_t new_diff = (uint64_t) new_low - exact_low;
+    new_low = new_diff;
+    if (new_diff & 0xffffffff00000000u) new_high --;
+    if (new_high & 0x8000000000000000u)
+      if (new_low) {
+        new_high = ~new_high;
+        new_low = -new_low;
+      } else
+        new_high = -new_high;
+    // and finally, compare and use the new results if they are better
+    if (new_high < old_high || (new_high == old_high && new_low <= old_low)) {
+      *numerator = cumulative_numerator;
+      *denominator = cumulative_denominator;
+    }
+  }
+}
+
+unsigned char * compress_GIF_data (struct context * context, const unsigned char * restrict data, size_t count, size_t * length, unsigned codesize) {
+  struct compressed_GIF_code * codes = ctxmalloc(context, sizeof *codes * 4097);
+  initialize_GIF_compression_codes(codes, codesize);
+  *length = 0;
+  size_t allocated = 254; // initial size
+  unsigned char * output = ctxmalloc(context, allocated);
+  unsigned current_codesize = codesize + 1, bits = current_codesize, max_code = (1 << codesize) + 1, current_code = *(data ++);
+  uint_fast32_t chain = 1, codeword = 1 << codesize;
+  uint_fast8_t shortchains = 0;
+  while (-- count) {
+    uint_fast8_t search = *(data ++);
+    uint_fast16_t p;
+    for (p = 0; p <= max_code; p ++) if (!codes[p].type && codes[p].reference == current_code && codes[p].value == search) break;
+    if (p <= max_code) {
+      current_code = p;
+      chain ++;
+    } else {
+      codeword |= current_code << bits;
+      bits += current_codesize;
+      codes[++ max_code] = (struct compressed_GIF_code) {.type = 0, .reference = current_code, .value = search};
+      current_code = search;
+      if (current_codesize > codesize + 2)
+        if (chain <= current_codesize / codesize)
+          shortchains ++;
+        else if (shortchains)
+          shortchains --;
+      chain = 1;
+      if (max_code > 4095) max_code = 4095;
+      if (max_code == (1 << current_codesize)) current_codesize ++;
+      if (shortchains > codesize + 8) {
+        // output a clear code and reset the code table
+        codeword |= 1 << (bits + codesize);
+        bits += current_codesize;
+        max_code = (1 << codesize) + 1;
+        current_codesize = codesize + 1;
+        shortchains = 0;
+      }
+    }
+    while (bits >= 8) {
+      if (allocated == *length) output = ctxrealloc(context, output, allocated <<= 1);
+      output[(*length) ++] = codeword;
+      codeword >>= 8;
+      bits -= 8;
+    }
+  }
+  codeword |= current_code << bits;
+  bits += current_codesize;
+  codeword |= ((1 << codesize) + 1) << bits;
+  bits += current_codesize;
+  while (bits) {
+    if (allocated == *length) output = ctxrealloc(context, output, allocated += 4);
+    output[(*length) ++] = codeword;
+    codeword >>= 8;
+    bits = (bits > 8) ? bits - 8 : 0;
+  }
+  ctxfree(context, codes);
+  return output;
+}
+
+void decompress_GIF_data (struct context * context, unsigned char * restrict result, const unsigned char * restrict source, size_t expected_length,
+                          size_t length, unsigned codesize) {
+  struct compressed_GIF_code * codes = ctxmalloc(context, sizeof *codes * 4097);
+  initialize_GIF_compression_codes(codes, codesize);
+  unsigned bits = 0, current_codesize = codesize + 1, max_code = (1 << codesize) + 1;
+  uint_fast32_t codeword = 0;
+  int lastcode = -1;
+  unsigned char * current = result;
+  unsigned char * limit = result + expected_length;
+  while (true) {
+    while (bits < current_codesize) {
+      if (!(length --)) {
+        // handle images that are so broken that they never emit a stop code
+        if (current != limit) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        ctxfree(context, codes);
+        return;
+      }
+      codeword |= (uint_fast32_t) *(source ++) << bits;
+      bits += 8;
+    }
+    uint_fast16_t code = codeword & ((1u << current_codesize) - 1);
+    codeword >>= current_codesize;
+    bits -= current_codesize;
+    switch (codes[code].type) {
+      case 0:
+        emit_GIF_data(context, codes, code, &current, limit);
+        if (lastcode >= 0)
+          codes[++ max_code] = (struct compressed_GIF_code) {.reference = lastcode, .value = find_leading_GIF_code(codes, code), .type = 0};
+        lastcode = code;
+        break;
+      case 1:
+        initialize_GIF_compression_codes(codes, codesize);
+        current_codesize = codesize + 1;
+        max_code = (1 << codesize) + 1;
+        lastcode = -1;
+        break;
+      case 2:
+        if (current != limit) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        ctxfree(context, codes);
+        return;
+      case 3:
+        if (code != max_code + 1) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (lastcode < 0) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        codes[++ max_code] = (struct compressed_GIF_code) {.reference = lastcode, .value = find_leading_GIF_code(codes, lastcode), .type = 0};
+        emit_GIF_data(context, codes, max_code, &current, limit);
+        lastcode = code;
+    }
+    if (max_code >= 4095)
+      max_code = 4095;
+    else if (max_code == ((1 << current_codesize) - 1))
+      current_codesize ++;
+  }
+}
+
+void initialize_GIF_compression_codes (struct compressed_GIF_code * restrict codes, unsigned codesize) {
+  unsigned code;
+  for (code = 0; code < (1 << codesize); code ++) codes[code] = (struct compressed_GIF_code) {.reference = -1, .value = code, .type = 0};
+  codes[code ++] = (struct compressed_GIF_code) {.type = 1, .reference = -1};
+  codes[code ++] = (struct compressed_GIF_code) {.type = 2, .reference = -1};
+  for (; code < 4096; code ++) codes[code] = (struct compressed_GIF_code) {.type = 3, .reference = -1};
+}
+
+uint8_t find_leading_GIF_code (const struct compressed_GIF_code * restrict codes, unsigned code) {
+  return (codes[code].reference < 0) ? codes[code].value : find_leading_GIF_code(codes, codes[code].reference);
+}
+
+void emit_GIF_data (struct context * context, const struct compressed_GIF_code * restrict codes, unsigned code, unsigned char ** result, unsigned char * limit) {
+  if (codes[code].reference >= 0) emit_GIF_data(context, codes, codes[code].reference, result, limit);
+  if (*result >= limit) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  *((*result) ++) = codes[code].value;
+}
+
+void load_GIF_data (struct context * context, unsigned flags, size_t limit) {
+  if (context -> size < 14) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  context -> image -> type = PLUM_IMAGE_GIF;
+  context -> image -> width = read_le16_unaligned(context -> data + 6);
+  context -> image -> height = read_le16_unaligned(context -> data + 8);
+  size_t offset = 13;
+  uint64_t transparent = 0xffff000000000000u;
+  // note: load_GIF_palettes also initializes context -> image -> frames (and context -> image -> palette) and validates the image's structure
+  uint64_t ** palettes = load_GIF_palettes_and_frame_count(context, flags, &offset, &transparent); // will be leaked (collected at the end)
+  validate_image_size(context, limit);
+  allocate_framebuffers(context, flags, !!context -> image -> palette);
+  uint64_t * durations;
+  uint8_t * disposals;
+  add_animation_metadata(context, &durations, &disposals);
+  struct plum_rectangle * frameareas = add_frame_area_metadata(context);
+  for (uint_fast32_t frame = 0; frame < context -> image -> frames; frame ++)
+    load_GIF_frame(context, &offset, flags, frame, palettes ? palettes[frame] : NULL, transparent, durations + frame, disposals + frame, frameareas + frame);
+  if (!plum_find_metadata(context -> image, PLUM_METADATA_LOOP_COUNT)) add_loop_count_metadata(context, 1);
+}
+
+uint64_t ** load_GIF_palettes_and_frame_count (struct context * context, unsigned flags, size_t * restrict offset, uint64_t * restrict transparent_color) {
+  // will also validate block order
+  unsigned char depth = 1 + ((context -> data[10] >> 4) & 7);
+  add_color_depth_metadata(context, depth, depth, depth, 1, 0);
+  uint64_t * global_palette = ctxcalloc(context, 256 * sizeof *global_palette);
+  unsigned global_palette_size = 0;
+  if (context -> data[10] & 0x80) {
+    global_palette_size = 2 << (context -> data[10] & 7);
+    load_GIF_palette(context, global_palette, offset, global_palette_size);
+    if (context -> data[11] < global_palette_size) {
+      add_background_color_metadata(context, global_palette[context -> data[11]], flags);
+      *transparent_color |= global_palette[context -> data[11]];
+    }
+  }
+  size_t scan_offset = *offset;
+  unsigned real_global_palette_size = global_palette_size, transparent_index = 256, next_transparent_index = 256;
+  bool seen_extension = false;
+  uint64_t ** result = NULL;
+  while (scan_offset < context -> size) switch (context -> data[scan_offset ++]) {
+    case 0x21: {
+      if (scan_offset == context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      uint_fast8_t exttype = context -> data[scan_offset ++];
+      if (exttype == 0xf9) {
+        if (seen_extension) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        seen_extension = true;
+        size_t extsize;
+        unsigned char * extdata = load_GIF_data_blocks(context, &scan_offset, &extsize);
+        if (extsize != 4) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (*extdata & 1)
+          next_transparent_index = extdata[3];
+        else
+          next_transparent_index = 256;
+        ctxfree(context, extdata);
+      } else if (exttype < 0x80)
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      else
+        skip_GIF_data_blocks(context, &scan_offset);
+    } break;
+    case 0x2c: {
+      if (scan_offset > context -> size - 9) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      scan_offset += 9;
+      context -> image -> frames ++;
+      if (!context -> image -> frames) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+      bool smaller_size = read_le16_unaligned(context -> data + scan_offset - 9) || read_le16_unaligned(context -> data + scan_offset - 7) ||
+                          read_le16_unaligned(context -> data + scan_offset - 5) != context -> image -> width ||
+                          read_le16_unaligned(context -> data + scan_offset - 3) != context -> image -> height;
+      uint64_t * local_palette = ctxmalloc(context, 256 * sizeof *local_palette);
+      unsigned local_palette_size = 2 << (context -> data[scan_offset - 1] & 7);
+      if (context -> data[scan_offset - 1] & 0x80)
+        load_GIF_palette(context, local_palette, &scan_offset, local_palette_size);
+      else
+        local_palette_size = 0;
+      if (!(local_palette_size || real_global_palette_size)) throw(context, PLUM_ERR_UNDEFINED_PALETTE);
+      if (next_transparent_index < (local_palette_size ? local_palette_size : real_global_palette_size))
+        local_palette[next_transparent_index] = *transparent_color;
+      else
+        next_transparent_index = 256;
+      if (transparent_index == 256) transparent_index = next_transparent_index;
+      if (global_palette_size && !result) {
+        // check if the current palette is compatible with the global one; if so, don't add any per-frame palettes
+        if (!(smaller_size && next_transparent_index == 256) && transparent_index == next_transparent_index) {
+          if (!local_palette_size) goto added;
+          unsigned min = (local_palette_size < global_palette_size) ? local_palette_size : global_palette_size;
+          // temporarily reset this location so it won't fail the check on that spot
+          if (next_transparent_index < min) local_palette[next_transparent_index] = global_palette[next_transparent_index];
+          bool palcheck = !memcmp(local_palette, global_palette, min * sizeof *global_palette);
+          if (next_transparent_index < min) local_palette[next_transparent_index] = *transparent_color;
+          if (palcheck) {
+            if (local_palette_size > global_palette_size) {
+              memcpy(global_palette + global_palette_size, local_palette + global_palette_size,
+                     (local_palette_size - global_palette_size) * sizeof *global_palette);
+              global_palette_size = local_palette_size;
+            }
+            goto added;
+          }
+        }
+        // palettes are incompatible: break down the current global palette into per-frame copies
+        if (context -> image -> frames) {
+          result = ctxmalloc(context, (context -> image -> frames - 1) * sizeof *result);
+          uint64_t * palcopy = ctxcalloc(context, 256 * sizeof *palcopy);
+          // it doesn't matter that the pointer is reused, because it won't be freed explicitly
+          for (uint_fast32_t p = 0; p < context -> image -> frames - 1; p ++) result[p] = palcopy;
+          memcpy(palcopy, global_palette, global_palette_size * sizeof *palcopy);
+          if (transparent_index < global_palette_size) palcopy[transparent_index] = *transparent_color;
+        }
+      }
+      result = ctxrealloc(context, result, context -> image -> frames * sizeof *result);
+      result[context -> image -> frames - 1] = ctxcalloc(context, 256 * sizeof **result);
+      if (local_palette_size)
+        memcpy(result[context -> image -> frames - 1], local_palette, local_palette_size * sizeof **result);
+      else {
+        memcpy(result[context -> image -> frames - 1], global_palette, global_palette_size * sizeof **result);
+        if (next_transparent_index < global_palette_size)
+          result[context -> image -> frames - 1][next_transparent_index] = *transparent_color;
+      }
+      // either the frame palette has been added to the per-frame list or the global palette is still in use
+      added:
+      ctxfree(context, local_palette);
+      scan_offset ++;
+      if (scan_offset >= context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      skip_GIF_data_blocks(context, &scan_offset);
+      next_transparent_index = 256;
+      seen_extension = false;
+    } break;
+    case 0x3b:
+      if (!seen_extension) goto done;
+    default:
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  done:
+  if (!context -> image -> frames) throw(context, PLUM_ERR_NO_DATA);
+  if (!result) {
+    if (transparent_index < global_palette_size) global_palette[transparent_index] = *transparent_color;
+    context -> image -> max_palette_index = global_palette_size - 1;
+    context -> image -> palette = plum_malloc(context -> image, plum_color_buffer_size(global_palette_size, flags));
+    if (!context -> image -> palette) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+    plum_convert_colors(context -> image -> palette, global_palette, global_palette_size, flags, PLUM_COLOR_64);
+  }
+  ctxfree(context, global_palette);
+  return result;
+}
+
+void load_GIF_palette (struct context * context, uint64_t * restrict palette, size_t * restrict offset, unsigned size) {
+  if (3 * size > context -> size - *offset) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  while (size --) {
+    uint_fast64_t color = context -> data[(*offset) ++];
+    color |= (uint_fast64_t) context -> data[(*offset) ++] << 16;
+    color |= (uint_fast64_t) context -> data[(*offset) ++] << 32;
+    *(palette ++) = color * 0x101;
+  }
+}
+
+void * load_GIF_data_blocks (struct context * context, size_t * restrict offset, size_t * restrict loaded_size) {
+  if (*offset >= context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  size_t current_size = 0, p = *offset;
+  uint_fast8_t block;
+  while (block = context -> data[p ++]) {
+    p += block;
+    current_size += block;
+    if (p >= context -> size || p <= block) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  *loaded_size = current_size;
+  unsigned char * result = ctxmalloc(context, current_size);
+  for (size_t copied_size = 0; block = context -> data[(*offset) ++]; copied_size += block) {
+    memcpy(result + copied_size, context -> data + *offset, block);
+    *offset += block;
+  }
+  return result;
+}
+
+void skip_GIF_data_blocks (struct context * context, size_t * restrict offset) {
+  uint_fast8_t skip;
+  do {
+    if (*offset >= context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    skip = context -> data[(*offset) ++];
+    if (context -> size < skip || *offset > context -> size - skip) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    *offset += skip;
+  } while (skip);
+}
+
+void load_GIF_frame (struct context * context, size_t * restrict offset, unsigned flags, uint32_t frame, const uint64_t * restrict palette,
+                     uint64_t transparent_color, uint64_t * restrict duration, uint8_t * restrict disposal, struct plum_rectangle * restrict framearea) {
+  *duration = *disposal = 0;
+  int transparent_index = -1;
+  // frames have already been validated, so at this point, we can only have extensions (0x21 ID block block block...) or image descriptors
+  while (context -> data[(*offset) ++] == 0x21) {
+    unsigned char extkind = context -> data[(*offset) ++];
+    if (extkind != 0xf9 && extkind != 0xff) {
+      skip_GIF_data_blocks(context, offset);
+      continue;
+    }
+    size_t extsize;
+    unsigned char * extdata = load_GIF_data_blocks(context, offset, &extsize);
+    if (extkind == 0xff) {
+      if (extsize == 14 && bytematch(extdata, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, 0x01)) {
+        if (plum_find_metadata(context -> image, PLUM_METADATA_LOOP_COUNT)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        add_loop_count_metadata(context, read_le16_unaligned(extdata + 12));
+      }
+    } else {
+      *duration = (uint64_t) 10000000 * read_le16_unaligned(extdata + 1);
+      uint_fast8_t dispindex = (*extdata >> 2) & 7;
+      if (dispindex > 3) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      if (dispindex) *disposal = dispindex - 1;
+      if (*extdata & 1) transparent_index = extdata[3];
+    }
+    ctxfree(context, extdata);
+  }
+  if (!*duration) *duration = 1;
+  uint_fast32_t left = read_le16_unaligned(context -> data + *offset);
+  uint_fast32_t top = read_le16_unaligned(context -> data + *offset + 2);
+  uint_fast32_t width = read_le16_unaligned(context -> data + *offset + 4);
+  uint_fast32_t height = read_le16_unaligned(context -> data + *offset + 6);
+  if (left + width > context -> image -> width || top + height > context -> image -> height) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  *framearea = (struct plum_rectangle) {.left = left, .top = top, .width = width, .height = height};
+  uint_fast8_t frameflags = context -> data[*offset + 8];
+  *offset += 9;
+  uint8_t max_palette_index;
+  if (frameflags & 0x80) {
+    *offset += 6 << (frameflags & 7);
+    max_palette_index = (2 << (frameflags & 7)) - 1;
+  } else
+    max_palette_index = (2 << (context -> data[10] & 7)) - 1;
+  uint8_t codesize = context -> data[(*offset) ++];
+  if (codesize < 2 || codesize > 11) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  size_t length;
+  unsigned char * compressed = load_GIF_data_blocks(context, offset, &length);
+  unsigned char * buffer = ctxmalloc(context, (size_t) width * height);
+  decompress_GIF_data(context, buffer, compressed, width * height, length, codesize);
+  ctxfree(context, compressed);
+  if (frameflags & 0x40) {
+    // interlaced frame
+    unsigned char * temp = ctxmalloc(context, (size_t) width * height);
+    uint_fast32_t target = 0;
+    for (uint_fast32_t row = 0; row < height; row += 8) memcpy(temp + row * width, buffer + (target ++) * width, width);
+    for (uint_fast32_t row = 4; row < height; row += 8) memcpy(temp + row * width, buffer + (target ++) * width, width);
+    for (uint_fast32_t row = 2; row < height; row += 4) memcpy(temp + row * width, buffer + (target ++) * width, width);
+    for (uint_fast32_t row = 1; row < height; row += 2) memcpy(temp + row * width, buffer + (target ++) * width, width);
+    ctxfree(context, buffer);
+    buffer = temp;
+  }
+  for (size_t p = 0; p < width * height; p ++) if (buffer[p] > max_palette_index) throw(context, PLUM_ERR_INVALID_COLOR_INDEX);
+  if (width == context -> image -> width && height == context -> image -> height)
+    write_palette_framebuffer_to_image(context, buffer, palette, frame, flags, 0xff);
+  else if (context -> image -> palette) {
+    if (transparent_index < 0) throw(context, PLUM_ERR_INVALID_FILE_FORMAT); // if we got here somehow, it's irrecoverable
+    uint8_t * fullframe = ctxmalloc(context, context -> image -> width * context -> image -> height);
+    memset(fullframe, transparent_index, context -> image -> width * context -> image -> height);
+    for (uint_fast16_t row = top; row < top + height; row ++)
+      memcpy(fullframe + context -> image -> width * row + left, buffer + width * (row - top), width);
+    write_palette_framebuffer_to_image(context, fullframe, palette, frame, flags, 0xff);
+    ctxfree(context, fullframe);
+  } else {
+    uint64_t * fullframe = ctxmalloc(context, sizeof *fullframe * context -> image -> width * context -> image -> height);
+    uint64_t * current = fullframe;
+    for (uint_fast16_t row = 0; row < top; row ++)
+      for (uint_fast16_t col = 0; col < context -> image -> width; col ++) *(current ++) = transparent_color;
+    for (uint_fast16_t row = top; row < top + height; row ++) {
+      for (uint_fast16_t col = 0; col < left; col ++) *(current ++) = transparent_color;
+      for (uint_fast16_t col = left; col < left + width; col ++) *(current ++) = palette[buffer[(row - top) * width + col - left]];
+      for (uint_fast16_t col = left + width; col < context -> image -> width; col ++) *(current ++) = transparent_color;
+    }
+    for (uint_fast16_t row = top + height; row < context -> image -> height; row ++)
+      for (uint_fast16_t col = 0; col < context -> image -> width; col ++) *(current ++) = transparent_color;
+    write_framebuffer_to_image(context -> image, fullframe, frame, flags);
+    ctxfree(context, fullframe);
+  }
+  ctxfree(context, buffer);
+}
+
+void generate_GIF_data (struct context * context) {
+  if (context -> source -> width > 0xffffu || context -> source -> height > 0xffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  // technically, some GIFs could be 87a; however, at the time of writing, 89a is over three decades old and supported by everything relevant
+  byteoutput(context, 0x47, 0x49, 0x46, 0x38, 0x39, 0x61);
+  unsigned char * header = append_output_node(context, 7);
+  write_le16_unaligned(header, context -> source -> width);
+  write_le16_unaligned(header + 2, context -> source -> height);
+  uint_fast32_t depth = get_true_color_depth(context -> source);
+  uint8_t overall = depth;
+  if ((uint8_t) (depth >> 8) > overall) overall = depth >> 8;
+  if ((uint8_t) (depth >> 16) > overall) overall = depth >> 16;
+  if (overall > 8) overall = 8;
+  header[4] = (overall - 1) << 4;
+  header[5] = header[6] = 0;
+  if (context -> source -> palette)
+    generate_GIF_data_with_palette(context, header);
+  else
+    generate_GIF_data_from_raw(context, header);
+  byteoutput(context, 0x3b);
+}
+
+void generate_GIF_data_with_palette (struct context * context, unsigned char * header) {
+  uint_fast16_t colors = context -> source -> max_palette_index + 1;
+  uint32_t * palette = ctxcalloc(context, 256 * sizeof *palette);
+  plum_convert_colors(palette, context -> source -> palette, colors, PLUM_COLOR_32, context -> source -> color_format);
+  int transparent = -1;
+  uint8_t * mapping = NULL;
+  for (uint_fast16_t p = 0; p <= context -> source -> max_palette_index; p ++) {
+    if (palette[p] & 0x80000000u)
+      if (transparent < 0)
+        transparent = p;
+      else {
+        if (!mapping) {
+          mapping = ctxmalloc(context, colors * sizeof *mapping);
+          for (uint_fast16_t index = 0; index <= context -> source -> max_palette_index; index ++) mapping[index] = index;
+        }
+        mapping[p] = transparent;
+      }
+    palette[p] &= 0xffffffu;
+  }
+  int_fast32_t background = get_GIF_background_color(context);
+  if (background >= 0) {
+    uint_fast16_t index;
+    for (index = 0; index < colors; index ++) if (palette[index] == background) break;
+    if (index == colors && colors < 256) palette[colors ++] = background;
+    header[5] = index; // if index > 255, this truncates, but it doesn't matter because any value would be wrong in that case
+  }
+  uint_fast16_t colorbits;
+  for (colorbits = 0; colors > (2 << colorbits); colorbits ++);
+  header[4] |= 0x80 + colorbits;
+  uint_fast16_t colorcount = 2 << colorbits;
+  write_GIF_palette(context, palette, colorcount);
+  ctxfree(context, palette);
+  write_GIF_loop_info(context);
+  size_t framesize = (size_t) context -> source -> width * context -> source -> height;
+  unsigned char * framebuffer = ctxmalloc(context, framesize);
+  const struct plum_metadata * durations = plum_find_metadata(context -> source, PLUM_METADATA_FRAME_DURATION);
+  const struct plum_metadata * disposals = plum_find_metadata(context -> source, PLUM_METADATA_FRAME_DISPOSAL);
+  int64_t duration_remainder = 0;
+  struct plum_rectangle * boundaries = get_frame_boundaries(context, false);
+  for (uint_fast32_t frame = 0; frame < context -> source -> frames; frame ++) {
+    if (mapping)
+      for (size_t pixel = 0; pixel < framesize; pixel ++) framebuffer[pixel] = mapping[context -> source -> data8[frame * framesize + pixel]];
+    else
+      memcpy(framebuffer, context -> source -> data8 + frame * framesize, framesize);
+    uint_fast16_t left = 0, top = 0, width = context -> source -> width, height = context -> source -> height;
+    if (transparent >= 0) {
+      size_t index = 0;
+      if (boundaries) {
+        while (index < context -> source -> width * boundaries[frame].top) if (framebuffer[index ++] != transparent) goto findbounds;
+        for (uint_fast16_t row = 0; row < boundaries[frame].height; row ++) {
+          for (uint_fast16_t col = 0; col < boundaries[frame].left; col ++) if (framebuffer[index ++] != transparent) goto findbounds;
+          index += boundaries[frame].width;
+          for (uint_fast16_t col = boundaries[frame].left + boundaries[frame].width; col < context -> source -> width; col ++)
+            if (framebuffer[index ++] != transparent) goto findbounds;
+        }
+        while (index < framesize) if (framebuffer[index ++] != transparent) goto findbounds;
+        left = boundaries[frame].left;
+        top = boundaries[frame].top;
+        width = boundaries[frame].width;
+        height = boundaries[frame].height;
+        goto gotbounds;
+      }
+      findbounds:
+      for (index = 0; index < framesize; index ++) if (framebuffer[index] != transparent) break;
+      if (index == framesize)
+        width = height = 1;
+      else {
+        top = index / width;
+        height -= top;
+        for (index = 0; index < framesize; index ++) if (framebuffer[framesize - 1 - index] != transparent) break;
+        height -= index / width;
+        for (left = 0; left < width; left ++) for (index = top; index < top + height; index ++)
+          if (framebuffer[index * context -> source -> width + left] != transparent) goto leftdone;
+        leftdone:
+        width -= left;
+        uint_fast16_t col;
+        for (col = 0; col < width; col ++) for (index = top; index < top + height; index ++)
+          if (framebuffer[(index + 1) * context -> source -> width - 1 - col] != transparent) goto rightdone;
+        rightdone:
+        width -= col;
+      }
+      gotbounds:
+      if (left || width != context -> source -> width) {
+        unsigned char * target = framebuffer;
+        for (uint_fast16_t row = 0; row < height; row ++) for (uint_fast16_t col = 0; col < width; col ++)
+          *(target ++) = framebuffer[context -> source -> width * (row + top) + col + left];
+      } else if (top)
+        memmove(framebuffer, framebuffer + context -> source -> width * top, context -> source -> width * height);
+    }
+    write_GIF_frame(context, framebuffer, NULL, colorcount, transparent, frame, left, top, width, height, durations, disposals, &duration_remainder);
+  }
+  ctxfree(context, boundaries);
+  ctxfree(context, mapping);
+  ctxfree(context, framebuffer);
+}
+
+void generate_GIF_data_from_raw (struct context * context, unsigned char * header) {
+  int_fast32_t background = get_GIF_background_color(context);
+  if (background >= 0) {
+    header[4] |= 0x80;
+    write_GIF_palette(context, (const uint32_t []) {background, 0}, 2);
+  }
+  write_GIF_loop_info(context);
+  size_t framesize = (size_t) context -> source -> width * context -> source -> height;
+  uint32_t * colorbuffer = ctxmalloc(context, sizeof *colorbuffer * framesize);
+  unsigned char * framebuffer = ctxmalloc(context, framesize);
+  const struct plum_metadata * durations = plum_find_metadata(context -> source, PLUM_METADATA_FRAME_DURATION);
+  const struct plum_metadata * disposals = plum_find_metadata(context -> source, PLUM_METADATA_FRAME_DISPOSAL);
+  size_t offset = plum_color_buffer_size(framesize, context -> source -> color_format);
+  int64_t duration_remainder = 0;
+  struct plum_rectangle * boundaries = get_frame_boundaries(context, false);
+  for (uint_fast32_t frame = 0; frame < context -> source -> frames; frame ++) {
+    plum_convert_colors(colorbuffer, context -> source -> data8 + offset * frame, framesize, PLUM_COLOR_32, context -> source -> color_format);
+    generate_GIF_frame_data(context, colorbuffer, framebuffer, frame, durations, disposals, &duration_remainder, boundaries ? boundaries + frame : NULL);
+  }
+  ctxfree(context, boundaries);
+  ctxfree(context, framebuffer);
+  ctxfree(context, colorbuffer);
+}
+
+void generate_GIF_frame_data (struct context * context, uint32_t * restrict pixels, unsigned char * restrict framebuffer, uint32_t frame,
+                              const struct plum_metadata * durations, const struct plum_metadata * disposals, int64_t * restrict duration_remainder,
+                              const struct plum_rectangle * boundaries) {
+  size_t framesize = (size_t) context -> source -> height * context -> source -> width;
+  uint32_t transparent = 0;
+  for (size_t index = 0; index < framesize; index ++)
+    if (pixels[index] & 0x80000000u) {
+      if (!transparent) transparent = 0xff000000u | pixels[index];
+      pixels[index] = transparent;
+    } else
+      pixels[index] &= 0xffffffu;
+  uint_fast16_t left = 0, top = 0, width = context -> source -> width, height = context -> source -> height;
+  if (transparent) {
+    size_t index = 0;
+    if (boundaries) {
+      while (index < context -> source -> width * boundaries -> top) if (pixels[index ++] != transparent) goto findbounds;
+      for (uint_fast16_t row = 0; row < boundaries -> height; row ++) {
+        for (uint_fast16_t col = 0; col < boundaries -> left; col ++) if (pixels[index ++] != transparent) goto findbounds;
+        index += boundaries -> width;
+        for (uint_fast16_t col = boundaries -> left + boundaries -> width; col < context -> source -> width; col ++)
+          if (pixels[index ++] != transparent) goto findbounds;
+      }
+      while (index < framesize) if (pixels[index ++] != transparent) goto findbounds;
+      left = boundaries -> left;
+      top = boundaries -> top;
+      width = boundaries -> width;
+      height = boundaries -> height;
+      goto gotbounds;
+    }
+    findbounds:
+    for (index = 0; index < framesize; index ++) if (pixels[index] != transparent) break;
+    if (index == framesize)
+      width = height = 1;
+    else {
+      top = index / width;
+      height -= top;
+      for (index = 0; index < framesize; index ++) if (pixels[framesize - 1 - index] != transparent) break;
+      height -= index / width;
+      for (left = 0; left < width; left ++) for (index = top; index < top + height; index ++)
+        if (pixels[index * context -> source -> width + left] != transparent) goto leftdone;
+      leftdone:
+      width -= left;
+      uint_fast16_t col;
+      for (col = 0; col < width; col ++) for (index = top; index < top + height; index ++)
+        if (pixels[(index + 1) * context -> source -> width - 1 - col] != transparent) goto rightdone;
+      rightdone:
+      width -= col;
+    }
+    gotbounds:
+    if (left || width != context -> source -> width) {
+      uint32_t * target = pixels;
+      for (uint_fast16_t row = 0; row < height; row ++) for (uint_fast16_t col = 0; col < width; col ++)
+        *(target ++) = pixels[context -> source -> width * (row + top) + col + left];
+    } else if (top)
+      memmove(pixels, pixels + context -> source -> width * top, sizeof *pixels * context -> source -> width * height);
+  }
+  uint32_t * palette = ctxcalloc(context, 256 * sizeof *palette);
+  int colorcount = plum_convert_colors_to_indexes(framebuffer, pixels, palette, (size_t) width * height, PLUM_COLOR_32);
+  if (colorcount < 0) throw(context, -colorcount);
+  int transparent_index = -1;
+  if (transparent)
+    for (uint_fast16_t index = 0; index <= colorcount; index ++) if (palette[index] == transparent) {
+      transparent_index = index;
+      break;
+    }
+  write_GIF_frame(context, framebuffer, palette, colorcount + 1, transparent_index, frame, left, top, width, height, durations, disposals, duration_remainder);
+  ctxfree(context, palette);
+}
+
+int_fast32_t get_GIF_background_color (struct context * context) {
+  const struct plum_metadata * metadata = plum_find_metadata(context -> source, PLUM_METADATA_BACKGROUND);
+  if (!metadata) return -1;
+  uint32_t converted;
+  plum_convert_colors(&converted, metadata -> data, 1, PLUM_COLOR_32, context -> source -> color_format);
+  return converted & 0xffffffu;
+}
+
+void write_GIF_palette (struct context * context, const uint32_t * restrict palette, unsigned count) {
+  for (unsigned char * data = append_output_node(context, 3 * count); count; count --, palette ++)
+    data += byteappend(data, *palette, *palette >> 8, *palette >> 16);
+}
+
+void write_GIF_loop_info (struct context * context) {
+  const struct plum_metadata * metadata = plum_find_metadata(context -> source, PLUM_METADATA_LOOP_COUNT);
+  if (!metadata) return;
+  uint_fast32_t count = *(const uint32_t *) metadata -> data;
+  if (count > 0xffffu) count = 0; // too many loops, so just make it loop forever
+  if (count == 1) return;
+  byteoutput(context, 0x21, 0xff, 0x0b, 0x4e, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2e, 0x30, 0x03, 0x01, count, count >> 8, 0x00);
+}
+
+void write_GIF_frame (struct context * context, const unsigned char * restrict data, const uint32_t * restrict palette, unsigned colors, int transparent,
+                      uint32_t frame, unsigned left, unsigned top, unsigned width, unsigned height, const struct plum_metadata * durations,
+                      const struct plum_metadata * disposals, int64_t * restrict duration_remainder) {
+  uint64_t duration = 0;
+  uint8_t disposal = 0;
+  if (durations && durations -> size > sizeof(uint64_t) * frame) {
+    duration = frame[(const uint64_t *) durations -> data];
+    if (duration) {
+      if (duration == 1) duration = 0;
+      uint64_t true_duration = adjust_frame_duration(duration, duration_remainder);
+      duration = (true_duration / 5000000u + 1) >> 1;
+      if (duration > 0xffffu) duration = 0xffffu; // maxed out
+      update_frame_duration_remainder(true_duration, duration * 10000000u, duration_remainder);
+    }
+  }
+  if (disposals && disposals -> size > frame) {
+    disposal = frame[(const uint8_t *) disposals -> data];
+    if (disposal >= PLUM_DISPOSAL_REPLACE) disposal -= PLUM_DISPOSAL_REPLACE;
+  }
+  uint_fast8_t colorbits;
+  for (colorbits = 0; colors > (2 << colorbits); colorbits ++);
+  unsigned colorcount = 2 << colorbits;
+  byteoutput(context, 0x21, 0xf9, 0x04, (disposal + 1) * 4 + (transparent >= 0), duration, duration >> 8, (transparent >= 0) ? transparent : 0, 0x00,
+                      0x2c, left, left >> 8, top, top >> 8, width, width >> 8, height, height >> 8, colorbits | (palette ? 0x80 : 0));
+  if (palette) write_GIF_palette(context, palette, colorcount);
+  if (!colorbits) colorbits = 1;
+  byteoutput(context, ++ colorbits); // incremented because compression starts with one bit extra
+  size_t length;
+  unsigned char * output = compress_GIF_data(context, data, (size_t) width * height, &length, colorbits);
+  write_GIF_data_blocks(context, output, length);
+  ctxfree(context, output);
+}
+
+void write_GIF_data_blocks (struct context * context, const unsigned char * restrict data, size_t size) {
+  uint_fast8_t remainder = size % 0xff;
+  size /= 0xff;
+  unsigned char * output = append_output_node(context, size * 0x100 + (remainder ? remainder + 2 : 1));
+  while (size --) {
+    *(output ++) = 0xff;
+    memcpy(output, data, 0xff);
+    output += 0xff;
+    data += 0xff;
+  }
+  if (remainder) {
+    *(output ++) = remainder;
+    memcpy(output, data, remainder);
+    output += remainder;
+  }
+  *output = 0;
+}
+
+void generate_Huffman_tree (struct context * context, const size_t * restrict counts, unsigned char * restrict lengths, size_t entries, unsigned char max) {
+  struct pair * sorted = ctxmalloc(context, entries * sizeof *sorted);
+  size_t truecount = 0;
+  for (size_t p = 0; p < entries; p ++) if (counts[p]) sorted[truecount ++] = (struct pair) {.value = p, .index = ~(uint64_t) counts[p]};
+  memset(lengths, 0, entries);
+  if (truecount < 2) {
+    if (truecount) lengths[sorted -> value] = 1;
+    ctxfree(context, sorted);
+    return;
+  }
+  sort_pairs(sorted, truecount);
+  size_t * pendingnodes = ctxmalloc(context, truecount * sizeof *pendingnodes);
+  size_t * pendingcounts = ctxmalloc(context, truecount * sizeof *pendingcounts);
+  for (size_t p = 0; p < truecount; p ++) {
+    pendingnodes[p] = sorted[p].value;
+    pendingcounts[p] = counts[sorted[p].value];
+  }
+  size_t next = entries;
+  size_t * parents = ctxmalloc(context, (entries + truecount) * sizeof *parents);
+  for (uint64_t remaining = truecount - 1; remaining; remaining --) {
+    parents[pendingnodes[remaining]] = parents[pendingnodes[remaining - 1]] = next;
+    size_t sum = pendingcounts[remaining - 1] + pendingcounts[remaining];
+    size_t first = 0, last = remaining - 1;
+    while (first < last) {
+      size_t p = (first + last) >> 1;
+      if (sum >= pendingcounts[p])
+        last = p;
+      else if (last > first + 1)
+        first = p;
+      else
+        first = p + 1;
+    }
+    memmove(pendingnodes + first + 1, pendingnodes + first, sizeof *pendingnodes * (remaining - 1 - first));
+    memmove(pendingcounts + first + 1, pendingcounts + first, sizeof *pendingcounts * (remaining - 1 - first));
+    pendingnodes[first] = next ++;
+    pendingcounts[first] = sum;
+  }
+  ctxfree(context, pendingcounts);
+  ctxfree(context, pendingnodes);
+  size_t root = next - 1;
+  unsigned char maxlength = 0;
+  for (size_t p = 0; p < truecount; p ++) {
+    next = sorted[p].value;
+    unsigned char length = 0;
+    while (next != root) {
+      if (length < 0xff) length ++;
+      next = parents[next];
+    }
+    lengths[sorted[p].value] = length;
+    if (length > maxlength) maxlength = length;
+  }
+  ctxfree(context, parents);
+  if (maxlength > max) {
+    // the maximum length has been exceeded, so increase some other lengths to make everything fit
+    uint64_t remaining = (uint64_t) 1 << max;
+    for (size_t p = 0; p < truecount; p ++) {
+      next = sorted[p].value;
+      if (lengths[next] > max) {
+        lengths[next] = max;
+        remaining --;
+      } else {
+        while (((uint64_t) 1 << (max - lengths[next])) > remaining) lengths[next] ++;
+        while (remaining - ((uint64_t) 1 << (max - lengths[next])) < truecount - p - 1) lengths[next] ++;
+        remaining -= (uint64_t) 1 << (max - lengths[next]);
+      }
+    }
+    for (size_t p = 0; remaining; p ++) {
+      next = sorted[p].value;
+      while (lengths[next] > 1 && remaining >= ((uint64_t) 1 << (max - lengths[next]))) {
+        remaining -= (uint64_t) 1 << (max - lengths[next]);
+        lengths[next] --;
+      }
+    }
+  }
+  ctxfree(context, sorted);
+}
+
+void generate_Huffman_codes (unsigned short * restrict codes, size_t count, const unsigned char * restrict lengths, bool invert) {
+  // generates codes in ascending order: shorter codes before longer codes, and for the same length, smaller values before larger values
+  size_t remaining = 0;
+  for (size_t p = 0; p < count; p ++) if (lengths[p]) remaining ++;
+  uint_fast8_t length = 0;
+  uint_fast16_t code = 0;
+  for (size_t p = SIZE_MAX; remaining; p ++) {
+    if (p >= count) {
+      length ++;
+      code <<= 1;
+      p = 0;
+    }
+    if (lengths[p] != length) continue;
+    if (invert) {
+      // for some image formats, invert the code so it can be written out directly (first branch at the LSB)
+      uint_fast16_t temp = code ++;
+      codes[p] = 0;
+      for (uint_fast8_t bits = 0; bits < length; bits ++) {
+        codes[p] = (codes[p] << 1) | (temp & 1);
+        temp >>= 1;
+      }
+    } else
+      codes[p] = code ++;
+    remaining --;
+  }
+}
+
+void decompress_JPEG_arithmetic_scan (struct context * context, struct JPEG_decompressor_state * restrict state, const struct JPEG_decoder_tables * tables,
+                                      size_t rowunits, const struct JPEG_component_info * components, const size_t * restrict offsets, unsigned shift,
+                                      unsigned char first, unsigned char last, bool differential) {
+  for (size_t restart_interval = 0; restart_interval <= state -> restart_count; restart_interval ++) {
+    size_t units = (restart_interval == state -> restart_count) ? state -> last_size : state -> restart_size;
+    if (!units) break;
+    size_t offset = *(offsets ++);
+    size_t remaining = *(offsets ++);
+    size_t colcount = 0, rowcount = 0, skipunits = 0;
+    uint16_t accumulator = 0;
+    uint32_t current = 0;
+    unsigned char bits = 0;
+    initialize_JPEG_arithmetic_counters(context, &offset, &remaining, &current);
+    signed char indexesDC[4][49] = {0};
+    signed char indexesAC[4][245] = {0};
+    uint16_t prevDC[4] = {0};
+    uint16_t prevdiff[4] = {0};
+    while (units --) {
+      int16_t (* outputunit)[64];
+      for (const unsigned char * decodepos = state -> MCU; *decodepos != MCU_END_LIST; decodepos ++) switch (*decodepos) {
+        case MCU_ZERO_COORD:
+          outputunit = state -> current_block[decodepos[1]];
+          break;
+        case MCU_NEXT_ROW:
+          outputunit += state -> row_offset[decodepos[1]];
+          break;
+        default: {
+          bool prevzero = false; // was the previous coefficient zero?
+          for (uint_fast8_t p = first; p <= last; p ++) {
+            if (skipunits)
+              p[*outputunit] = 0;
+            else if (p) {
+              unsigned char conditioning = tables -> arithmetic[components[*decodepos].tableAC + 4];
+              signed char * index = indexesAC[components[*decodepos].tableAC] + 3 * (p - 1);
+              if (!prevzero && next_JPEG_arithmetic_bit(context, &offset, &remaining, index, &current, &accumulator, &bits)) {
+                p[*outputunit] = 0;
+                skipunits ++;
+              } else if (next_JPEG_arithmetic_bit(context, &offset, &remaining, index + 1, &current, &accumulator, &bits)) {
+                p[*outputunit] = next_JPEG_arithmetic_value(context, &offset, &remaining, &current, &accumulator, &bits, indexesAC[components[*decodepos].tableAC],
+                                                            1, p, conditioning);
+                prevzero = false;
+              } else {
+                p[*outputunit] = 0;
+                prevzero = true;
+              }
+            } else {
+              unsigned char conditioning = tables -> arithmetic[components[*decodepos].tableDC];
+              unsigned char category = classify_JPEG_arithmetic_value(prevdiff[*decodepos], conditioning);
+              if (next_JPEG_arithmetic_bit(context, &offset, &remaining, indexesDC[components[*decodepos].tableDC] + 4 * category, &current, &accumulator, &bits))
+                prevdiff[*decodepos] = next_JPEG_arithmetic_value(context, &offset, &remaining, &current, &accumulator, &bits,
+                                                                  indexesDC[components[*decodepos].tableDC], 0, category, conditioning);
+              else
+                prevdiff[*decodepos] = 0;
+              if (differential)
+                **outputunit = make_signed_16(prevdiff[*decodepos]);
+              else
+                prevDC[*decodepos] = **outputunit = make_signed_16(prevDC[*decodepos] + prevdiff[*decodepos]);
+            }
+            p[*outputunit] = make_signed_16((uint16_t) p[*outputunit] << shift);
+          }
+          outputunit ++;
+          if (skipunits) skipunits --;
+        }
+      }
+      if (++ colcount == rowunits) {
+        colcount = 0;
+        rowcount ++;
+        if (rowcount == state -> row_skip_index) skipunits += (rowunits - state -> column_skip_count) * state -> row_skip_count;
+      }
+      if (colcount == state -> column_skip_index) skipunits += state -> column_skip_count;
+      for (uint_fast8_t p = 0; p < 4; p ++) if (state -> current_block[p]) {
+        state -> current_block[p] += state -> unit_offset[p];
+        if (!colcount) state -> current_block[p] += state -> unit_row_offset[p];
+      }
+    }
+    if (remaining || skipunits) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+}
+
+void decompress_JPEG_arithmetic_bit_scan (struct context * context, struct JPEG_decompressor_state * restrict state, size_t rowunits,
+                                          const struct JPEG_component_info * components, const size_t * restrict offsets, unsigned shift, unsigned char first,
+                                          unsigned char last) {
+  // this function is very similar to decompress_JPEG_arithmetic_scan, but it only decodes the next bit for already-initialized data
+  if (last && !first) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  for (size_t restart_interval = 0; restart_interval <= state -> restart_count; restart_interval ++) {
+    size_t units = (restart_interval == state -> restart_count) ? state -> last_size : state -> restart_size;
+    if (!units) break;
+    size_t offset = *(offsets ++);
+    size_t remaining = *(offsets ++);
+    size_t colcount = 0, rowcount = 0, skipunits = 0;
+    uint16_t accumulator = 0;
+    uint32_t current = 0;
+    unsigned char bits = 0;
+    initialize_JPEG_arithmetic_counters(context, &offset, &remaining, &current);
+    signed char indexes[4][189] = {0}; // most likely very few will be actually used, but allocate for the worst case
+    while (units --) {
+      int16_t (* outputunit)[64];
+      for (const unsigned char * decodepos = state -> MCU; *decodepos != MCU_END_LIST; decodepos ++) switch (*decodepos) {
+        case MCU_ZERO_COORD:
+          outputunit = state -> current_block[decodepos[1]];
+          break;
+        case MCU_NEXT_ROW:
+          outputunit += state -> row_offset[decodepos[1]];
+          break;
+        default:
+          if (skipunits)
+            skipunits --;
+          else if (first) {
+            unsigned char lastnonzero; // last non-zero coefficient up to the previous scan (for the same component)
+            for (lastnonzero = 63; lastnonzero; lastnonzero --) if (lastnonzero[*outputunit]) break;
+            bool prevzero = false; // was the previous coefficient zero?
+            for (uint_fast8_t p = first; p <= last; p ++) {
+              signed char * index = indexes[components[*decodepos].tableAC] + 3 * (p - 1);
+              if (!prevzero && p > lastnonzero && next_JPEG_arithmetic_bit(context, &offset, &remaining, index, &current, &accumulator, &bits)) break;
+              if (p[*outputunit]) {
+                prevzero = false;
+                if (next_JPEG_arithmetic_bit(context, &offset, &remaining, index + 2, &current, &accumulator, &bits))
+                  if (p[*outputunit] < 0)
+                    p[*outputunit] -= 1 << shift;
+                  else
+                    p[*outputunit] += 1 << shift;
+              } else if (next_JPEG_arithmetic_bit(context, &offset, &remaining, index + 1, &current, &accumulator, &bits)) {
+                prevzero = false;
+                p[*outputunit] = next_JPEG_arithmetic_bit(context, &offset, &remaining, NULL, &current, &accumulator, &bits) ?
+                                 make_signed_16(0xffffu << shift) : (1 << shift);
+              } else
+                prevzero = true;
+            }
+          } else if (next_JPEG_arithmetic_bit(context, &offset, &remaining, NULL, &current, &accumulator, &bits))
+            **outputunit += 1 << shift;
+          outputunit ++;
+      }
+      if (++ colcount == rowunits) {
+        colcount = 0;
+        rowcount ++;
+        if (rowcount == state -> row_skip_index) skipunits += (rowunits - state -> column_skip_count) * state -> row_skip_count;
+      }
+      if (colcount == state -> column_skip_index) skipunits += state -> column_skip_count;
+      for (uint_fast8_t p = 0; p < 4; p ++) if (state -> current_block[p]) {
+        state -> current_block[p] += state -> unit_offset[p];
+        if (!colcount) state -> current_block[p] += state -> unit_row_offset[p];
+      }
+    }
+    if (remaining || skipunits) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+}
+
+void decompress_JPEG_arithmetic_lossless_scan (struct context * context, struct JPEG_decompressor_state * restrict state, const struct JPEG_decoder_tables * tables,
+                                               size_t rowunits, const struct JPEG_component_info * components, const size_t * restrict offsets,
+                                               unsigned char predictor, unsigned precision) {
+  bool scancomponents[4] = {0};
+  for (uint_fast8_t p = 0; state -> MCU[p] != MCU_END_LIST; p ++) if (state -> MCU[p] < 4) scancomponents[state -> MCU[p]] = true;
+  uint16_t * rowdifferences[4] = {0};
+  for (uint_fast8_t p = 0; p < 4; p ++) if (scancomponents[p])
+    rowdifferences[p] = ctxmalloc(context, sizeof **rowdifferences * rowunits * ((state -> component_count > 1) ? components[p].scaleH : 1));
+  for (size_t restart_interval = 0; restart_interval <= state -> restart_count; restart_interval ++) {
+    size_t units = (restart_interval == state -> restart_count) ? state -> last_size : state -> restart_size;
+    if (!units) break;
+    size_t offset = *(offsets ++);
+    size_t remaining = *(offsets ++);
+    size_t colcount = 0, rowcount = 0, skipunits = 0;
+    uint16_t accumulator = 0;
+    uint32_t current = 0;
+    unsigned char bits = 0;
+    initialize_JPEG_arithmetic_counters(context, &offset, &remaining, &current);
+    signed char indexes[4][158] = {0};
+    for (uint_fast8_t p = 0; p < 4; p ++) if (scancomponents[p])
+      for (uint_fast16_t x = 0; x < (rowunits * ((state -> component_count > 1) ? components[p].scaleH : 1)); x ++) rowdifferences[p][x] = 0;
+    uint16_t coldifferences[4][4] = {0};
+    while (units --) {
+      uint_fast16_t x, y;
+      uint16_t * outputpos;
+      for (const unsigned char * decodepos = state -> MCU; *decodepos != MCU_END_LIST; decodepos ++) switch (*decodepos) {
+        case MCU_ZERO_COORD:
+          outputpos = state -> current_value[decodepos[1]];
+          x = colcount * ((state -> component_count > 1) ? components[decodepos[1]].scaleH : 1);
+          y = 0;
+          break;
+        case MCU_NEXT_ROW:
+          outputpos += state -> row_offset[decodepos[1]];
+          x = colcount * ((state -> component_count > 1) ? components[decodepos[1]].scaleH : 1);
+          y ++;
+          break;
+        default:
+          if (skipunits) {
+            *(outputpos ++) = 0;
+            skipunits --;
+          } else {
+            unsigned char conditioning = tables -> arithmetic[components[*decodepos].tableDC];
+            size_t rowsize = rowunits * ((state -> component_count > 1) ? components[*decodepos].scaleH : 1);
+            uint16_t difference, predicted = predict_JPEG_lossless_sample(outputpos, rowsize, !x, !(y || rowcount), predictor, precision);
+            // the JPEG standard calculates this the other way around, but it makes no difference and doing it in this order enables an optimization
+            unsigned char reference = 5 * classify_JPEG_arithmetic_value(rowdifferences[*decodepos][x], conditioning) +
+                                      classify_JPEG_arithmetic_value(coldifferences[*decodepos][y], conditioning);
+            if (next_JPEG_arithmetic_bit(context, &offset, &remaining, indexes[components[*decodepos].tableDC] + 4 * reference, &current, &accumulator, &bits))
+              difference = next_JPEG_arithmetic_value(context, &offset, &remaining, &current, &accumulator, &bits, indexes[components[*decodepos].tableDC],
+                                                      2, reference, conditioning);
+            else
+              difference = 0;
+            rowdifferences[*decodepos][x] = coldifferences[*decodepos][y] = difference;
+            *(outputpos ++) = predicted + difference;
+          }
+          x ++;
+      }
+      if (++ colcount == rowunits) {
+        colcount = 0;
+        rowcount ++;
+        if (rowcount == state -> row_skip_index) skipunits += (rowunits - state -> column_skip_count) * state -> row_skip_count;
+        memset(coldifferences, 0, sizeof coldifferences);
+      }
+      if (colcount == state -> column_skip_index) skipunits += state -> column_skip_count;
+      for (uint_fast8_t p = 0; p < 4; p ++) if (state -> current_value[p]) {
+        state -> current_value[p] += state -> unit_offset[p];
+        if (!colcount) state -> current_value[p] += state -> unit_row_offset[p];
+      }
+    }
+    if (remaining || skipunits) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  for (uint_fast8_t p = 0; p < state -> component_count; p ++) ctxfree(context, rowdifferences[p]);
+}
+
+void initialize_JPEG_arithmetic_counters (struct context * context, size_t * restrict offset, size_t * restrict remaining, uint32_t * restrict current) {
+  for (uint_fast8_t loopcount = 0; loopcount < 2; loopcount ++) {
+    unsigned char data = 0;
+    if (*remaining) {
+      data = context -> data[(*offset) ++];
+      -- *remaining;
+    }
+    if (data == 0xff) while (*remaining) {
+      -- *remaining;
+      if (context -> data[(*offset) ++] != 0xff) break;
+    }
+    *current = (*current | data) << 8;
+  }
+}
+
+int16_t next_JPEG_arithmetic_value (struct context * context, size_t * restrict offset, size_t * restrict remaining, uint32_t * restrict current,
+                                    uint16_t * restrict accumulator, unsigned char * restrict bits, signed char * restrict indexes, unsigned mode,
+                                    unsigned reference, unsigned char conditioning) {
+  // mode = 0 for DC (reference = DC category), 1 for AC (reference = coefficient index), 2 for lossless (reference = 5 * top category + left category)
+  signed char * index = (mode == 1) ? NULL : (indexes + 4 * reference + 1);
+  bool negative = next_JPEG_arithmetic_bit(context, offset, remaining, index, current, accumulator, bits);
+  index = (mode == 1) ? indexes + 3 * reference - 1 : (index + 1 + negative);
+  uint_fast8_t size = next_JPEG_arithmetic_bit(context, offset, remaining, index, current, accumulator, bits);
+  uint16_t result = 0;
+  if (size) {
+    if (!mode)
+      index = indexes + 20;
+    else if (mode == 2)
+      index = indexes + 100 + 29 * (reference >= 15);
+    signed char * next_index = (mode == 1) ? indexes + 189 + 28 * (reference > conditioning) : (index + 1);
+    while (next_JPEG_arithmetic_bit(context, offset, remaining, index, current, accumulator, bits)) {
+      size ++;
+      if (size > 15) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      index = next_index ++;
+    }
+    result = 1;
+    index += 14;
+    while (-- size) result = (result << 1) + next_JPEG_arithmetic_bit(context, offset, remaining, index, current, accumulator, bits);
+  }
+  result ++;
+  if (negative) result = -result;
+  return make_signed_16(result);
+}
+
+unsigned char classify_JPEG_arithmetic_value (uint16_t value, unsigned char conditioning) {
+  // 0-4 for zero, small positive, small negative, large positive, large negative
+  uint16_t absolute = (value >= 0x8000u) ? -value : value;
+  uint16_t low = 0, high = (uint16_t) 1 << (conditioning >> 4);
+  conditioning &= 15;
+  if (conditioning) low = 1 << (conditioning - 1);
+  if (absolute <= low) return 0;
+  return ((value >= 0x8000u) ? 2 : 1) + 2 * (absolute > high);
+}
+
+bool next_JPEG_arithmetic_bit (struct context * context, size_t * restrict offset, size_t * restrict remaining, signed char * restrict index,
+                               uint32_t * restrict current, uint16_t * restrict accumulator, unsigned char * restrict bits) {
+  // negative state index: MPS = 1; null state: use 0 and don't update
+  // index 0 implies MPS = 0; there's no way to encode index = 0 and MPS = 1 (because that'd be state = -0), but that state cannot happen
+  static const struct JPEG_arithmetic_decoder_state states[] = {
+    /*   0 */ {0x5a1d,  true,   1,   1}, {0x2586, false,   2,  14}, {0x1114, false,   3,  16}, {0x080b, false,   4,  18}, {0x03d8, false,   5,  20},
+    /*   5 */ {0x01da, false,   6,  23}, {0x00e5, false,   7,  25}, {0x006f, false,   8,  28}, {0x0036, false,   9,  30}, {0x001a, false,  10,  33},
+    /*  10 */ {0x000d, false,  11,  35}, {0x0006, false,  12,   9}, {0x0003, false,  13,  10}, {0x0001, false,  13,  12}, {0x5a7f,  true,  15,  15},
+    /*  15 */ {0x3f25, false,  16,  36}, {0x2cf2, false,  17,  38}, {0x207c, false,  18,  39}, {0x17b9, false,  19,  40}, {0x1182, false,  20,  42},
+    /*  20 */ {0x0cef, false,  21,  43}, {0x09a1, false,  22,  45}, {0x072f, false,  23,  46}, {0x055c, false,  24,  48}, {0x0406, false,  25,  49},
+    /*  25 */ {0x0303, false,  26,  51}, {0x0240, false,  27,  52}, {0x01b1, false,  28,  54}, {0x0144, false,  29,  56}, {0x00f5, false,  30,  57},
+    /*  30 */ {0x00b7, false,  31,  59}, {0x008a, false,  32,  60}, {0x0068, false,  33,  62}, {0x004e, false,  34,  63}, {0x003b, false,  35,  32},
+    /*  35 */ {0x002c, false,   9,  33}, {0x5ae1,  true,  37,  37}, {0x484c, false,  38,  64}, {0x3a0d, false,  39,  65}, {0x2ef1, false,  40,  67},
+    /*  40 */ {0x261f, false,  41,  68}, {0x1f33, false,  42,  69}, {0x19a8, false,  43,  70}, {0x1518, false,  44,  72}, {0x1177, false,  45,  73},
+    /*  45 */ {0x0e74, false,  46,  74}, {0x0bfb, false,  47,  75}, {0x09f8, false,  48,  77}, {0x0861, false,  49,  78}, {0x0706, false,  50,  79},
+    /*  50 */ {0x05cd, false,  51,  48}, {0x04de, false,  52,  50}, {0x040f, false,  53,  50}, {0x0363, false,  54,  51}, {0x02d4, false,  55,  52},
+    /*  55 */ {0x025c, false,  56,  53}, {0x01f8, false,  57,  54}, {0x01a4, false,  58,  55}, {0x0160, false,  59,  56}, {0x0125, false,  60,  57},
+    /*  60 */ {0x00f6, false,  61,  58}, {0x00cb, false,  62,  59}, {0x00ab, false,  63,  61}, {0x008f, false,  32,  61}, {0x5b12,  true,  65,  65},
+    /*  65 */ {0x4d04, false,  66,  80}, {0x412c, false,  67,  81}, {0x37d8, false,  68,  82}, {0x2fe8, false,  69,  83}, {0x293c, false,  70,  84},
+    /*  70 */ {0x2379, false,  71,  86}, {0x1edf, false,  72,  87}, {0x1aa9, false,  73,  87}, {0x174e, false,  74,  72}, {0x1424, false,  75,  72},
+    /*  75 */ {0x119c, false,  76,  74}, {0x0f6b, false,  77,  74}, {0x0d51, false,  78,  75}, {0x0bb6, false,  79,  77}, {0x0a40, false,  48,  77},
+    /*  80 */ {0x5832,  true,  81,  80}, {0x4d1c, false,  82,  88}, {0x438e, false,  83,  89}, {0x3bdd, false,  84,  90}, {0x34ee, false,  85,  91},
+    /*  85 */ {0x2eae, false,  86,  92}, {0x299a, false,  87,  93}, {0x2516, false,  71,  86}, {0x5570,  true,  89,  88}, {0x4ca9, false,  90,  95},
+    /*  90 */ {0x44d9, false,  91,  96}, {0x3e22, false,  92,  97}, {0x3824, false,  93,  99}, {0x32b4, false,  94,  99}, {0x2e17, false,  86,  93},
+    /*  95 */ {0x56a8,  true,  96,  95}, {0x4f46, false,  97, 101}, {0x47e5, false,  98, 102}, {0x41cf, false,  99, 103}, {0x3c3d, false, 100, 104},
+    /* 100 */ {0x375e, false,  93,  99}, {0x5231, false, 102, 105}, {0x4c0f, false, 103, 106}, {0x4639, false, 104, 107}, {0x415e, false,  99, 103},
+    /* 105 */ {0x5627,  true, 106, 105}, {0x50e7, false, 107, 108}, {0x4b85, false, 103, 109}, {0x5597, false, 109, 110}, {0x504f, false, 107, 111},
+    /* 110 */ {0x5a10,  true, 111, 110}, {0x5522, false, 109, 112}, {0x59eb,  true, 111, 112}
+  };
+  const struct JPEG_arithmetic_decoder_state * state = states + (index ? absolute_value(*index) : 0);
+  bool decoded, predicted = index && *index < 0; // predict the MPS; decode a 1 if the prediction is false
+  *accumulator -= state -> probability;
+  if (*accumulator > (*current >> 8)) {
+    if (*accumulator >= 0x8000u) return predicted;
+    decoded = *accumulator < state -> probability;
+  } else {
+    decoded = *accumulator >= state -> probability;
+    *current -= (uint32_t) *accumulator << 8;
+    *accumulator = state -> probability;
+  }
+  if (index)
+    if (decoded)
+      *index = (predicted != state -> switch_MPS) ? -state -> next_LPS : state -> next_LPS;
+    else
+      *index = predicted ? -state -> next_MPS : state -> next_MPS;
+  // normalize the counters, consuming new data if needed
+  do {
+    if (!*bits) {
+      unsigned char data = 0;
+      if (*remaining) {
+        data = context -> data[(*offset) ++];
+        -- *remaining;
+      }
+      if (data == 0xff) while (*remaining) {
+        -- *remaining;
+        if (context -> data[(*offset) ++] != 0xff) break;
+      }
+      *current |= data;
+      *bits = 8;
+    }
+    *accumulator <<= 1;
+    *current = (*current << 1) & 0xffffffu;
+    -- *bits;
+  } while (*accumulator < 0x8000u);
+  return predicted != decoded;
+}
+
+uint32_t determine_JPEG_components (struct context * context, size_t offset) {
+  uint_fast16_t size = read_be16_unaligned(context -> data + offset);
+  if (size < 8) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint_fast8_t count = context -> data[offset + 7];
+  if (!count || count > 4) throw(context, PLUM_ERR_INVALID_FILE_FORMAT); // only recognize up to four components
+  if (size != 8 + 3 * count) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  unsigned char components[4] = {0};
+  for (uint_fast8_t p = 0; p < count; p ++) components[p] = context -> data[offset + 8 + 3 * p];
+  switch (count) {
+    // since there's at most four components, a simple swap-based sort is the best implementation
+    case 4:
+      if (components[3] < *components) swap(uint_fast8_t, *components, components[3]);
+      if (components[3] < components[1]) swap(uint_fast8_t, components[1], components[3]);
+      if (components[3] < components[2]) swap(uint_fast8_t, components[2], components[3]);
+    case 3:
+      if (components[2] < *components) swap(uint_fast8_t, *components, components[2]);
+      if (components[2] < components[1]) swap(uint_fast8_t, components[1], components[2]);
+    case 2:
+      if (components[1] < *components) swap(uint_fast8_t, *components, components[1]);
+  }
+  for (uint_fast8_t p = 1; p < count; p ++) if (components[p - 1] == components[p]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  return read_le32_unaligned(components);
+}
+
+unsigned get_JPEG_component_count (uint32_t components) {
+  if (components < 0x100)
+    return 1;
+  else if (components < 0x10000u)
+    return 2;
+  else if (components < 0x1000000u)
+    return 3;
+  else
+    return 4;
+}
+
+void (* get_JPEG_component_transfer_function (struct context * context, const struct JPEG_marker_layout * layout, uint32_t components))
+      (uint64_t * restrict, size_t, unsigned, const double **) {
+  /* The JPEG standard has a very large deficiency: it specifies how to encode an arbitrary set of components of an
+     image, but it doesn't specify what those components mean. Components have a single byte ID to identify them, but
+     beyond that, the standard just hopes that applications can somehow figure it all out.
+     Of course, this means that different extensions make different choices about what components an image can have
+     and what those components' IDs should be. Determining the components of an image largely becomes a guessing
+     process, typically based on what the IJG's libjpeg does (except that it's not even stable across versions...).
+     This function therefore attempts to guess what the image's components mean, and errors out if it can't. */
+  if (components < 0x100)
+    // if there's only one component, assume the image is just grayscale
+    return &JPEG_transfer_grayscale;
+  if (layout -> Adobe) {
+    if (read_be16_unaligned(context -> data + layout -> Adobe) < 14) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    // Adobe stores a color format ID and specifies four possibilities based on it
+    switch (context -> data[layout -> Adobe + 13]) {
+      case 0:
+        // RGB or CMYK, so check the component count and try to detect the order
+        if (components < 0x10000u)
+          throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        else if (components < 0x1000000u)
+          if (components == 0x524742u || components == 0x726762u) // 'R', 'G', 'B' (including lowercase)
+            return &JPEG_transfer_BGR;
+          else if (!((components + 0x102) % 0x10101u)) // any sequential IDs
+            return &JPEG_transfer_RGB;
+          else
+            throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        else
+          if (components == 0x594d4b43u || components == 0x796d6b63u) // 'C', 'M', 'Y', 'K' (including lowercase)
+            return &JPEG_transfer_CKMY;
+          else if (!((components + 0x10203u) % 0x1010101u)) // any sequential IDs
+            return &JPEG_transfer_CMYK;
+          else
+            throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      case 1:
+        // YCbCr: verify three components and detect the order
+        if (components < 0x10000u || components >= 0x1000000u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (components == 0x635943u) // 'Y', 'C', 'c'
+          return &JPEG_transfer_CbYCr;
+        else if (!((components + 0x102) % 0x10101u)) // any sequential IDs
+          return &JPEG_transfer_YCbCr;
+        else
+          throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      case 2:
+        // YCbCrK: verify four components and detect the order
+        if (components < 0x1000000u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (components == 0x63594b43u) // 'Y', 'C', 'c', 'K'
+          return &JPEG_transfer_CbKYCr;
+        else if (!((components + 0x10203u) % 0x1010101u)) // any sequential IDs
+          return &JPEG_transfer_YCbCrK;
+      default:
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+  }
+  if (layout -> JFIF) {
+    // JFIF mandates one of two possibilities: grayscale (handled already) or YCbCr with IDs of 1, 2, 3 (although some encoders also use 0, 1, 2)
+    if (components == 0x30201u || components == 0x20100u) return &JPEG_transfer_YCbCr;
+    throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  // below this line it's pure guesswork: there are no application headers hinting at components, so just guess from popular ID values
+  if ((*layout -> frametype & 3) == 3 && components >= 0x10000u && components < 0x1000000u && !((components + 0x102) % 0x10101u))
+    // lossless encoding, three sequential component IDs
+    return &JPEG_transfer_RGB;
+  switch (components) {
+    case 0x5941u: // 'Y', 'A'
+      return &JPEG_transfer_alpha_grayscale;
+    case 0x20100u: // 0, 1, 2: used by libjpeg sometimes
+    case 0x30201u: // 1, 2, 3: JFIF's standard IDs
+    case 0x232201u: // 1, 0x22, 0x23: used by some library for 'big gamut' colors
+      return &JPEG_transfer_YCbCr;
+    case 0x635943u: // 'Y', 'C', 'c'
+      return &JPEG_transfer_CbYCr;
+    case 0x524742u: // 'R', 'G', 'B'
+    case 0x726762u: // 'r', 'g', 'b'
+      return &JPEG_transfer_BGR;
+    case 0x4030201u: // 1, 2, 3, 4
+      return &JPEG_transfer_YCbCrK;
+    case 0x63594b43u: // 'Y', 'C', 'c', 'K'
+      return &JPEG_transfer_CbKYCr;
+    case 0x63594341u: // 'Y', 'C', 'c', 'A'
+      return &JPEG_transfer_ACbYCr;
+    case 0x52474241u: // 'R', 'G', 'B', 'A'
+    case 0x72676261u: // 'r', 'g', 'b', 'a'
+      return &JPEG_transfer_ABGR;
+    case 0x594d4b43u: // 'C', 'M', 'Y', 'K'
+    case 0x796d6b63u: // 'c', 'm', 'y', 'k'
+      return &JPEG_transfer_CKMY;
+    default:
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+}
+
+void append_JPEG_color_depth_metadata (struct context * context, void (* transfer) (uint64_t * restrict, size_t, unsigned, const double **), unsigned bitdepth) {
+  if (transfer == &JPEG_transfer_grayscale)
+    add_color_depth_metadata(context, 0, 0, 0, 0, bitdepth);
+  else if (transfer == &JPEG_transfer_alpha_grayscale)
+    add_color_depth_metadata(context, 0, 0, 0, bitdepth, bitdepth);
+  else if (transfer == &JPEG_transfer_ABGR || transfer == &JPEG_transfer_ACbYCr)
+    add_color_depth_metadata(context, bitdepth, bitdepth, bitdepth, bitdepth, 0);
+  else
+    add_color_depth_metadata(context, bitdepth, bitdepth, bitdepth, 0, 0);
+}
+
+void JPEG_transfer_RGB (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  double factor = 65535.0 / limit;
+  const double * red = *input;
+  const double * green = input[1];
+  const double * blue = input[2];
+  while (count --) *(output ++) = color_from_floats(*(red ++) * factor, *(green ++) * factor, *(blue ++) * factor, 0);
+}
+
+void JPEG_transfer_BGR (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  JPEG_transfer_RGB(output, count, limit, (const double * []) {input[2], input[1], *input});
+}
+
+void JPEG_transfer_ABGR (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  double factor = 65535.0 / limit;
+  const double * red = input[3];
+  const double * green = input[2];
+  const double * blue = input[1];
+  const double * alpha = *input;
+  while (count --) *(output ++) = color_from_floats(*(red ++) * factor, *(green ++) * factor, *(blue ++) * factor, (limit - *(alpha ++)) * factor);
+}
+
+void JPEG_transfer_grayscale (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  double factor = 65535.0 / limit;
+  const double * luma = *input;
+  while (count --) {
+    double scaled = *(luma ++) * factor;
+    *(output ++) = color_from_floats(scaled, scaled, scaled, 0);
+  }
+}
+
+void JPEG_transfer_alpha_grayscale (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  double factor = 65535.0 / limit;
+  const double * luma = input[1];
+  const double * alpha = *input;
+  while (count --) {
+    double scaled = *(luma ++) * factor;
+    *(output ++) = color_from_floats(scaled, scaled, scaled, (limit - *(alpha ++)) * factor);
+  }
+}
+
+// all constants are defined to have exactly 53 bits of precision (matching IEEE 754 doubles)
+#define RED_COEF      0x0.b374bc6a7ef9d8p+0
+#define BLUE_COEF     0x0.e2d0e560418938p+0
+#define GREEN_CR_COEF 0x0.5b68d15d0f6588p+0
+#define GREEN_CB_COEF 0x0.2c0ca8674cd62ep+0
+
+void JPEG_transfer_YCbCr (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  double factor = 65535.0 / limit;
+  const double * luma = *input;
+  const double * blue_chroma = input[1];
+  const double * red_chroma = input[2];
+  while (count --) {
+    double blue_offset = limit - *(blue_chroma ++) * 2;
+    double red_offset = limit - *(red_chroma ++) * 2;
+    double red = *luma - RED_COEF * red_offset, blue = *luma - BLUE_COEF * blue_offset;
+    double green = *luma + GREEN_CB_COEF * blue_offset + GREEN_CR_COEF * red_offset;
+    luma ++;
+    *(output ++) = color_from_floats(red * factor, green * factor, blue * factor, 0);
+  }
+}
+
+void JPEG_transfer_CbYCr (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  JPEG_transfer_YCbCr(output, count, limit, (const double * []) {input[1], *input, input[2]});
+}
+
+void JPEG_transfer_YCbCrK (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  // this function replicates the YCbCr transfer function, but then darkens each color by the K component
+  double factor = 65535.0 / ((uint32_t) limit * limit);
+  const double * luma = *input;
+  const double * blue_chroma = input[1];
+  const double * red_chroma = input[2];
+  const double * black = input[3];
+  while (count --) {
+    double blue_offset = limit - *(blue_chroma ++) * 2;
+    double red_offset = limit - *(red_chroma ++) * 2;
+    double red = *luma - RED_COEF * red_offset, blue = *luma - BLUE_COEF * blue_offset;
+    double green = *luma + GREEN_CB_COEF * blue_offset + GREEN_CR_COEF * red_offset;
+    luma ++;
+    double scale = *(black ++) * factor;
+    *(output ++) = color_from_floats(red * scale, green * scale, blue * scale, 0);
+  }
+}
+
+void JPEG_transfer_CbKYCr (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  JPEG_transfer_YCbCrK(output, count, limit, (const double * []) {input[2], *input, input[3], input[1]});
+}
+
+void JPEG_transfer_ACbYCr (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  // this function replicates the YCbCr transfer function and computes a separate alpha channel
+  double factor = 65535.0 / limit;
+  const double * luma = input[2];
+  const double * blue_chroma = input[1];
+  const double * red_chroma = input[3];
+  const double * alpha = *input;
+  while (count --) {
+    double blue_offset = limit - *(blue_chroma ++) * 2;
+    double red_offset = limit - *(red_chroma ++) * 2;
+    double red = *luma - RED_COEF * red_offset, blue = *luma - BLUE_COEF * blue_offset;
+    double green = *luma + GREEN_CB_COEF * blue_offset + GREEN_CR_COEF * red_offset;
+    luma ++;
+    *(output ++) = color_from_floats(red * factor, green * factor, blue * factor, (limit - *(alpha ++)) * factor);
+  }
+}
+
+#undef RED_COEF
+#undef BLUE_COEF
+#undef GREEN_CR_COEF
+#undef GREEN_CB_COEF
+
+void JPEG_transfer_CMYK (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  double factor = 65535.0 / ((uint32_t) limit * limit);
+  const double * cyan = *input;
+  const double * magenta = input[1];
+  const double * yellow = input[2];
+  const double * black = input[3];
+  while (count --) {
+    double scale = *(black ++) * factor;
+    *(output ++) = color_from_floats(*(cyan ++) * scale, *(magenta ++) * scale, *(yellow ++) * scale, 0);
+  }
+}
+
+void JPEG_transfer_CKMY (uint64_t * restrict output, size_t count, unsigned limit, const double ** input) {
+  JPEG_transfer_CMYK(output, count, limit, (const double * []) {*input, input[2], input[3], input[1]});
+}
+
+struct JPEG_encoded_value * generate_JPEG_luminance_data_stream (struct context * context, double (* restrict data)[64], size_t units,
+                                                                 const uint8_t quantization[restrict static 64], size_t * restrict count) {
+  *count = 0;
+  size_t allocated = 3 * units + 64;
+  struct JPEG_encoded_value * result = ctxmalloc(context, sizeof *result * allocated);
+  double predicted = 0.0;
+  for (size_t unit = 0; unit < units; unit ++) {
+    if (allocated - *count < 64) {
+      size_t newsize = allocated + 3 * (units - unit) + 64;
+      if (newsize < allocated) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+      result = ctxrealloc(context, result, sizeof *result * (allocated = newsize));
+    }
+    predicted = generate_JPEG_data_unit(result, count, data[unit], quantization, predicted);
+  }
+  return ctxrealloc(context, result, *count * sizeof *result);
+}
+
+struct JPEG_encoded_value * generate_JPEG_chrominance_data_stream (struct context * context, double (* restrict blue)[64], double (* restrict red)[64],
+                                                                   size_t units, const uint8_t quantization[restrict static 64], size_t * restrict count) {
+  *count = 0;
+  size_t allocated = 6 * units + 128;
+  struct JPEG_encoded_value * result = ctxmalloc(context, sizeof *result * allocated);
+  double predicted_blue = 0.0, predicted_red = 0.0;
+  for (size_t unit = 0; unit < units; unit ++) {
+    if (allocated - *count < 128) {
+      size_t newsize = allocated + 6 * (units - unit) + 128;
+      if (newsize < allocated) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+      result = ctxrealloc(context, result, sizeof *result * (allocated = newsize));
+    }
+    predicted_blue = generate_JPEG_data_unit(result, count, blue[unit], quantization, predicted_blue);
+    predicted_red = generate_JPEG_data_unit(result, count, red[unit], quantization, predicted_red);
+  }
+  return ctxrealloc(context, result, *count * sizeof *result);
+}
+
+double generate_JPEG_data_unit (struct JPEG_encoded_value * data, size_t * restrict count, const double unit[restrict static 64],
+                                const uint8_t quantization[restrict static 64], double predicted) {
+  int16_t output[64];
+  predicted = apply_JPEG_DCT(output, unit, quantization, predicted);
+  uint_fast8_t last = 0;
+  encode_JPEG_value(data + (*count) ++, *output, 0, 0);
+  for (uint_fast8_t p = 1; p < 63; p ++) if (output[p]) {
+    for (; (p - last) > 16; last += 16) data[(*count) ++] = (struct JPEG_encoded_value) {.code = 0xf0, .bits = 0, .type = 1};
+    encode_JPEG_value(data + (*count) ++, output[p], 1, (p - last - 1) << 4);
+    last = p;
+  }
+  if (last != 63) data[(*count) ++] = (struct JPEG_encoded_value) {.code = 0, .bits = 0, .type = 1};
+  return predicted;
+}
+
+void encode_JPEG_value (struct JPEG_encoded_value * data, int16_t value, unsigned type, unsigned char addend) {
+  unsigned bits = bit_width(absolute_value(value));
+  if (value < 0) value += 0x7fff; // make it positive and subtract 1 from the significant bits
+  value &= (1u << bits) - 1;
+  *data = (struct JPEG_encoded_value) {.code = addend + bits, .bits = bits, .type = type, .value = value};
+}
+
+size_t generate_JPEG_Huffman_table (struct context * context, const struct JPEG_encoded_value * data, size_t count, unsigned char * restrict output,
+                                    unsigned char table[restrict static 0x100], unsigned char index) {
+  // returns the number of bytes spent encoding the table in the JPEG data (in output)
+  size_t counts[0x101] = {[0x100] = 1}; // use 0x100 as a dummy value to absorb the highest (invalid) code
+  unsigned char lengths[0x101];
+  *output = index;
+  index >>= 4;
+  for (size_t p = 0; p < count; p ++) if (data[p].type == index) counts[data[p].code] ++;
+  generate_Huffman_tree(context, counts, lengths, 0x101, 16);
+  unsigned char codecounts[16] = {0};
+  uint_fast8_t maxcode, maxlength = 0;
+  for (uint_fast16_t p = 0; p < 0x100; p ++) if (lengths[p]) {
+    codecounts[lengths[p] - 1] ++;
+    if (lengths[p] > maxlength) {
+      maxlength = lengths[p];
+      maxcode = p;
+    }
+  }
+  if (lengths[0x100] < maxlength) {
+    codecounts[maxlength] --;
+    codecounts[lengths[0x100]] ++;
+    lengths[maxcode] = lengths[0x100];
+  }
+  memcpy(table, lengths, 0x100);
+  memcpy(output + 1, codecounts, 16);
+  size_t outsize = 17;
+  for (uint_fast8_t length = 1; length <= 16; length ++) for (uint_fast16_t p = 0; p < 0x100; p ++) if (lengths[p] == length) output[outsize ++] = p;
+  return outsize;
+}
+
+void encode_JPEG_scan (struct context * context, const struct JPEG_encoded_value * data, size_t count, const unsigned char table[restrict static 0x200]) {
+  unsigned short codes[0x200]; // no need to create a dummy entry for the highest (invalid) code here: it simply won't be generated
+  generate_Huffman_codes(codes, 0x100, table, false);
+  generate_Huffman_codes(codes + 0x100, 0x100, table + 0x100, false);
+  unsigned char * node = append_output_node(context, 0x4000);
+  size_t size = 0;
+  uint_fast32_t output = 0;
+  unsigned char bits = 0;
+  for (size_t p = 0; p < count; p ++) {
+    if (size > 0x3ff8) {
+      context -> output -> size = size;
+      node = append_output_node(context, 0x4000);
+      size = 0;
+    }
+    unsigned short index = data[p].type * 0x100 + data[p].code;
+    output = (output << table[index]) | codes[index];
+    bits += table[index];
+    while (bits >= 8) {
+      node[size ++] = output >> (bits -= 8);
+      if (node[size - 1] == 0xff) node[size ++] = 0;
+    }
+    if (data[p].bits) {
+      output = (output << data[p].bits) | data[p].value;
+      bits += data[p].bits;
+      while (bits >= 8) {
+        node[size ++] = output >> (bits -= 8);
+        if (node[size - 1] == 0xff) node[size ++] = 0;
+      }
+    }
+  }
+  if (bits) node[size ++] = output << (8 - bits);
+  context -> output -> size = size;
+}
+
+// Cx = 0.5 * cos(x * pi / 16), rounded so that it fits exactly in 53 bits (standard precision for IEEE doubles)
+// note that C4 = 0.5 / sqrt(2), so this value is also used for that purpose
+#define C1 0x0.7d8a5f3fdd72c0p+0
+#define C2 0x0.7641af3cca3518p+0
+#define C3 0x0.6a6d98a43a868cp+0
+#define C4 0x0.5a827999fcef34p+0
+#define C5 0x0.471cece6b9a320p+0
+#define C6 0x0.30fbc54d5d52c6p+0
+#define C7 0x0.18f8b83c69a60bp+0
+
+// half the square root of 2
+#define HR2 0x0.b504f333f9de68p+0
+
+double apply_JPEG_DCT (int16_t output[restrict static 64], const double input[restrict static 64], const uint8_t quantization[restrict static 64], double prevDC) {
+  // coefficient(dst, src) = cos((2 * src + 1) * dst * pi / 16) / 2; this absorbs a leading factor of 1/4 (square rooted)
+  static const double coefficients[8][8] = {
+    {0.5,  C1,  C2,  C3,  C4,  C5,  C6,  C7},
+    {0.5,  C3,  C6, -C7, -C4, -C1, -C2, -C5},
+    {0.5,  C5, -C6, -C1, -C4,  C7,  C2,  C3},
+    {0.5,  C7, -C2, -C5,  C4,  C3, -C6, -C1},
+    {0.5, -C7, -C2,  C5,  C4, -C3, -C6,  C1},
+    {0.5, -C5, -C6,  C1, -C4, -C7,  C2, -C3},
+    {0.5, -C3,  C6,  C7, -C4,  C1, -C2,  C5},
+    {0.5, -C1,  C2, -C3,  C4, -C5,  C6, -C7}
+  };
+  // factor(row, col) = (row ? 1 : 1 / sqrt(2)) * (col ? 1 : 1 / sqrt(2)); converted into zigzag order
+  static const double factors[] = {
+    0.5, HR2, HR2, HR2, 1.0, HR2, HR2, 1.0, 1.0, HR2, HR2, 1.0, 1.0, 1.0, HR2, HR2, 1.0, 1.0, 1.0, 1.0, HR2, HR2, 1.0, 1.0, 1.0, 1.0, 1.0, HR2, HR2, 1.0, 1.0, 1.0,
+    1.0, 1.0, 1.0, HR2, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
+  };
+  // zero-flushing threshold: for later coefficients, round some values slightly larger than 0.5 to 0 instead of +/- 1 for better compression
+  static const double zeroflush[] = {
+    0x0.80p+0, 0x0.80p+0, 0x0.80p+0, 0x0.80p+0, 0x0.81p+0, 0x0.80p+0, 0x0.84p+0, 0x0.85p+0, 0x0.85p+0, 0x0.84p+0,
+    0x0.88p+0, 0x0.89p+0, 0x0.8ap+0, 0x0.89p+0, 0x0.88p+0, 0x0.8cp+0, 0x0.8dp+0, 0x0.8ep+0, 0x0.8ep+0, 0x0.8dp+0,
+    0x0.8cp+0, 0x0.90p+0, 0x0.91p+0, 0x0.92p+0, 0x0.93p+0, 0x0.92p+0, 0x0.91p+0, 0x0.90p+0, 0x0.94p+0, 0x0.95p+0,
+    0x0.96p+0, 0x0.97p+0, 0x0.97p+0, 0x0.96p+0, 0x0.95p+0, 0x0.94p+0, 0x0.98p+0, 0x0.99p+0, 0x0.9ap+0, 0x0.9bp+0,
+    0x0.9ap+0, 0x0.99p+0, 0x0.98p+0, 0x0.9cp+0, 0x0.9dp+0, 0x0.9ep+0, 0x0.9ep+0, 0x0.9dp+0, 0x0.9cp+0, 0x0.a0p+0,
+    0x0.a1p+0, 0x0.a2p+0, 0x0.a1p+0, 0x0.a0p+0, 0x0.a4p+0, 0x0.a5p+0, 0x0.a5p+0, 0x0.a4p+0, 0x0.a8p+0, 0x0.a9p+0,
+    0x0.a8p+0, 0x0.acp+0, 0x0.acp+0, 0x0.b0p+0
+  };
+  for (uint_fast8_t index = 0; index < 64; index ++) {
+    uint_fast8_t p = 0;
+    double converted = 0.0;
+    for (uint_fast8_t row = 0; row < 8; row ++) for (uint_fast8_t col = 0; col < 8; col ++)
+      converted += input[p ++] * coefficients[col][JPEG_zigzag_columns[index]] * coefficients[row][JPEG_zigzag_rows[index]];
+    converted = converted * factors[index] / quantization[index];
+    if (index)
+      if (converted >= -zeroflush[index] && converted <= zeroflush[index])
+        output[index] = 0;
+      else if (converted > 1023.0)
+        output[index] = 1023;
+      else if (converted < -1023.0)
+        output[index] = -1023;
+      else if (converted < 0)
+        output[index] = converted - 0.5;
+      else
+        output[index] = converted + 0.5;
+    else {
+      converted -= prevDC;
+      if (converted > 2047.0)
+        *output = 2047;
+      else if (converted < -2047.0)
+        *output = -2047;
+      else if (converted < 0)
+        *output = converted - 0.5;
+      else
+        *output = converted + 0.5;
+    }
+  }
+  return prevDC + *output;
+}
+
+void apply_JPEG_inverse_DCT (double output[restrict static 64], const int16_t input[restrict static 64], const uint16_t quantization[restrict static 64]) {
+  // coefficient(dst, src) = 0.5 * (src ? cos((2 * dst + 1) * src * pi / 16) : 1 / sqrt(2)); this absorbs a leading factor of 1/4 (square rooted)
+  static const double coefficients[8][8] = {
+    {C4,  C1,  C2,  C3,  C4,  C5,  C6,  C7},
+    {C4,  C3,  C6, -C7, -C4, -C1, -C2, -C5},
+    {C4,  C5, -C6, -C1, -C4,  C7,  C2,  C3},
+    {C4,  C7, -C2, -C5,  C4,  C3, -C6, -C1},
+    {C4, -C7, -C2,  C5,  C4, -C3, -C6,  C1},
+    {C4, -C5, -C6,  C1, -C4, -C7,  C2, -C3},
+    {C4, -C3,  C6,  C7, -C4,  C1, -C2,  C5},
+    {C4, -C1,  C2, -C3,  C4, -C5,  C6, -C7}
+  };
+  double dequantized[64];
+  for (uint_fast8_t index = 0; index < 64; index ++) dequantized[index] = (double) input[index] * quantization[index];
+  uint_fast8_t p = 0;
+  for (uint_fast8_t row = 0; row < 8; row ++) for (uint_fast8_t col = 0; col < 8; col ++) {
+    output[p] = 0;
+    for (uint_fast8_t index = 0; index < 64; index ++)
+      output[p] += coefficients[col][JPEG_zigzag_columns[index]] * coefficients[row][JPEG_zigzag_rows[index]] * dequantized[index];
+    p ++;
+  }
+}
+
+#undef HR2
+#undef C7
+#undef C6
+#undef C5
+#undef C4
+#undef C3
+#undef C2
+#undef C1
+
+void initialize_JPEG_decompressor_state (struct context * context, struct JPEG_decompressor_state * restrict state, const struct JPEG_component_info * components,
+                                         const unsigned char * componentIDs, size_t * restrict unitsH, size_t unitsV, size_t width, size_t height,
+                                         unsigned char maxH, unsigned char maxV, const struct JPEG_decoder_tables * tables, const size_t * restrict offsets,
+                                         int16_t (* restrict * output)[64]) {
+  initialize_JPEG_decompressor_state_common(context, state, components, componentIDs, unitsH, unitsV, width, height, maxH, maxV, tables, offsets, 8);
+  for (uint_fast8_t p = 0; p < 4; p ++) state -> current_block[p] = NULL;
+  for (uint_fast8_t p = 0; p < state -> component_count; p ++) state -> current_block[componentIDs[p]] = output[componentIDs[p]];
+}
+
+void initialize_JPEG_decompressor_state_lossless (struct context * context, struct JPEG_decompressor_state * restrict state,
+                                                  const struct JPEG_component_info * components, const unsigned char * componentIDs, size_t * restrict unitsH,
+                                                  size_t unitsV, size_t width, size_t height, unsigned char maxH, unsigned char maxV,
+                                                  const struct JPEG_decoder_tables * tables, const size_t * restrict offsets, uint16_t * restrict * output) {
+  initialize_JPEG_decompressor_state_common(context, state, components, componentIDs, unitsH, unitsV, width, height, maxH, maxV, tables, offsets, 1);
+  for (uint_fast8_t p = 0; p < 4; p ++) state -> current_value[p] = NULL;
+  for (uint_fast8_t p = 0; p < state -> component_count; p ++) state -> current_value[componentIDs[p]] = output[componentIDs[p]];
+}
+
+void initialize_JPEG_decompressor_state_common (struct context * context, struct JPEG_decompressor_state * restrict state,
+                                                const struct JPEG_component_info * components, const unsigned char * componentIDs, size_t * restrict unitsH,
+                                                size_t unitsV, size_t width, size_t height, unsigned char maxH, unsigned char maxV,
+                                                const struct JPEG_decoder_tables * tables, const size_t * restrict offsets, unsigned char unit_dimensions) {
+  if (componentIDs[1] != 0xff) {
+    unsigned char * entry = state -> MCU;
+    uint_fast8_t component;
+    for (component = 0; component < 4 && componentIDs[component] != 0xff; component ++) {
+      uint_fast8_t p = componentIDs[component];
+      state -> unit_offset[p] = components[p].scaleH;
+      state -> row_offset[p] = *unitsH * state -> unit_offset[p];
+      state -> unit_row_offset[p] = (components[p].scaleV - 1) * state -> row_offset[p];
+      state -> row_offset[p] -= state -> unit_offset[p];
+      for (uint_fast8_t row = 0; row < components[p].scaleV; row ++) {
+        *(entry ++) = row ? MCU_NEXT_ROW : MCU_ZERO_COORD;
+        for (uint_fast8_t col = 0; col < components[p].scaleH; col ++) *(entry ++) = p;
+      }
+    }
+    *entry = MCU_END_LIST;
+    state -> component_count = component;
+    state -> row_skip_index = state -> row_skip_count = state -> column_skip_index = state -> column_skip_count = 0;
+  } else {
+    // if a scan contains a single component, it's considered a non-interleaved scan and the MCU is a single unit
+    state -> component_count = 1;
+    state -> unit_offset[*componentIDs] = 1;
+    state -> row_offset[*componentIDs] = state -> unit_row_offset[*componentIDs] = 0;
+    bytewrite(state -> MCU, MCU_ZERO_COORD, *componentIDs, MCU_END_LIST);
+    *unitsH *= components[*componentIDs].scaleH;
+    unitsV *= components[*componentIDs].scaleV;
+    state -> column_skip_index = 1 + (width * components[*componentIDs].scaleH - 1) / (unit_dimensions * maxH);
+    state -> column_skip_count = *unitsH - state -> column_skip_index;
+    state -> row_skip_index = 1 + (height * components[*componentIDs].scaleV - 1) / (unit_dimensions * maxV);
+    state -> row_skip_count = unitsV - state -> row_skip_index;
+  }
+  state -> last_size = *unitsH * unitsV;
+  if (state -> restart_size = tables -> restart) {
+    state -> restart_count = state -> last_size / state -> restart_size;
+    state -> last_size %= state -> restart_size;
+  } else
+    state -> restart_count = 0;
+  size_t true_restart_count = state -> restart_count + !!state -> last_size; // including the final restart interval
+  for (size_t index = 0; index < true_restart_count; index ++) if (!offsets[2 * index]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (offsets[2 * true_restart_count]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+}
+
+uint16_t predict_JPEG_lossless_sample (const uint16_t * next, ptrdiff_t rowsize, bool leftmost, bool topmost, unsigned predictor, unsigned precision) {
+  if (!predictor) return 0;
+  if (topmost && leftmost) return 1u << (precision - 1);
+  if (topmost) return next[-1];
+  if (leftmost) return next[-rowsize];
+  uint_fast32_t left = next[-1], top = next[-rowsize], corner = next[-1 - rowsize];
+  return predictor[(const uint16_t []) {0, left, top, corner, left + top - corner, left + ((top - corner) >> 1), top + ((left - corner) >> 1), (left + top) >> 1}];
+}
+
+unsigned load_hierarchical_JPEG (struct context * context, const struct JPEG_marker_layout * layout, uint32_t components, double ** output) {
+  unsigned component_count = get_JPEG_component_count(components);
+  unsigned char componentIDs[4];
+  write_le32_unaligned(componentIDs, components);
+  struct JPEG_decoder_tables tables;
+  initialize_JPEG_decoder_tables(context, &tables, layout);
+  unsigned precision = context -> data[layout -> hierarchical + 2];
+  if (precision < 2 || precision > 16) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  size_t metadata_index = 0;
+  uint16_t component_size[8] = {0}; // four widths followed by four heights
+  for (size_t frame = 0; layout -> frames[frame]; frame ++) {
+    if (context -> data[layout -> frames[frame] + 2] != precision) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    uint32_t framecomponents = determine_JPEG_components(context, layout -> frames[frame]);
+    unsigned char frameIDs[4]; // IDs into the componentIDs array
+    unsigned char framecount = 0;
+    double * frameoutput[4] = {0};
+    do {
+      uint_fast8_t p;
+      for (p = 0; p < component_count; p ++) if (((framecomponents >> (8 * framecount)) & 0xff) == componentIDs[p]) break;
+      if (p == component_count) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      frameoutput[framecount] = output[p];
+      frameIDs[framecount ++] = p;
+    } while (framecount < 4 && (framecomponents >> (8 * framecount)));
+    unsigned char expand = process_JPEG_metadata_until_offset(context, layout, &tables, &metadata_index, layout -> frames[frame]);
+    uint16_t framewidth = read_be16_unaligned(context -> data + layout -> frames[frame] + 5);
+    uint16_t frameheight = read_be16_unaligned(context -> data + layout -> frames[frame] + 3);
+    if (!(framewidth && frameheight) || framewidth > context -> image -> width || frameheight > context -> image -> height)
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (layout -> frametype[frame] & 4) {
+      for (uint_fast8_t p = 0; p < framecount; p ++) {
+        if (!component_size[frameIDs[p]] || framewidth < component_size[frameIDs[p]] || frameheight < component_size[frameIDs[p] + 4])
+          throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        // round all components to integers, since hierarchical progressions expect to compute differences against integers
+        size_t limit = (size_t) component_size[frameIDs[p]] * component_size[frameIDs[p] + 4];
+        double * data = output[frameIDs[p]];
+        for (size_t index = 0; index < limit; index ++) data[index] = (uint16_t) (long) (data[index] + 65536.5); // avoid UB and round negative values correctly
+      }
+      if (expand) {
+        double * buffer = ctxmalloc(context, sizeof *buffer * framewidth * frameheight);
+        if (expand & 0x10) for (uint_fast8_t p = 0; p < framecount; p ++) {
+          expand_JPEG_component_horizontally(context, output[frameIDs[p]], component_size[frameIDs[p]], component_size[frameIDs[p] + 4], framewidth, buffer);
+          component_size[frameIDs[p]] = framewidth;
+        }
+        if (expand & 1) for (uint_fast8_t p = 0; p < framecount; p ++) {
+          expand_JPEG_component_vertically(context, output[frameIDs[p]], component_size[frameIDs[p]], component_size[frameIDs[p] + 4], frameheight, buffer);
+          component_size[frameIDs[p] + 4] = frameheight;
+        }
+        ctxfree(context, buffer);
+      }
+      for (uint_fast8_t p = 0; p < framecount; p ++) if (component_size[frameIDs[p]] != framewidth || component_size[frameIDs[p] + 4] != frameheight)
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    } else {
+      if (expand) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      for (uint_fast8_t p = 0; p < framecount; p ++) {
+        if (component_size[frameIDs[p]]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        component_size[frameIDs[p]] = framewidth;
+        component_size[frameIDs[p] + 4] = frameheight;
+      }
+    }
+    if ((layout -> frametype[frame] & 3) == 3)
+      load_JPEG_lossless_frame(context, layout, framecomponents, frame, &tables, &metadata_index, frameoutput, precision, framewidth, frameheight);
+    else
+      load_JPEG_DCT_frame(context, layout, framecomponents, frame, &tables, &metadata_index, frameoutput, precision, framewidth, frameheight);
+  }
+  double normalization_offset;
+  if (precision < 15)
+    normalization_offset = 0.5;
+  else if (precision == 15)
+    normalization_offset = 0.25;
+  else
+    normalization_offset = 0.0;
+  for (uint_fast8_t p = 0; p < component_count; p ++) {
+    if (component_size[p] != context -> image -> width || component_size[p + 4] != context -> image -> height) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    normalize_JPEG_component(output[p], (size_t) context -> image -> width * context -> image -> height, normalization_offset);
+  }
+  return precision;
+}
+
+void expand_JPEG_component_horizontally (struct context * context, double * restrict component, size_t width, size_t height, size_t target,
+                                         double * restrict buffer) {
+  if ((target >> 1) > width) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  size_t index = 0;
+  for (size_t row = 0; row < height; row ++) for (size_t col = 0; col < target; col ++)
+    if (col & 1)
+      if (((col + 1) >> 1) == width)
+        buffer[index ++] = component[(row + 1) * width - 1];
+      else
+        buffer[index ++] = (uint32_t) ((long) component[row * width + (col >> 1)] + (long) component[row * width + ((col + 1) >> 1)]) >> 1;
+    else
+      buffer[index ++] = component[row * width + (col >> 1)];
+  memcpy(component, buffer, index * sizeof *component);
+}
+
+void expand_JPEG_component_vertically (struct context * context, double * restrict component, size_t width, size_t height, size_t target,
+                                       double * restrict buffer) {
+  if ((target >> 1) > height) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  size_t index = 0;
+  for (size_t row = 0; row < target; row ++)
+    if (row & 1)
+      if (((row + 1) >> 1) == height) {
+        memcpy(buffer + index, component + (height - 1) * width, sizeof *component * width);
+        index += width;
+      } else
+        for (size_t col = 0; col < width; col ++)
+          buffer[index ++] = (uint32_t) ((long) component[(row >> 1) * width + col] + (long) component[((row + 1) >> 1) * width + col]) >> 1;
+    else {
+      memcpy(buffer + index, component + (row >> 1) * width, sizeof *component * width);
+      index += width;
+    }
+  memcpy(component, buffer, index * sizeof *component);
+}
+
+void normalize_JPEG_component (double * restrict component, size_t count, double offset) {
+  while (count --) {
+    double high = *component / 65536.0 + offset;
+    // this merely calculates adjustment = -floor(high); not using floor() directly to avoid linking in the math library just for a single function
+    int64_t adjustment = 0;
+    if (high < 0) {
+      adjustment = 1 + (int64_t) -high;
+      high += adjustment;
+    }
+    adjustment -= (int64_t) high;
+    *(component ++) += adjustment * 65536.0;
+  }
+}
+
+void decompress_JPEG_Huffman_scan (struct context * context, struct JPEG_decompressor_state * restrict state, const struct JPEG_decoder_tables * tables,
+                                   size_t rowunits, const struct JPEG_component_info * components, const size_t * restrict offsets, unsigned shift,
+                                   unsigned char first, unsigned char last, bool differential) {
+  for (size_t restart_interval = 0; restart_interval <= state -> restart_count; restart_interval ++) {
+    size_t units = (restart_interval == state -> restart_count) ? state -> last_size : state -> restart_size;
+    if (!units) break;
+    size_t colcount = 0, rowcount = 0, skipcount = 0, skipunits = 0;
+    const unsigned char * data = context -> data + *(offsets ++);
+    size_t count = *(offsets ++);
+    uint16_t prevDC[4] = {0};
+    int16_t nextvalue = 0;
+    uint32_t dataword = 0;
+    uint8_t bits = 0;
+    while (units --) {
+      int16_t (* outputunit)[64];
+      for (const unsigned char * decodepos = state -> MCU; *decodepos != MCU_END_LIST; decodepos ++) switch (*decodepos) {
+        case MCU_ZERO_COORD:
+          outputunit = state -> current_block[decodepos[1]];
+          break;
+        case MCU_NEXT_ROW:
+          outputunit += state -> row_offset[decodepos[1]];
+          break;
+        default:
+          for (uint_fast8_t p = first; p <= last; p ++) {
+            if (!(skipcount || nextvalue || skipunits)) {
+              unsigned char decompressed;
+              if (p) {
+                decompressed = next_JPEG_Huffman_value(context, &data, &count, &dataword, &bits, tables -> Huffman[components[*decodepos].tableAC + 4]);
+                if (decompressed & 15)
+                  skipcount = decompressed >> 4;
+                else if (decompressed == 0xf0)
+                  skipcount = 16;
+                else
+                  skipunits = (1u << (decompressed >> 4)) + shift_in_right_JPEG(context, decompressed >> 4, &dataword, &bits, &data, &count);
+                decompressed &= 15;
+              } else {
+                decompressed = next_JPEG_Huffman_value(context, &data, &count, &dataword, &bits, tables -> Huffman[components[*decodepos].tableDC]);
+                if (decompressed > 15) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+              }
+              if (decompressed) {
+                uint_fast16_t extrabits = shift_in_right_JPEG(context, decompressed, &dataword, &bits, &data, &count);
+                if (!(extrabits >> (decompressed - 1))) nextvalue = make_signed_16(1u - (1u << decompressed));
+                nextvalue = make_signed_16(nextvalue + extrabits);
+              }
+            }
+            if (skipcount || skipunits) {
+              p[*outputunit] = 0;
+              if (skipcount) skipcount --;
+            } else {
+              p[*outputunit] = nextvalue * (1 << shift);
+              nextvalue = 0;
+            }
+            if (!(p || differential)) prevDC[*decodepos] = **outputunit = make_signed_16(prevDC[*decodepos] + (uint16_t) **outputunit);
+          }
+          outputunit ++;
+          if (skipunits) skipunits --;
+      }
+      if (++ colcount == rowunits) {
+        colcount = 0;
+        rowcount ++;
+        if (rowcount == state -> row_skip_index) skipunits += (rowunits - state -> column_skip_count) * state -> row_skip_count;
+      }
+      if (colcount == state -> column_skip_index) skipunits += state -> column_skip_count;
+      for (uint_fast8_t p = 0; p < 4; p ++) if (state -> current_block[p]) {
+        state -> current_block[p] += state -> unit_offset[p];
+        if (!colcount) state -> current_block[p] += state -> unit_row_offset[p];
+      }
+    }
+    if (count || skipcount || skipunits || nextvalue) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+}
+
+void decompress_JPEG_Huffman_bit_scan (struct context * context, struct JPEG_decompressor_state * restrict state, const struct JPEG_decoder_tables * tables,
+                                       size_t rowunits, const struct JPEG_component_info * components, const size_t * restrict offsets, unsigned shift,
+                                       unsigned char first, unsigned char last) {
+  // this function is essentially the same as decompress_JPEG_Huffman_scan, but it uses already-initialized component data, and it decodes one bit at a time
+  if (last && !first) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  for (size_t restart_interval = 0; restart_interval <= state -> restart_count; restart_interval ++) {
+    size_t units = (restart_interval == state -> restart_count) ? state -> last_size : state -> restart_size;
+    if (!units) break;
+    size_t colcount = 0, rowcount = 0, skipcount = 0, skipunits = 0;
+    const unsigned char * data = context -> data + *(offsets ++);
+    size_t count = *(offsets ++);
+    int16_t nextvalue = 0;
+    uint32_t dataword = 0;
+    uint8_t bits = 0;
+    while (units --) {
+      int16_t (* outputunit)[64];
+      for (const unsigned char * decodepos = state -> MCU; *decodepos != MCU_END_LIST; decodepos ++) switch (*decodepos) {
+        case MCU_ZERO_COORD:
+          outputunit = state -> current_block[decodepos[1]];
+          break;
+        case MCU_NEXT_ROW:
+          outputunit += state -> row_offset[decodepos[1]];
+          break;
+        default:
+          if (first) {
+            for (uint_fast8_t p = first; p <= last; p ++) {
+              if (!(skipcount || nextvalue || skipunits)) {
+                unsigned char decompressed = next_JPEG_Huffman_value(context, &data, &count, &dataword, &bits,
+                                                                     tables -> Huffman[components[*decodepos].tableAC + 4]);
+                if (decompressed & 15)
+                  skipcount = decompressed >> 4;
+                else if (decompressed == 0xf0)
+                  skipcount = 16;
+                else
+                  skipunits = (1u << (decompressed >> 4)) + shift_in_right_JPEG(context, decompressed >> 4, &dataword, &bits, &data, &count);
+                decompressed &= 15;
+                if (decompressed) {
+                  uint_fast16_t extrabits = shift_in_right_JPEG(context, decompressed, &dataword, &bits, &data, &count);
+                  if (!(extrabits >> (decompressed - 1))) nextvalue = make_signed_16(1u - (1u << decompressed));
+                  nextvalue = make_signed_16(nextvalue + extrabits);
+                }
+              }
+              if (p[*outputunit]) {
+                if (shift_in_right_JPEG(context, 1, &dataword, &bits, &data, &count))
+                  if (p[*outputunit] < 0)
+                    p[*outputunit] -= 1 << shift;
+                  else
+                    p[*outputunit] += 1 << shift;
+              } else if (skipcount || skipunits) {
+                if (skipcount) skipcount --;
+              } else {
+                p[*outputunit] = nextvalue * (1 << shift);
+                nextvalue = 0;
+              }
+            }
+          } else if (!skipunits)
+            **outputunit += shift_in_right_JPEG(context, 1, &dataword, &bits, &data, &count) << shift;
+          outputunit ++;
+          if (skipunits) skipunits --;
+      }
+      if (++ colcount == rowunits) {
+        colcount = 0;
+        rowcount ++;
+        if (rowcount == state -> row_skip_index) skipunits += (rowunits - state -> column_skip_count) * state -> row_skip_count;
+      }
+      if (colcount == state -> column_skip_index) skipunits += state -> column_skip_count;
+      for (uint_fast8_t p = 0; p < 4; p ++) if (state -> current_block[p]) {
+        state -> current_block[p] += state -> unit_offset[p];
+        if (!colcount) state -> current_block[p] += state -> unit_row_offset[p];
+      }
+    }
+    if (count || skipcount || skipunits || nextvalue) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+}
+
+void decompress_JPEG_Huffman_lossless_scan (struct context * context, struct JPEG_decompressor_state * restrict state, const struct JPEG_decoder_tables * tables,
+                                            size_t rowunits, const struct JPEG_component_info * components, const size_t * restrict offsets,
+                                            unsigned char predictor, unsigned precision) {
+  for (size_t restart_interval = 0; restart_interval <= state -> restart_count; restart_interval ++) {
+    size_t units = (restart_interval == state -> restart_count) ? state -> last_size : state -> restart_size;
+    if (!units) break;
+    const unsigned char * data = context -> data + *(offsets ++);
+    size_t count = *(offsets ++);
+    size_t colcount = 0, rowcount = 0, skipunits = 0;
+    uint32_t dataword = 0;
+    uint8_t bits = 0;
+    while (units --) {
+      uint16_t * outputpos;
+      bool leftmost, topmost;
+      for (const unsigned char * decodepos = state -> MCU; *decodepos != MCU_END_LIST; decodepos ++) switch (*decodepos) {
+        case MCU_ZERO_COORD:
+          outputpos = state -> current_value[decodepos[1]];
+          leftmost = topmost = true;
+          break;
+        case MCU_NEXT_ROW:
+          outputpos += state -> row_offset[decodepos[1]];
+          leftmost = true;
+          topmost = false;
+          break;
+        default:
+          if (skipunits) {
+            *(outputpos ++) = 0;
+            skipunits --;
+          } else {
+            size_t rowsize = rowunits * ((state -> component_count > 1) ? components[*decodepos].scaleH : 1);
+            uint16_t difference, predicted = predict_JPEG_lossless_sample(outputpos, rowsize, leftmost && !colcount, topmost && !rowcount, predictor, precision);
+            unsigned char diffsize = next_JPEG_Huffman_value(context, &data, &count, &dataword, &bits, tables -> Huffman[components[*decodepos].tableDC]);
+            if (diffsize > 16) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+            switch (diffsize) {
+              case 0:
+                difference = 0;
+                break;
+              case 16:
+                difference = 0x8000u;
+                break;
+              default:
+                difference = shift_in_right_JPEG(context, diffsize, &dataword, &bits, &data, &count);
+                if (!(difference >> (diffsize - 1))) difference -= (1u << diffsize) - 1;
+            }
+            *(outputpos ++) = predicted + difference;
+          }
+          leftmost = false;
+      }
+      if (++ colcount == rowunits) {
+        colcount = 0;
+        rowcount ++;
+        if (rowcount == state -> row_skip_index) skipunits += (rowunits - state -> column_skip_count) * state -> row_skip_count;
+      }
+      if (colcount == state -> column_skip_index) skipunits += state -> column_skip_count;
+      for (uint_fast8_t p = 0; p < 4; p ++) if (state -> current_value[p]) {
+        state -> current_value[p] += state -> unit_offset[p];
+        if (!colcount) state -> current_value[p] += state -> unit_row_offset[p];
+      }
+    }
+    if (count || skipunits) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+}
+
+unsigned char next_JPEG_Huffman_value (struct context * context, const unsigned char ** data, size_t * restrict count, uint32_t * restrict dataword,
+                                       uint8_t * restrict bits, const short * restrict tree) {
+  for (uint_fast16_t index = 0; index != 1; index = -tree[index]) {
+    index += shift_in_right_JPEG(context, 1, dataword, bits, data, count);
+    if (tree[index] >= 0) return tree[index];
+  }
+  throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+}
+
+void load_JPEG_data (struct context * context, unsigned flags, size_t limit) {
+  struct JPEG_marker_layout * layout = load_JPEG_marker_layout(context); // will be leaked (to be collected by context release)
+  uint32_t components = determine_JPEG_components(context, layout -> hierarchical ? layout -> hierarchical : *layout -> frames);
+  void (* transfer) (uint64_t * restrict, size_t, unsigned, const double **) = get_JPEG_component_transfer_function(context, layout, components);
+  context -> image -> type = PLUM_IMAGE_JPEG;
+  context -> image -> frames = 1;
+  if (layout -> hierarchical) {
+    context -> image -> width = read_be16_unaligned(context -> data + layout -> hierarchical + 5);
+    context -> image -> height = read_be16_unaligned(context -> data + layout -> hierarchical + 3);
+  } else {
+    if (layout -> frames[1]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    context -> image -> width = read_be16_unaligned(context -> data + *layout -> frames + 5);
+    context -> image -> height = read_be16_unaligned(context -> data + *layout -> frames + 3);
+    for (size_t p = 0; layout -> markers[p]; p ++) if (layout -> markertype[p] == 0xdc) { // DNL marker
+      if (read_be16_unaligned(context -> data + layout -> markers[p]) != 4) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      uint_fast16_t markerheight = read_be16_unaligned(context -> data + layout -> markers[p] + 2);
+      if (!markerheight) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      if (!context -> image -> height)
+        context -> image -> height = markerheight;
+      else if (context -> image -> height != markerheight)
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+  }
+  validate_image_size(context, limit);
+  size_t count = (size_t) context -> image -> width * context -> image -> height;
+  double * component_data[4] = {0};
+  for (uint_fast8_t p = 0; p < get_JPEG_component_count(components); p ++) component_data[p] = ctxmalloc(context, sizeof **component_data * count);
+  unsigned bitdepth;
+  if (layout -> hierarchical)
+    bitdepth = load_hierarchical_JPEG(context, layout, components, component_data);
+  else
+    bitdepth = load_single_frame_JPEG(context, layout, components, component_data);
+  append_JPEG_color_depth_metadata(context, transfer, bitdepth);
+  allocate_framebuffers(context, flags, false);
+  unsigned maxvalue = ((uint32_t) 1 << bitdepth) - 1;
+  if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64) {
+    transfer(context -> image -> data64, count, maxvalue, (const double **) component_data);
+    if (flags & PLUM_ALPHA_INVERT) for (size_t p = 0; p < count; p ++) context -> image -> data64[p] ^= 0xffff000000000000u;
+  } else {
+    uint64_t * buffer = ctxmalloc(context, count * sizeof *buffer);
+    transfer(buffer, count, maxvalue, (const double **) component_data);
+    plum_convert_colors(context -> image -> data, buffer, count, flags, PLUM_COLOR_64);
+    ctxfree(context, buffer);
+  }
+  for (uint_fast8_t p = 0; p < 4; p ++) ctxfree(context, component_data[p]); // unused components will be NULL anyway
+  if (layout -> Exif) {
+    unsigned rotation = get_JPEG_rotation(context, layout -> Exif);
+    if (rotation) {
+      unsigned error = plum_rotate_image(context -> image, rotation & 3, rotation & 4);
+      if (error) throw(context, error);
+    }
+  }
+}
+
+struct JPEG_marker_layout * load_JPEG_marker_layout (struct context * context) {
+  size_t offset = 1;
+  while (context -> data[offset ++] == 0xff); // the first marker must be SOI (from file type detection), so skip it
+  uint_fast8_t next_restart_marker = 0; // 0 if not in a scan
+  size_t restart_offset, restart_interval, scan, frame = SIZE_MAX, markers = 0;
+  struct JPEG_marker_layout * layout = ctxmalloc(context, sizeof *layout);
+  *layout = (struct JPEG_marker_layout) {0}; // ensure that integers and pointers are properly zero-initialized
+  while (offset < context -> size) {
+    size_t prev = offset;
+    if (context -> data[offset ++] != 0xff)
+      if (next_restart_marker)
+        continue;
+      else
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    while (offset < context -> size && context -> data[offset] == 0xff) offset ++;
+    if (offset >= context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    uint_fast8_t marker = context -> data[offset ++];
+    if (!marker)
+      if (next_restart_marker)
+        continue;
+      else
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (marker < 0xc0 || marker == 0xc8 || marker == 0xd8 || (marker >= 0xf0 && marker != 0xfe)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (next_restart_marker) {
+      layout -> framedata[frame][scan] = ctxrealloc(context, layout -> framedata[frame][scan], sizeof ***layout -> framedata * (restart_interval + 2));
+      layout -> framedata[frame][scan][restart_interval ++] = restart_offset;
+      layout -> framedata[frame][scan][restart_interval ++] = prev - restart_offset;
+    }
+    if (marker == 0xd9) break;
+    if (marker == next_restart_marker) {
+      if (++ next_restart_marker == 0xd8) next_restart_marker = 0xd0;
+      restart_offset = offset;
+      continue;
+    } else if ((marker & ~7u) == 0xd0)
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    // if we find a marker other than RST, we're definitely ending the current scan, and the marker definitely has a size
+    if (next_restart_marker) {
+      layout -> framedata[frame][scan] = ctxrealloc(context, layout -> framedata[frame][scan], sizeof ***layout -> framedata * (restart_interval + 1));
+      layout -> framedata[frame][scan][restart_interval] = 0;
+      next_restart_marker = 0;
+    }
+    if (offset > context -> size - 2) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    uint_fast16_t marker_size = read_be16_unaligned(context -> data + offset);
+    if (marker_size < 2 || marker_size > context -> size || offset > context -> size - marker_size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    switch (marker) {
+      case 0xc0: case 0xc1: case 0xc2: case 0xc3: case 0xc5: case 0xc6:
+      case 0xc7: case 0xc9: case 0xca: case 0xcb: case 0xcd: case 0xce: case 0xcf:
+        // start a new frame
+        if (frame != SIZE_MAX) {
+          if (scan == SIZE_MAX) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+          layout -> framescans[frame] = ctxrealloc(context, layout -> framescans[frame], sizeof **layout -> framescans * ((++ scan) + 1));
+          layout -> framescans[frame][scan] = 0;
+        }
+        layout -> frames = ctxrealloc(context, layout -> frames, sizeof *layout -> frames * ((size_t) (++ frame) + 1));
+        layout -> frames[frame] = offset;
+        layout -> framescans = ctxrealloc(context, layout -> framescans, sizeof *layout -> framescans * (frame + 1));
+        layout -> framescans[frame] = NULL;
+        layout -> framedata = ctxrealloc(context, layout -> framedata, sizeof *layout -> framedata * (frame + 1));
+        layout -> framedata[frame] = NULL;
+        layout -> frametype = ctxrealloc(context, layout -> frametype, sizeof *layout -> frametype * (frame + 1));
+        layout -> frametype[frame] = marker & 15;
+        scan = SIZE_MAX;
+        break;
+      case 0xda:
+        // start a new scan
+        if (frame == SIZE_MAX) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        layout -> framescans[frame] = ctxrealloc(context, layout -> framescans[frame], sizeof **layout -> framescans * ((size_t) (++ scan) + 1));
+        layout -> framescans[frame][scan] = offset;
+        layout -> framedata[frame] = ctxrealloc(context, layout -> framedata[frame], sizeof **layout -> framedata * (scan + 1));
+        layout -> framedata[frame][scan] = NULL;
+        restart_interval = 0;
+        restart_offset = offset + marker_size;
+        next_restart_marker = 0xd0;
+        break;
+      case 0xde:
+        if (layout -> hierarchical || frame != SIZE_MAX) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        layout -> hierarchical = offset;
+        break;
+      case 0xdf:
+        if (!layout -> hierarchical) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      case 0xc4: case 0xcc: case 0xdb: case 0xdc: case 0xdd:
+        layout -> markers = ctxrealloc(context, layout -> markers, sizeof *layout -> markers * (markers + 1));
+        layout -> markers[markers] = offset;
+        layout -> markertype = ctxrealloc(context, layout -> markertype, sizeof *layout -> markertype * (markers + 1));
+        layout -> markertype[markers ++] = marker;
+        break;
+      // For JFIF, Exif and Adobe markers, all want to come "first", i.e., immediately after SOI. This is obviously impossible if more than one is present.
+      // Therefore, "first" is interpreted to mean "before any SOF/DHP marker" here.
+      case 0xe0:
+        if (layout -> JFIF || layout -> hierarchical || frame != SIZE_MAX) break;
+        if (marker_size >= 7 && bytematch(context -> data + offset + 2, 0x4a, 0x46, 0x49, 0x46, 0x00)) layout -> JFIF = offset;
+        break;
+      case 0xe1:
+        if (layout -> Exif || layout -> hierarchical || frame != SIZE_MAX) break;
+        if (marker_size >= 16 && bytematch(context -> data + offset + 2, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00)) layout -> Exif = offset;
+        break;
+      case 0xee:
+        if (layout -> Adobe || layout -> hierarchical || frame != SIZE_MAX) break;
+        if (marker_size >= 9 && bytematch(context -> data + offset + 2, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00) &&
+            (context -> data[offset + 8] == 100 || context -> data[offset + 8] == 101))
+          layout -> Adobe = offset;
+    }
+    offset += marker_size;
+  }
+  if (frame == SIZE_MAX) throw(context, PLUM_ERR_NO_DATA);
+  if (scan == SIZE_MAX) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  layout -> markers = ctxrealloc(context, layout -> markers, sizeof *layout -> markers * (markers + 1));
+  layout -> markers[markers] = 0;
+  if (next_restart_marker) {
+    layout -> framedata[frame][scan] = ctxrealloc(context, layout -> framedata[frame][scan], sizeof ***layout -> framedata * (restart_interval + 1));
+    layout -> framedata[frame][scan][restart_interval] = 0;
+  }
+  layout -> framescans[frame] = ctxrealloc(context, layout -> framescans[frame], sizeof **layout -> framescans * (++ scan + 1));
+  layout -> framescans[frame][scan] = 0;
+  layout -> frames = ctxrealloc(context, layout -> frames, sizeof *layout -> frames * (++ frame + 1));
+  layout -> frames[frame] = 0;
+  return layout;
+}
+
+unsigned get_JPEG_rotation (struct context * context, size_t offset) {
+  // returns rotation count in bits 0-1 and vertical inversion in bit 2
+  uint_fast16_t size = read_be16_unaligned(context -> data + offset);
+  const unsigned char * data = context -> data + offset + 8;
+  size -= 8;
+  uint_fast16_t tag = read_le16_unaligned(data);
+  bool bigendian;
+  if (tag == 0x4949)
+    bigendian = false; // little endian
+  else if (tag == 0x4d4d)
+    bigendian = true;
+  else
+    return 0;
+  tag = bigendian ? read_be16_unaligned(data + 2) : read_le16_unaligned(data + 2);
+  if (tag != 42) return 0;
+  uint_fast32_t pos = bigendian ? read_be32_unaligned(data + 4) : read_le32_unaligned(data + 4);
+  if (pos > size - 2) return 0;
+  uint_fast16_t count = bigendian ? read_be16_unaligned(data + pos) : read_le16_unaligned(data + pos);
+  pos += 2;
+  if (size - pos < (uint_fast32_t) count * 12) return 0;
+  for (; count; pos += 12, count --) {
+    tag = bigendian ? read_be16_unaligned(data + pos) : read_le16_unaligned(data + pos);
+    if (tag == 0x112) break; // 0x112 = orientation data
+  }
+  if (!count) return 0;
+  tag = bigendian ? read_be16_unaligned(data + pos + 2) : read_le16_unaligned(data + pos + 2);
+  uint_fast32_t datasize = bigendian ? read_be32_unaligned(data + pos + 4) : read_le32_unaligned(data + pos + 4);
+  if (tag != 3 || datasize != 1) return 0;
+  tag = bigendian ? read_be16_unaligned(data + pos + 8) : read_le16_unaligned(data + pos + 8);
+  static const unsigned rotations[] = {0, 6, 2, 4, 7, 1, 5, 3};
+  if (-- tag >= sizeof rotations / sizeof *rotations) tag = 0;
+  return rotations[tag];
+}
+
+unsigned load_single_frame_JPEG (struct context * context, const struct JPEG_marker_layout * layout, uint32_t components, double ** output) {
+  if (*layout -> frametype & 4) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  struct JPEG_decoder_tables tables;
+  initialize_JPEG_decoder_tables(context, &tables, layout);
+  unsigned precision = context -> data[*layout -> frames + 2];
+  if (precision < 2 || precision > 16) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  size_t metadata_index = 0;
+  if (*layout -> frametype == 3 || *layout -> frametype == 11)
+    load_JPEG_lossless_frame(context, layout, components, 0, &tables, &metadata_index, output, precision, context -> image -> width, context -> image -> height);
+  else
+    load_JPEG_DCT_frame(context, layout, components, 0, &tables, &metadata_index, output, precision, context -> image -> width, context -> image -> height);
+  return precision;
+}
+
+unsigned char process_JPEG_metadata_until_offset (struct context * context, const struct JPEG_marker_layout * layout, struct JPEG_decoder_tables * tables,
+                                                  size_t * restrict index, size_t limit) {
+  unsigned char expansion = 0;
+  for (; layout -> markers[*index] && layout -> markers[*index] < limit; ++ *index) {
+    const unsigned char * markerdata = context -> data + layout -> markers[*index];
+    uint16_t markersize = read_be16_unaligned(markerdata) - 2;
+    markerdata += 2;
+    switch (layout -> markertype[*index]) {
+      case 0xc4: // DHT
+        while (markersize) {
+          if (*markerdata & ~0x13u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+          unsigned char target = (*markerdata & 3) | (*markerdata >> 2);
+          markerdata ++;
+          markersize --;
+          if (tables -> Huffman[target]) ctxfree(context, tables -> Huffman[target]);
+          tables -> Huffman[target] = process_JPEG_Huffman_table(context, &markerdata, &markersize);
+        }
+        break;
+      case 0xcc: // DAC
+        if (markersize % 2) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        for (uint_fast16_t count = markersize / 2; count; count --) {
+          unsigned char target = *(markerdata ++);
+          if (target & ~0x13u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+          target = (target >> 2) | (target & 3);
+          if (target & 4) {
+            if (!*markerdata || *markerdata > 63) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+          } else
+            if ((*markerdata >> 4) < (*markerdata & 15)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+          tables -> arithmetic[target] = *(markerdata ++);
+        }
+        break;
+      case 0xdb: // DQT
+        while (markersize) {
+          if (*markerdata & ~0x13u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+          unsigned char target = *markerdata & 3, type = *markerdata >> 4, length = type ? 128 : 64;
+          markerdata ++;
+          if (-- markersize < length) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+          markersize -= length;
+          if (!tables -> quantization[target]) tables -> quantization[target] = ctxmalloc(context, 64 * sizeof *tables -> quantization[target]);
+          if (type)
+            for (uint_fast8_t p = 0; p < 64; p ++, markerdata += 2) tables -> quantization[target][p] = read_be16_unaligned(markerdata);
+          else
+            for (uint_fast8_t p = 0; p < 64; p ++) tables -> quantization[target][p] = *(markerdata ++);
+        }
+        break;
+      case 0xdd: // DRI
+        if (markersize != 2) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        tables -> restart = read_be16_unaligned(markerdata);
+        break;
+      case 0xdf: // EXP
+        if (markersize != 1) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        expansion = *markerdata;
+    }
+  }
+  return expansion;
+}
+
+void load_JPEG_DCT_frame (struct context * context, const struct JPEG_marker_layout * layout, uint32_t components, size_t frameindex,
+                          struct JPEG_decoder_tables * tables, size_t * restrict metadata_index, double ** output, unsigned precision, size_t width,
+                          size_t height) {
+  const size_t * scans = layout -> framescans[frameindex];
+  const size_t ** offsets = (const size_t **) layout -> framedata[frameindex];
+  // obtain this frame's components' parameters and compute the number of (non-subsampled) blocks per MCU (maximum scale factor for each dimension)
+  struct JPEG_component_info component_info[4];
+  uint_fast8_t maxH = 1, maxV = 1, count = get_JPEG_component_info(context, context -> data + layout -> frames[frameindex], component_info, components);
+  for (uint_fast8_t p = 0; p < count; p ++) {
+    if (component_info[p].scaleV > maxV) maxV = component_info[p].scaleV;
+    if (component_info[p].scaleH > maxH) maxH = component_info[p].scaleH;
+  }
+  // compute the image dimensions in MCUs and allocate space for that many coefficients for each component (including padding blocks to fill up edge MCUs)
+  size_t unitrow = (width - 1) / (8 * maxH) + 1, unitcol = (height - 1) / (8 * maxV) + 1, units = unitrow * unitcol;
+  int16_t (* restrict component_data[4])[64] = {0};
+  for (uint_fast8_t p = 0; p < count; p ++)
+    component_data[p] = ctxmalloc(context, sizeof **component_data * units * component_info[p].scaleH * component_info[p].scaleV);
+  unsigned char currentbits[4][64]; // successive approximation bit positions for each component and coefficient, for progressive scans
+  memset(currentbits, 0xff, sizeof currentbits); // 0xff = no data yet (i.e., the coefficient hasn't shown up yet in any scans)
+  for (; *scans; scans ++, offsets ++) {
+    if (process_JPEG_metadata_until_offset(context, layout, tables, metadata_index, **offsets)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    unsigned char scancomponents[4];
+    const unsigned char * progdata = get_JPEG_scan_components(context, *scans, component_info, count, scancomponents);
+    // validate the spectral selection parameters
+    uint_fast8_t first = *progdata, last = progdata[1];
+    if (first > last || last > 63) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    // validate and update the successive approximation bit positions for each component and coefficient involved in the scan
+    uint_fast8_t bitstart = progdata[2] >> 4, bitend = progdata[2] & 15;
+    if ((bitstart && bitstart - 1 != bitend) || bitend > 13 || bitend >= precision) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (!bitstart) bitstart = 0xff; // 0xff = not a progressive scan (intentionally matches the "no data yet" 0xff in currentbits)
+    for (uint_fast8_t p = 0; p < 4 && scancomponents[p] != 0xff; p ++) {
+      // check that all quantization and Huffman tables (when applicable) used by the scan have already been loaded
+      if (!tables -> quantization[component_info[scancomponents[p]].tableQ]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      if (!(layout -> frametype[frameindex] & 8)) {
+        if (!first && !tables -> Huffman[component_info[scancomponents[p]].tableDC]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (last && !tables -> Huffman[component_info[scancomponents[p]].tableAC + 4]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      }
+      // ensure that this scan's successive approximation bit parameters match what is expected based on previous scans, and update currentbits
+      for (uint_fast8_t coefficient = first; coefficient <= last; coefficient ++) {
+        if (currentbits[scancomponents[p]][coefficient] != bitstart) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        currentbits[scancomponents[p]][coefficient] = bitend;
+      }
+    }
+    size_t scanunitrow = unitrow;
+    struct JPEG_decompressor_state state;
+    initialize_JPEG_decompressor_state(context, &state, component_info, scancomponents, &scanunitrow, unitcol, width, height, maxH, maxV, tables, *offsets,
+                                       component_data);
+    // call the decompression function, depending on the frame type (Huffman or arithmetic) and whether it is progressive or not
+    if (bitstart == 0xff)
+      if (layout -> frametype[frameindex] & 8)
+        decompress_JPEG_arithmetic_scan(context, &state, tables, scanunitrow, component_info, *offsets, bitend, first, last, layout -> frametype[frameindex] & 4);
+      else
+        decompress_JPEG_Huffman_scan(context, &state, tables, scanunitrow, component_info, *offsets, bitend, first, last, layout -> frametype[frameindex] & 4);
+    else
+      if (layout -> frametype[frameindex] & 8)
+        decompress_JPEG_arithmetic_bit_scan(context, &state, scanunitrow, component_info, *offsets, bitend, first, last);
+      else
+        decompress_JPEG_Huffman_bit_scan(context, &state, tables, scanunitrow, component_info, *offsets, bitend, first, last);
+  }
+  // ensure that the frame's scans contain all bits for all coefficients, for each one of its components
+  for (uint_fast8_t p = 0; p < count; p ++) for (uint_fast8_t coefficient = 0; coefficient < 64; coefficient ++)
+    if (currentbits[p][coefficient]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  // if the frame is non-differential, initialize all components in the final image to the level shift value
+  if (!(layout -> frametype[frameindex] & 4)) {
+    double levelshift = 1u << (precision - 1);
+    for (uint_fast8_t p = 0; p < count; p ++) for (size_t i = 0; i < width * height; i ++) output[p][i] = levelshift;
+  }
+  // transform all blocks into component data and add it to the output (level shift value for non-differential frames, previous values for differential frames)
+  // loop backwards so DCT data is released in reverse allocation order after transforming it into output data
+  while (count --) {
+    size_t compwidth = unitrow * component_info[count].scaleH * 8 + 2, compheight = unitcol * component_info[count].scaleV * 8 + 2;
+    double * transformed = ctxmalloc(context, sizeof *transformed * compwidth * compheight); // component data buffer, plus a pixel of padding around the edges
+    for (size_t y = 0; y < unitcol * component_info[count].scaleV; y ++) for (size_t x = 0; x < unitrow * component_info[count].scaleH; x ++) {
+      // apply the reverse DCT to each block, transforming it into component data
+      double buffer[64];
+      apply_JPEG_inverse_DCT(buffer, component_data[count][y * unitrow * component_info[count].scaleH + x], tables -> quantization[component_info[count].tableQ]);
+      // copy the block's data to the correct location in the component data buffer (accounting for the padding)
+      double * current = transformed + (y * 8 + 1) * compwidth + x * 8 + 1;
+      for (uint_fast8_t row = 0; row < 8; row ++) memcpy(current + compwidth * row, buffer + 8 * row, sizeof *buffer * 8);
+    }
+    // scale up subsampled components and add them to the output
+    unpack_JPEG_component(output[count], transformed, width, height, compwidth, compheight, component_info[count].scaleH, component_info[count].scaleV, maxH, maxV);
+    ctxfree(context, transformed);
+    ctxfree(context, component_data[count]);
+  }
+}
+
+void load_JPEG_lossless_frame (struct context * context, const struct JPEG_marker_layout * layout, uint32_t components, size_t frameindex,
+                               struct JPEG_decoder_tables * tables, size_t * restrict metadata_index, double ** output, unsigned precision, size_t width,
+                               size_t height) {
+  const size_t * scans = layout -> framescans[frameindex];
+  const size_t ** offsets = (const size_t **) layout -> framedata[frameindex];
+  // obtain this frame's components' parameters and compute the number of pixels per MCU (maximum scale factor for each dimension)
+  struct JPEG_component_info component_info[4];
+  uint_fast8_t maxH = 1, maxV = 1, count = get_JPEG_component_info(context, context -> data + layout -> frames[frameindex], component_info, components);
+  for (uint_fast8_t p = 0; p < count; p ++) {
+    if (component_info[p].scaleV > maxV) maxV = component_info[p].scaleV;
+    if (component_info[p].scaleH > maxH) maxH = component_info[p].scaleH;
+  }
+  // compute the image dimensions in MCUs and allocate space for that many pixels for each component (including padding pixels to fill edge MCUs)
+  size_t unitrow = (width - 1) / maxH + 1, unitcol = (height - 1) / maxV + 1, units = unitrow * unitcol;
+  uint16_t * restrict component_data[4] = {0};
+  for (uint_fast8_t p = 0; p < count; p ++)
+    component_data[p] = ctxmalloc(context, sizeof **component_data * units * component_info[p].scaleH * component_info[p].scaleV);
+  double initial_value[4]; // offset to add to pixel data, to reduce rounding errors in shifted-down components (0 if no offset is needed)
+  int component_shift[4] = {-1, -1, -1, -1}; // shift amounts for each component (negative: the component hasn't shown up in any scans yet)
+  for (; *scans; scans ++, offsets ++) {
+    if (process_JPEG_metadata_until_offset(context, layout, tables, metadata_index, **offsets)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    unsigned char scancomponents[4];
+    const unsigned char * progdata = get_JPEG_scan_components(context, *scans, component_info, count, scancomponents);
+    uint_fast8_t predictor = *progdata, shift = progdata[2] & 15;
+    if (predictor > 7 || shift >= precision) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    for (uint_fast8_t p = 0; p < 4 && scancomponents[p] != 0xff; p ++) {
+      // check that the components from this scan haven't already been decoded, and update the components' shift amount and initial value
+      if (component_shift[scancomponents[p]] >= 0) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      component_shift[scancomponents[p]] = shift;
+      if (layout -> hierarchical)
+        initial_value[scancomponents[p]] = 0;
+      else
+        initial_value[scancomponents[p]] = shift ? 1u << (shift - 1) : 0;
+      // if the frame is a Huffman frame, ensure that all Huffman tables used by the scan have already been loaded
+      if (!((layout -> frametype[frameindex] & 8) || tables -> Huffman[component_info[scancomponents[p]].tableDC])) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+    size_t scanunitrow = unitrow;
+    struct JPEG_decompressor_state state;
+    initialize_JPEG_decompressor_state_lossless(context, &state, component_info, scancomponents, &scanunitrow, unitcol, width, height, maxH, maxV, tables,
+                                                *offsets, component_data);
+    // call the decompression function, depending on the frame type (Huffman or arithmetic) - lossless scans cannot be progressive
+    if (layout -> frametype[frameindex] & 8)
+      decompress_JPEG_arithmetic_lossless_scan(context, &state, tables, scanunitrow, component_info, *offsets, predictor, precision - shift);
+    else
+      decompress_JPEG_Huffman_lossless_scan(context, &state, tables, scanunitrow, component_info, *offsets, predictor, precision - shift);
+  }
+  // ensure that all components in the frame have appeared in some scan
+  for (uint_fast8_t p = 0; p < count; p ++) if (component_shift[p] < 0) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  // convert all decoded component data into actual component pixels; loop backwards so that temporary component data is released in reverse allocation order
+  while (count --) {
+    size_t compwidth = unitrow * component_info[count].scaleH + 2, compheight = unitcol * component_info[count].scaleV + 2;
+    double * converted = ctxmalloc(context, sizeof *converted * compwidth * compheight); // output data buffer, plus a pixel of padding around the edges
+    // shift up all component pixels as needed, and store them in the correct location in the output data buffer (accounting for the padding)
+    for (size_t y = 0; y < unitcol * component_info[count].scaleV; y ++) for (size_t x = 0; x < unitrow * component_info[count].scaleH; x ++)
+      converted[(y + 1) * compwidth + x + 1] = component_data[count][y * unitrow * component_info[count].scaleH + x] << component_shift[count];
+    // add the initial value to all component values and scale up subsampled components as needed
+    if (!(layout -> frametype[frameindex] & 4)) for (size_t p = 0; p < width * height; p ++) output[count][p] = initial_value[count];
+    unpack_JPEG_component(output[count], converted, width, height, compwidth, compheight, component_info[count].scaleH, component_info[count].scaleV, maxH, maxV);
+    ctxfree(context, converted);
+    ctxfree(context, component_data[count]);
+  }
+}
+
+unsigned get_JPEG_component_info (struct context * context, const unsigned char * frameheader, struct JPEG_component_info * restrict output, uint32_t components) {
+  // assumes the component list is correct - true by definition for single-frame images and checked elsewhere for hierarchical ones
+  unsigned char component_numbers[4];
+  write_le32_unaligned(component_numbers, components);
+  uint_fast8_t count = get_JPEG_component_count(components);
+  for (uint_fast8_t current = 0; current < count; current ++) {
+    uint_fast8_t index;
+    for (index = 0; index < count; index ++) if (component_numbers[index] == frameheader[8 + 3 * current]) break;
+    output[index].index = component_numbers[index];
+    uint_fast8_t p = frameheader[9 + 3 * current];
+    output[index].scaleH = p >> 4;
+    output[index].scaleV = p & 15;
+    output[index].tableQ = frameheader[10 + 3 * current];
+    if (!output[index].scaleH || output[index].scaleH > 4 || !output[index].scaleV || output[index].scaleV > 4 || output[index].tableQ > 3)
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  return count;
+}
+
+const unsigned char * get_JPEG_scan_components (struct context * context, size_t offset, struct JPEG_component_info * restrict compinfo, unsigned framecount,
+                                                unsigned char * restrict output) {
+  uint_fast16_t headerlength = read_be16_unaligned(context -> data + offset);
+  if (headerlength < 8) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint_fast8_t count = context -> data[offset + 2];
+  if (headerlength != 6 + 2 * count) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  memset(output, 0xff, 4);
+  for (uint_fast8_t p = 0; p < count; p ++) {
+    uint_fast8_t index;
+    for (index = 0; index < framecount; index ++) if (compinfo[index].index == context -> data[offset + 3 + 2 * p]) break;
+    if (index == framecount) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    output[p] = index;
+    compinfo[index].tableDC = context -> data[offset + 4 + 2 * p] >> 4;
+    compinfo[index].tableAC = context -> data[offset + 4 + 2 * p] & 15;
+    if (compinfo[index].tableDC > 3 || compinfo[index].tableAC > 3) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  return context -> data + offset + 3 + 2 * count;
+}
+
+void unpack_JPEG_component (double * restrict result, double * restrict source, size_t width, size_t height, size_t scaled_width, size_t scaled_height,
+                            unsigned char scaleH, unsigned char scaleV, unsigned char maxH, unsigned char maxV) {
+  // fill the border of the component data (one pixel of dummy values around the edges) by copying values from the true edges
+  size_t scaled_size = scaled_width * scaled_height;
+  for (size_t p = 1; p < scaled_width - 1; p ++) source[p] = source[p + scaled_width];
+  for (size_t p = 2; p < scaled_width; p ++) source[scaled_size - p] = source[scaled_size - p - scaled_width];
+  for (size_t p = 0; p < scaled_height; p ++) {
+    source[p * scaled_width] = source[p * scaled_width + 1];
+    source[(p + 1) * scaled_width - 1] = source[(p + 1) * scaled_width - 2];
+  }
+  // if the scaling parameters form a reducible fraction, reduce it
+  if (scaleH == maxH)
+    scaleH = maxH = 1;
+  else if (maxH == 4 && scaleH == 2) {
+    maxH = 2;
+    scaleH = 1;
+  }
+  if (scaleV == maxV)
+    scaleV = maxV = 1;
+  else if (maxV == 4 && scaleV == 2) {
+    maxV = 2;
+    scaleV = 1;
+  }
+  // indexes into the interpolation index lists: 1-4 for integer ratios (scale = 1 after normalization above), 0 for 3/2, 5 for 4/3
+  unsigned char indexH = (scaleH == 2) ? 0 : (scaleH == 3) ? 5 : maxH, indexV = (scaleV == 2) ? 0 : (scaleV == 3) ? 5 : maxV;
+  // weights for all possible scaling factors (3/2, 1 to 4, 4/3); a subset of these will be selected for each axis depending on the actual scale factor
+  static const double interpolation_weights[] = {0x0.55555555555558p+0, 0x0.aaaaaaaaaaaaa8p+0, 1.0, 0x0.aaaaaaaaaaaaa8p+0, 0x0.55555555555558p+0, 0.0,
+                                                 0x0.4p+0, 0x0.cp+0, 0x0.4p+0, 0x0.2aaaaaaaaaaaa8p+0, 0x0.8p+0, 0x0.d5555555555558p+0, 0x0.8p+0,
+                                                 0x0.2aaaaaaaaaaaa8p+0, 0x0.2p+0, 0x0.6p+0, 0x0.ap+0, 0x0.ep+0, 0x0.ap+0, 0x0.6p+0, 0x0.2p+0};
+  static const unsigned char first_interpolation_indexes[] = {9, 5, 7, 3, 17, 14};
+  static const unsigned char second_interpolation_indexes[] = {11, 2, 6, 0, 14, 17};
+  // actual interpolation weights for each pixel in a row/column of upscaled pixels
+  const double * firstH = interpolation_weights + first_interpolation_indexes[indexH];
+  const double * firstV = interpolation_weights + first_interpolation_indexes[indexV];
+  const double * secondH = interpolation_weights + second_interpolation_indexes[indexH];
+  const double * secondV = interpolation_weights + second_interpolation_indexes[indexV];
+  // scale up the component, as determined by the scale parameters, by interpolating the decoded data
+  unsigned char offsetV = maxV / (2 * scaleV), offsetH = maxH / (2 * scaleH);
+  for (size_t p = 0, sourceY = 0, row = 0; row < height; row ++) {
+    for (size_t sourceX = 0, col = 0; col < width; col ++) {
+      result[p ++] += source[sourceX + sourceY * scaled_width] * firstH[offsetH] * firstV[offsetV] +
+                      source[sourceX + 1 + sourceY * scaled_width] * secondH[offsetH] * firstV[offsetV] +
+                      source[sourceX + (sourceY + 1) * scaled_width] * firstH[offsetH] * secondV[offsetV] +
+                      source[sourceX + 1 + (sourceY + 1) * scaled_width] * secondH[offsetH] * secondV[offsetV];
+      if (++ offsetH == maxH) {
+        offsetH = 0;
+        if (scaleH == 1) sourceX ++;
+      } else if (scaleH != 1)
+        sourceX ++;
+    }
+    if (++ offsetV == maxV) {
+      offsetV = 0;
+      if (scaleV == 1) sourceY ++;
+    } else if (scaleV != 1)
+      sourceY ++;
+  }
+}
+
+void initialize_JPEG_decoder_tables (struct context * context, struct JPEG_decoder_tables * tables, const struct JPEG_marker_layout * layout) {
+  *tables = (struct JPEG_decoder_tables) {
+    .Huffman = {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL},
+    .quantization = {NULL, NULL, NULL, NULL},
+    .arithmetic = {0x10, 0x10, 0x10, 0x10, 5, 5, 5, 5},
+    .restart = 0
+  };
+  // if the image doesn't define a Huffman table (no DHT markers), load the standard's recommended tables as "default" tables
+  for (size_t p = 0; layout -> markers[p]; p ++) if (layout -> markertype[p] == 0xc4) return;
+  load_default_JPEG_Huffman_tables(context, tables);
+}
+
+short * process_JPEG_Huffman_table (struct context * context, const unsigned char ** restrict markerdata, uint16_t * restrict markersize) {
+  if (*markersize < 16) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint_fast16_t totalsize = 0, tablesize = 16; // 16 so it counts the initial length bytes too
+  const unsigned char * lengths = *markerdata;
+  const unsigned char * data = *markerdata + 16;
+  for (uint_fast8_t size = 0; size < 16; size ++) {
+    tablesize += lengths[size];
+    totalsize += lengths[size] * (size + 1) * 2; // not necessarily the real size of the table, but an easily calculated upper bound
+  }
+  if (*markersize < tablesize) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  *markersize -= tablesize;
+  *markerdata += tablesize;
+  short * result = ctxmalloc(context, totalsize * sizeof *result);
+  for (uint_fast16_t p = 0; p < totalsize; p ++) result[p] = -1;
+  uint_fast16_t code = 0, next = 2, offset = 0x8000u;
+  // size is one less because we don't count the link to the leaf
+  for (uint_fast8_t size = 0; size < 16; size ++, offset >>= 1) for (uint_fast8_t count = lengths[size]; count; count --) {
+    uint_fast16_t current = 0x8000u, index = 0;
+    for (uint_fast8_t remainder = size; remainder; remainder --) {
+      if (code & current) index ++;
+      current >>= 1;
+      if (result[index] == -1) {
+        result[index] = -(short) next;
+        next += 2;
+      }
+      index = -result[index];
+    }
+    if (code & current) index ++;
+    result[index] = *(data ++);
+    if ((uint_fast32_t) code + offset > 0xffffu) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    code += offset;
+  }
+  return ctxrealloc(context, result, next * sizeof *result);
+}
+
+void load_default_JPEG_Huffman_tables (struct context * context, struct JPEG_decoder_tables * restrict tables) {
+  /* default tables from the JPEG specification, already preprocessed into a tree, in the same format as other trees:
+     two values per node, non-negative values are leaves, negative values are array indexes where the next node is
+     found (always even, because each node takes up two entries), -1 is an empty branch; index 0 is the root node */
+  static const short luminance_DC_table[] = {
+    //           0     1     2     3     4     5     6     7     8     9    10    11    12    13    14    15    16    17    18    19
+    /*   0 */   -2,   -6, 0x00,   -4, 0x01, 0x02,   -8,  -10, 0x03, 0x04, 0x05,  -12, 0x06,  -14, 0x07,  -16, 0x08,  -18, 0x09,  -20,
+    /*  20 */ 0x0a,  -22, 0x0b,   -1
+  };
+  static const short chrominance_DC_table[] = {
+    //           0     1     2     3     4     5     6     7     8     9    10    11    12    13    14    15    16    17    18    19
+    /*   0 */   -2,   -4, 0x00, 0x01, 0x02,   -6, 0x03,   -8, 0x04,  -10, 0x05,  -12, 0x06,  -14, 0x07,  -16, 0x08,  -18, 0x09,  -20,
+    /*  20 */ 0x0a,  -22, 0x0b,   -1
+  };
+  static const short luminance_AC_table[] = {
+    //           0     1     2     3     4     5     6     7     8     9    10    11    12    13    14    15    16    17    18    19
+    /*   0 */   -2,   -4, 0x01, 0x02,   -6,  -10, 0x03,   -8, 0x00, 0x04,  -12,  -16, 0x11,  -14, 0x05, 0x12,  -18,  -22, 0x21,  -20,
+    /*  20 */ 0x31, 0x41,  -24,  -30,  -26,  -28, 0x06, 0x13, 0x51, 0x61,  -32,  -40,  -34,  -36, 0x07, 0x22, 0x71,  -38, 0x14, 0x32,
+    /*  40 */  -42,  -50,  -44,  -46, 0x81, 0x91, 0xa1,  -48, 0x08, 0x23,  -52,  -60,  -54,  -56, 0x42, 0xb1, 0xc1,  -58, 0x15, 0x52,
+    /*  60 */  -62,  -72,  -64,  -66, 0xd1, 0xf0,  -68,  -70, 0x24, 0x33, 0x62, 0x72,  -74, -198,  -76, -136,  -78, -106,  -80,  -92,
+    /*  80 */  -82,  -86, 0x82,  -84, 0x09, 0x0a,  -88,  -90, 0x16, 0x17, 0x18, 0x19,  -94, -100,  -96,  -98, 0x1a, 0x25, 0x26, 0x27,
+    /* 100 */ -102, -104, 0x28, 0x29, 0x2a, 0x34, -108, -122, -110, -116, -112, -114, 0x35, 0x36, 0x37, 0x38, -118, -120, 0x39, 0x3a,
+    /* 120 */ 0x43, 0x44, -124, -130, -126, -128, 0x45, 0x46, 0x47, 0x48, -132, -134, 0x49, 0x4a, 0x53, 0x54, -138, -168, -140, -154,
+    /* 140 */ -142, -148, -144, -146, 0x55, 0x56, 0x57, 0x58, -150, -152, 0x59, 0x5a, 0x63, 0x64, -156, -162, -158, -160, 0x65, 0x66,
+    /* 160 */ 0x67, 0x68, -164, -166, 0x69, 0x6a, 0x73, 0x74, -170, -184, -172, -178, -174, -176, 0x75, 0x76, 0x77, 0x78, -180, -182,
+    /* 180 */ 0x79, 0x7a, 0x83, 0x84, -186, -192, -188, -190, 0x85, 0x86, 0x87, 0x88, -194, -196, 0x89, 0x8a, 0x92, 0x93, -200, -262,
+    /* 200 */ -202, -232, -204, -218, -206, -212, -208, -210, 0x94, 0x95, 0x96, 0x97, -214, -216, 0x98, 0x99, 0x9a, 0xa2, -220, -226,
+    /* 220 */ -222, -224, 0xa3, 0xa4, 0xa5, 0xa6, -228, -230, 0xa7, 0xa8, 0xa9, 0xaa, -234, -248, -236, -242, -238, -240, 0xb2, 0xb3,
+    /* 240 */ 0xb4, 0xb5, -244, -246, 0xb6, 0xb7, 0xb8, 0xb9, -250, -256, -252, -254, 0xba, 0xc2, 0xc3, 0xc4, -258, -260, 0xc5, 0xc6,
+    /* 260 */ 0xc7, 0xc8, -264, -294, -266, -280, -268, -274, -270, -272, 0xc9, 0xca, 0xd2, 0xd3, -276, -278, 0xd4, 0xd5, 0xd6, 0xd7,
+    /* 280 */ -282, -288, -284, -286, 0xd8, 0xd9, 0xda, 0xe1, -290, -292, 0xe2, 0xe3, 0xe4, 0xe5, -296, -310, -298, -304, -300, -302,
+    /* 300 */ 0xe6, 0xe7, 0xe8, 0xe9, -306, -308, 0xea, 0xf1, 0xf2, 0xf3, -312, -318, -314, -316, 0xf4, 0xf5, 0xf6, 0xf7, -320, -322,
+    /* 320 */ 0xf8, 0xf9, 0xfa,   -1
+  };
+  static const short chrominance_AC_table[] = {
+    //           0     1     2     3     4     5     6     7     8     9    10    11    12    13    14    15    16    17    18    19
+    /*   0 */   -2,   -4, 0x00, 0x01,   -6,  -10, 0x02,   -8, 0x03, 0x11,  -12,  -18,  -14,  -16, 0x04, 0x05, 0x21, 0x31,  -20,  -26,
+    /*  20 */  -22,  -24, 0x06, 0x12, 0x41, 0x51,  -28,  -36,  -30,  -32, 0x07, 0x61, 0x71,  -34, 0x13, 0x22,  -38,  -48,  -40,  -42,
+    /*  40 */ 0x32, 0x81,  -44,  -46, 0x08, 0x14, 0x42, 0x91,  -50,  -58,  -52,  -54, 0xa1, 0xb1, 0xc1,  -56, 0x09, 0x23,  -60,  -68,
+    /*  60 */  -62,  -64, 0x33, 0x52, 0xf0,  -66, 0x15, 0x62,  -70,  -80,  -72,  -74, 0x72, 0xd1,  -76,  -78, 0x0a, 0x16, 0x24, 0x34,
+    /*  80 */  -82, -198,  -84, -136,  -86, -106,  -88,  -92, 0xe1,  -90, 0x25, 0xf1,  -94, -100,  -96,  -98, 0x17, 0x18, 0x19, 0x1a,
+    /* 100 */ -102, -104, 0x26, 0x27, 0x28, 0x29, -108, -122, -110, -116, -112, -114, 0x2a, 0x35, 0x36, 0x37, -118, -120, 0x38, 0x39,
+    /* 120 */ 0x3a, 0x43, -124, -130, -126, -128, 0x44, 0x45, 0x46, 0x47, -132, -134, 0x48, 0x49, 0x4a, 0x53, -138, -168, -140, -154,
+    /* 140 */ -142, -148, -144, -146, 0x54, 0x55, 0x56, 0x57, -150, -152, 0x58, 0x59, 0x5a, 0x63, -156, -162, -158, -160, 0x64, 0x65,
+    /* 160 */ 0x66, 0x67, -164, -166, 0x68, 0x69, 0x6a, 0x73, -170, -184, -172, -178, -174, -176, 0x74, 0x75, 0x76, 0x77, -180, -182,
+    /* 180 */ 0x78, 0x79, 0x7a, 0x82, -186, -192, -188, -190, 0x83, 0x84, 0x85, 0x86, -194, -196, 0x87, 0x88, 0x89, 0x8a, -200, -262,
+    /* 200 */ -202, -232, -204, -218, -206, -212, -208, -210, 0x92, 0x93, 0x94, 0x95, -214, -216, 0x96, 0x97, 0x98, 0x99, -220, -226,
+    /* 220 */ -222, -224, 0x9a, 0xa2, 0xa3, 0xa4, -228, -230, 0xa5, 0xa6, 0xa7, 0xa8, -234, -248, -236, -242, -238, -240, 0xa9, 0xaa,
+    /* 240 */ 0xb2, 0xb3, -244, -246, 0xb4, 0xb5, 0xb6, 0xb7, -250, -256, -252, -254, 0xb8, 0xb9, 0xba, 0xc2, -258, -260, 0xc3, 0xc4,
+    /* 260 */ 0xc5, 0xc6, -264, -294, -266, -280, -268, -274, -270, -272, 0xc7, 0xc8, 0xc9, 0xca, -276, -278, 0xd2, 0xd3, 0xd4, 0xd5,
+    /* 280 */ -282, -288, -284, -286, 0xd6, 0xd7, 0xd8, 0xd9, -290, -292, 0xda, 0xe2, 0xe3, 0xe4, -296, -310, -298, -304, -300, -302,
+    /* 300 */ 0xe5, 0xe6, 0xe7, 0xe8, -306, -308, 0xe9, 0xea, 0xf2, 0xf3, -312, -318, -314, -316, 0xf4, 0xf5, 0xf6, 0xf7, -320, -322,
+    /* 320 */ 0xf8, 0xf9, 0xfa,   -1
+  };
+  *tables -> Huffman = memcpy(ctxmalloc(context, sizeof luminance_DC_table), luminance_DC_table, sizeof luminance_DC_table);
+  tables -> Huffman[1] = memcpy(ctxmalloc(context, sizeof chrominance_DC_table), chrominance_DC_table, sizeof chrominance_DC_table);
+  tables -> Huffman[4] = memcpy(ctxmalloc(context, sizeof luminance_AC_table), luminance_AC_table, sizeof luminance_AC_table);
+  tables -> Huffman[5] = memcpy(ctxmalloc(context, sizeof chrominance_AC_table), chrominance_AC_table, sizeof chrominance_AC_table);
+}
+
+void generate_JPEG_data (struct context * context) {
+  if (context -> source -> frames > 1) throw(context, PLUM_ERR_NO_MULTI_FRAME);
+  if (context -> source -> width > 0xffffu || context -> source -> height > 0xffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  byteoutput(context,
+             0xff, 0xd8, // SOI
+             0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, // JFIF marker (no thumbnail)
+             0xff, 0xee, 0x00, 0x0e, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x01, // Adobe marker (YCbCr colorspace)
+             0xff, 0xc0, 0x00, 0x11, 0x08, // SOF, baseline DCT coding, 8 bits per component...
+             context -> source -> height >> 8, context -> source -> height, context -> source -> width >> 8, context -> source -> width, // dimensions...
+             0x03, 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01 // 3 components, component 1 is 4:4:4, table 0, components 2-3 are 4:2:0, table 1
+            );
+  uint8_t luminance_table[64];
+  uint8_t chrominance_table[64];
+  calculate_JPEG_quantization_tables(context, luminance_table, chrominance_table);
+  unsigned char * node = append_output_node(context, 134);
+  bytewrite(node, 0xff, 0xdb, 0x00, 0x84, 0x00); // DQT, 132 bytes long, table 0 first
+  memcpy(node + 5, luminance_table, sizeof luminance_table);
+  node[69] = 1; // table 1 afterwards
+  memcpy(node + 70, chrominance_table, sizeof chrominance_table);
+  size_t unitsH = (context -> image -> width + 7) / 8, unitsV = (context -> image -> height + 7) / 8, units = unitsH * unitsV;
+  double (* luminance)[64] = ctxmalloc(context, units * sizeof *luminance);
+  double (* blue_chrominance)[64] = ctxmalloc(context, units * sizeof *blue_chrominance);
+  double (* red_chrominance)[64] = ctxmalloc(context, units * sizeof *red_chrominance);
+  convert_JPEG_components_to_YCbCr(context, luminance, blue_chrominance, red_chrominance);
+  size_t reduced_units = ((unitsH + 1) >> 1) * ((unitsV + 1) >> 1);
+  double (* buffer)[64] = ctxmalloc(context, reduced_units * sizeof *buffer);
+  subsample_JPEG_component(blue_chrominance, buffer, unitsH, unitsV);
+  ctxfree(context, blue_chrominance);
+  blue_chrominance = buffer;
+  buffer = ctxmalloc(context, reduced_units * sizeof *buffer);
+  subsample_JPEG_component(red_chrominance, buffer, unitsH, unitsV);
+  ctxfree(context, red_chrominance);
+  red_chrominance = buffer;
+  size_t luminance_count, chrominance_count;
+  // do chrominance first, since it will generally use less memory, so the chrominance data can be freed afterwards to reduce overall memory usage
+  struct JPEG_encoded_value * chrominance_data = generate_JPEG_chrominance_data_stream(context, blue_chrominance, red_chrominance, reduced_units,
+                                                                                       chrominance_table, &chrominance_count);
+  ctxfree(context, red_chrominance);
+  ctxfree(context, blue_chrominance);
+  struct JPEG_encoded_value * luminance_data = generate_JPEG_luminance_data_stream(context, luminance, units, luminance_table, &luminance_count);
+  ctxfree(context, luminance);
+  unsigned char Huffman_table_data[0x400]; // luminance DC, AC, chrominance DC, AC
+  node = append_output_node(context, 1096);
+  size_t size = 4;
+  size += generate_JPEG_Huffman_table(context, luminance_data, luminance_count, node + size, Huffman_table_data, 0x00);
+  size += generate_JPEG_Huffman_table(context, luminance_data, luminance_count, node + size, Huffman_table_data + 0x100, 0x10);
+  size += generate_JPEG_Huffman_table(context, chrominance_data, chrominance_count, node + size, Huffman_table_data + 0x200, 0x01);
+  size += generate_JPEG_Huffman_table(context, chrominance_data, chrominance_count, node + size, Huffman_table_data + 0x300, 0x11);
+  bytewrite(node, 0xff, 0xc4, (size - 2) >> 8, size - 2); // DHT
+  context -> output -> size = size;
+  byteoutput(context, 0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00); // SOS, component 1, table 0, not progressive
+  encode_JPEG_scan(context, luminance_data, luminance_count, Huffman_table_data);
+  ctxfree(context, luminance_data);
+  byteoutput(context, 0xff, 0xda, 0x00, 0x0a, 0x02, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00); // SOS, components 2-3, table 1, not progressive
+  encode_JPEG_scan(context, chrominance_data, chrominance_count, Huffman_table_data + 0x200);
+  ctxfree(context, chrominance_data);
+  byteoutput(context, 0xff, 0xd9); // EOI
+}
+
+void calculate_JPEG_quantization_tables (struct context * context, uint8_t luminance_table[restrict static 64], uint8_t chrominance_table[restrict static 64]) {
+  // start with the standard's tables (reduced by 1, since that will be added back later)
+  static const uint8_t luminance_base[64] =   { 15,  10,  11,  13,  11,   9,  15,  13,  12,  13,  17,  16,  15,  18,  23,  39,  25,  23,  21,  21,  23,
+                                                48,  34,  36,  28,  39,  57,  50,  60,  59,  56,  50,  55,  54,  63,  71,  91,  77,  63,  67,  86,  68,
+                                                54,  55,  79, 108,  80,  86,  94,  97, 102, 103, 102,  61,  76, 112, 120, 111,  99, 119,  91, 100, 102,  98};
+  static const uint8_t chrominance_base[64] = { 16,  17,  17,  23,  20,  23,  46,  25,  25,  46,  98,  65,  55,  65,  98,  98,  98,  98,  98,  98,  98,
+                                                98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,
+                                                98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98,  98};
+  // compute a score based on the logarithm of the image's dimensions (approximated using integer math)
+  uint_fast32_t current, score = 0;
+  for (current = context -> source -> width; current > 4; current >>= 1) score += 2;
+  score += current;
+  for (current = context -> source -> height; current > 4; current >>= 1) score += 2;
+  score += current;
+  score = (score > 24) ? score - 22 : 2;
+  // adjust the chrominance accuracy based on the color depth
+  uint_fast32_t depth = get_true_color_depth(context -> source);
+  uint_fast32_t adjustment = 72 - (depth & 0xff) - ((depth >> 8) & 0xff) - ((depth >> 16) & 0xff);
+  // compute the final quantization coefficients based on the scores above
+  for (uint_fast8_t p = 0; p < 64; p ++) {
+    luminance_table[p] = 1 + luminance_base[p] * score / 25;
+    chrominance_table[p] = 1 + chrominance_base[p] * score * adjustment / 1200;
+  }
+}
+
+void convert_JPEG_components_to_YCbCr (struct context * context, double (* restrict luminance)[64], double (* restrict blue)[64], double (* restrict red)[64]) {
+  const unsigned char * data = context -> source -> data;
+  size_t offset = context -> source -> palette ? 1 : plum_color_buffer_size(1, context -> source -> color_format), rowoffset = offset * context -> source -> width;
+  double palette_luminance[256];
+  double palette_blue[256];
+  double palette_red[256];
+  uint64_t * buffer = ctxmalloc(context, sizeof *buffer * ((context -> source -> palette && context -> source -> max_palette_index > 7) ?
+                                                           context -> source -> max_palette_index + 1 : 8));
+  // define macros to reduce repetition within the function
+  #define nextunit luminance ++, blue ++, red ++
+  #define convertblock(rows, cols) do                                                                                                                          \
+    if (context -> source -> palette)                                                                                                                          \
+      for (uint_fast8_t row = 0; row < (rows); row ++) for (uint_fast8_t col = 0; col < (cols); col ++) {                                                      \
+        unsigned char index = data[(unitrow * 8 + row) * context -> source -> width + unitcol * 8 + col], coord = row * 8 + col;                               \
+        coord[*luminance] = palette_luminance[index];                                                                                                          \
+        coord[*blue] = palette_blue[index];                                                                                                                    \
+        coord[*red] = palette_red[index];                                                                                                                      \
+      }                                                                                                                                                        \
+    else {                                                                                                                                                     \
+      size_t index = unitrow * 8 * rowoffset + unitcol * 8 * offset;                                                                                           \
+      for (uint_fast8_t row = 0; row < (rows); row ++, index += rowoffset)                                                                                     \
+        convert_JPEG_colors_to_YCbCr(data + index, cols, context -> source -> color_format, *luminance + 8 * row, *blue + 8 * row, *red + 8 * row, buffer);    \
+    }                                                                                                                                                          \
+  while (false)
+  #define copyvalues(index, offset) do {                     \
+    uint_fast8_t coord = (index), ref = coord - (offset);    \
+    coord[*luminance] = ref[*luminance];                     \
+    coord[*blue] = ref[*blue];                               \
+    coord[*red] = ref[*red];                                 \
+  } while (false)
+  // actually do the conversion
+  if (context -> source -> palette)
+    convert_JPEG_colors_to_YCbCr(context -> source -> palette, context -> source -> max_palette_index + 1, context -> source -> color_format, palette_luminance,
+                                 palette_blue, palette_red, buffer);
+  size_t unitrow, unitcol; // used by convertblock (and thus required to keep their values after the loops exit)
+  for (unitrow = 0; unitrow < (context -> source -> height >> 3); unitrow ++) {
+    for (unitcol = 0; unitcol < (context -> source -> width >> 3); unitcol ++) {
+      convertblock(8, 8);
+      nextunit;
+    }
+    if (context -> source -> width & 7) {
+      convertblock(8, context -> source -> width & 7);
+      for (uint_fast8_t row = 0; row < 8; row ++) for (uint_fast8_t col = context -> source -> width & 7; col < 8; col ++) copyvalues(row * 8 + col, 1);
+      nextunit;
+    }
+  }
+  if (context -> source -> height & 7) {
+    for (unitcol = 0; unitcol < (context -> source -> width >> 3); unitcol ++) {
+      convertblock(context -> source -> height & 7, 8);
+      for (uint_fast8_t p = 8 * (context -> source -> height & 7); p < 64; p ++) copyvalues(p, 8);
+      nextunit;
+    }
+    if (context -> source -> width & 7) {
+      convertblock(context -> source -> height & 7, context -> source -> width & 7);
+      for (uint_fast8_t row = 0; row < (context -> source -> height & 7); row ++) for (uint_fast8_t col = context -> source -> width & 7; col < 8; col ++)
+        copyvalues(row * 8 + col, 1);
+      for (uint_fast8_t p = 8 * (context -> source -> height & 7); p < 64; p ++) copyvalues(p, 8);
+    }
+  }
+  #undef copyvalues
+  #undef convertblock
+  #undef nextunit
+  ctxfree(context, buffer);
+}
+
+void convert_JPEG_colors_to_YCbCr (const void * restrict colors, size_t count, unsigned char flags, double * restrict luminance, double * restrict blue,
+                                   double * restrict red, uint64_t * restrict buffer) {
+  plum_convert_colors(buffer, colors, count, PLUM_COLOR_64, flags);
+  for (size_t p = 0; p < count; p ++) {
+    double R = (double) (buffer[p] & 0xffffu) / 257.0, G = (double) ((buffer[p] >> 16) & 0xffffu) / 257.0, B = (double) ((buffer[p] >> 32) & 0xffffu) / 257.0;
+    luminance[p] = 0x0.4c8b4395810628p+0 * R + 0x0.9645a1cac08310p+0 * G + 0x0.1d2f1a9fbe76c8p+0 * B - 128.0;
+    blue[p] = 0.5 * (B - 1.0) - 0x0.2b32468049f7e8p+0 * R - 0x0.54cdb97fb60818p+0 * G;
+    red[p] = 0.5 * (R - 1.0) - 0x0.6b2f1c1ead19ecp+0 * G - 0x0.14d0e3e152e614p+0 * B;
+  }
+}
+
+void subsample_JPEG_component (double (* restrict component)[64], double (* restrict output)[64], size_t unitsH, size_t unitsV) {
+  #define reduce(offset, shift, y, x) do {                                              \
+    uint_fast8_t index = (y) * 8 + (x);                                                 \
+    const double * ref = component[(offset) * unitsH] + (index * 2 - 64 * (offset));    \
+    (*output)[index + (shift)] = (*ref + ref[1] + ref[8] + ref[9]) * 0.25;              \
+  } while (false)
+  for (size_t unitrow = 0; unitrow < (unitsV >> 1); unitrow ++) {
+    for (size_t unitcol = 0; unitcol < (unitsH >> 1); unitcol ++) {
+      for (uint_fast8_t p = 0; p < 8; p += 4) {
+        for (uint_fast8_t row = 0; row < 4; row ++) for (uint_fast8_t col = 0; col < 4; col ++) {
+          reduce(0, p, row, col);
+          reduce(1, p, row + 4, col);
+        }
+        component ++;
+      }
+      output ++;
+    }
+    if (unitsH & 1) {
+      for (uint_fast8_t row = 0; row < 4; row ++) for (uint_fast8_t col = 0; col < 4; col ++) {
+        reduce(0, 0, row, col);
+        reduce(1, 0, row + 4, col);
+      }
+      component ++;
+      for (uint_fast8_t row = 0; row < 8; row ++) for (uint_fast8_t col = 4; col < 8; col ++) (*output)[row * 8 + col] = (*output)[row * 8 + col - 1];
+      output ++;
+    }
+    component += unitsH; // skip odd rows
+  }
+  if (unitsV & 1) {
+    for (size_t unitcol = 0; unitcol < (unitsH >> 1); unitcol ++) {
+      for (uint_fast8_t p = 0; p < 8; p += 4) {
+        for (uint_fast8_t row = 0; row < 4; row ++) for (uint_fast8_t col = 0; col < 4; col ++) reduce(0, p, row, col);
+        component ++;
+      }
+      for (uint_fast8_t p = 32; p < 64; p ++) (*output)[p] = (*output)[p - 8];
+      output ++;
+    }
+    if (unitsH & 1) {
+      for (uint_fast8_t row = 0; row < 4; row ++) for (uint_fast8_t col = 0; col < 4; col ++) {
+        reduce(0, 0, row, col);
+        (*output)[row * 8 + col + 4] = (*output)[row * 8 + col];
+      }
+      for (uint_fast8_t p = 32; p < 64; p ++) (*output)[p] = (*output)[p - 8];
+    }
+  }
+  #undef reduce
+}
+
+struct plum_image * plum_load_image (const void * restrict buffer, size_t size_mode, unsigned flags, unsigned * restrict error) {
+  return plum_load_image_limited(buffer, size_mode, flags, SIZE_MAX, error);
+}
+
+struct plum_image * plum_load_image_limited (const void * restrict buffer, size_t size_mode, unsigned flags, size_t limit, unsigned * restrict error) {
+  struct context * context = create_context();
+  if (!context) {
+    if (error) *error = PLUM_ERR_OUT_OF_MEMORY;
+    return NULL;
+  }
+  if (!setjmp(context -> target)) {
+    if (!buffer) throw(context, PLUM_ERR_INVALID_ARGUMENTS);
+    if (!(context -> image = plum_new_image())) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+    prepare_image_buffer_data(context, buffer, size_mode);
+    load_image_buffer_data(context, flags, limit);
+    if (flags & PLUM_ALPHA_REMOVE) plum_remove_alpha(context -> image);
+    if (flags & PLUM_PALETTE_GENERATE)
+      if (context -> image -> palette) {
+        int colors = plum_get_highest_palette_index(context -> image);
+        if (colors < 0) throw(context, -colors);
+        context -> image -> max_palette_index = colors;
+        update_loaded_palette(context, flags);
+      } else {
+        generate_palette(context, flags);
+        // PLUM_PALETTE_FORCE == PLUM_PALETTE_LOAD | PLUM_PALETTE_GENERATE
+        if (!(context -> image -> palette) && (flags & PLUM_PALETTE_LOAD)) throw(context, PLUM_ERR_TOO_MANY_COLORS);
+      }
+    else if (context -> image -> palette)
+      if ((flags & PLUM_PALETTE_MASK) == PLUM_PALETTE_NONE)
+        remove_palette(context);
+      else
+        update_loaded_palette(context, flags);
+  }
+  if (context -> file) fclose(context -> file);
+  if (error) *error = context -> status;
+  struct plum_image * image = context -> image;
+  if (context -> status) {
+    plum_destroy_image(image);
+    image = NULL;
+  }
+  destroy_allocator_list(context -> allocator);
+  return image;
+}
+
+void load_image_buffer_data (struct context * context, unsigned flags, size_t limit) {
+  if (context -> size == 7 && (bytematch(context -> data, 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x3b) ||
+                               bytematch(context -> data, 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x3b)))
+    // empty GIF file
+    throw(context, PLUM_ERR_NO_DATA);
+  if (context -> size < 8) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (bytematch(context -> data, 0x42, 0x4d))
+    load_BMP_data(context, flags, limit);
+  else if (bytematch(context -> data, 0x47, 0x49, 0x46, 0x38, 0x39, 0x61))
+    load_GIF_data(context, flags, limit);
+  else if (bytematch(context -> data, 0x47, 0x49, 0x46, 0x38, 0x37, 0x61))
+    // treat GIF87a as GIF89a for compatibility, since it's a strict subset anyway
+    load_GIF_data(context, flags, limit);
+  else if (bytematch(context -> data, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a))
+    // APNG files disguise as PNG files, so handle them all as PNG and split them later
+    load_PNG_data(context, flags, limit);
+  else if (*context -> data == 0x50 && context -> data[1] >= 0x31 && context -> data[1] <= 0x37)
+    load_PNM_data(context, flags, limit);
+  else if (bytematch(context -> data, 0xef, 0xbb, 0xbf, 0x50) && context -> data[4] >= 0x31 && context -> data[4] <= 0x37)
+    // text-based PNM data destroyed by a UTF-8 BOM: load it anyway, just in case a broken text editor does this
+    load_PNM_data(context, flags, limit);
+  else {
+    // JPEG detection: one or more 0xff bytes followed by 0xd8
+    size_t position;
+    for (position = 0; position < context -> size && context -> data[position] == 0xff; position ++);
+    if (position && position < context -> size && context -> data[position] == 0xd8)
+      load_JPEG_data(context, flags, limit);
+    else
+      // all attempts to detect the file type failed
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+}
+
+void prepare_image_buffer_data (struct context * context, const void * restrict buffer, size_t size_mode) {
+  switch (size_mode) {
+    case PLUM_MODE_FILENAME:
+      load_file(context, buffer);
+      return;
+    case PLUM_MODE_BUFFER:
+      context -> data = ((const struct plum_buffer *) buffer) -> data;
+      context -> size = ((const struct plum_buffer *) buffer) -> size;
+      if (!context -> data) throw(context, PLUM_ERR_INVALID_ARGUMENTS);
+      return;
+    case PLUM_MODE_CALLBACK:
+      load_from_callback(context, buffer);
+      return;
+    default:
+      context -> data = buffer;
+      context -> size = size_mode;
+  }
+}
+
+void load_file (struct context * context, const char * filename) {
+  context -> file = fopen(filename, "rb");
+  if (!context -> file) throw(context, PLUM_ERR_FILE_INACCESSIBLE);
+  size_t allocated;
+  char * buffer = resize_read_buffer(context, NULL, &allocated);
+  size_t size = fread(buffer, 1, allocated, context -> file);
+  if (ferror(context -> file)) throw(context, PLUM_ERR_FILE_ERROR);
+  while (!feof(context -> file)) {
+    if (allocated - size < 0x4000) buffer = resize_read_buffer(context, buffer, &allocated);
+    size += fread(buffer + size, 1, 0x4000, context -> file);
+    if (ferror(context -> file)) throw(context, PLUM_ERR_FILE_ERROR);
+  }
+  fclose(context -> file);
+  context -> file = NULL;
+  context -> data = ctxrealloc(context, buffer, size);
+  context -> size = size;
+}
+
+void load_from_callback (struct context * context, const struct plum_callback * callback) {
+  size_t allocated;
+  unsigned char * buffer = resize_read_buffer(context, NULL, &allocated);
+  int iteration = callback -> callback(callback -> userdata, buffer, 0x4000 - sizeof(struct allocator_node));
+  if (iteration < 0 || iteration > 0x4000 - sizeof(struct allocator_node)) throw(context, PLUM_ERR_FILE_ERROR);
+  context -> size = iteration;
+  while (iteration) {
+    if (allocated - context -> size < 0x4000) buffer = resize_read_buffer(context, buffer, &allocated);
+    iteration = callback -> callback(callback -> userdata, buffer + context -> size, 0x4000);
+    if (iteration < 0 || iteration > 0x4000) throw(context, PLUM_ERR_FILE_ERROR);
+    context -> size += iteration;
+  }
+  context -> data = buffer;
+}
+
+void * resize_read_buffer (struct context * context, void * buffer, size_t * restrict allocated) {
+  // will set the buffer to its initial size on first call (buffer = NULL, allocated = ignored), or extend it on further calls
+  if (buffer)
+    if (*allocated < 0x20000u - sizeof(struct allocator_node))
+      *allocated += 0x4000;
+    else
+      *allocated += (size_t) 0x4000 << (bit_width(*allocated + sizeof(struct allocator_node)) - 17);
+  else
+    *allocated = 0x4000 - sizeof(struct allocator_node); // keep the buffer aligned to 4K memory pages
+  return ctxrealloc(context, buffer, *allocated);
+}
+
+void update_loaded_palette (struct context * context, unsigned flags) {
+  if (flags & PLUM_SORT_EXISTING) sort_palette(context -> image, flags);
+  if (flags & PLUM_PALETTE_REDUCE) {
+    reduce_palette(context -> image);
+    context -> image -> palette = plum_realloc(context -> image, context -> image -> palette, plum_palette_buffer_size(context -> image));
+    if (!context -> image -> palette) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  }
+}
+
+struct plum_metadata * plum_allocate_metadata (struct plum_image * image, size_t size) {
+  struct {
+    struct plum_metadata result;
+    alignas(max_align_t) unsigned char data[];
+  } * result = plum_malloc(image, sizeof *result + size);
+  if (result) result -> result = (struct plum_metadata) {
+    .type = PLUM_METADATA_NONE,
+    .size = size,
+    .data = result -> data,
+    .next = NULL
+  };
+  return (struct plum_metadata *) result;
+}
+
+unsigned plum_append_metadata (struct plum_image * image, int type, const void * data, size_t size) {
+  if (!image || (size && !data)) return PLUM_ERR_INVALID_ARGUMENTS;
+  struct plum_metadata * metadata = plum_allocate_metadata(image, size);
+  if (!metadata) return PLUM_ERR_OUT_OF_MEMORY;
+  metadata -> type = type;
+  if (size) memcpy(metadata -> data, data, size);
+  metadata -> next = image -> metadata;
+  image -> metadata = metadata;
+  return 0;
+}
+
+struct plum_metadata * plum_find_metadata (const struct plum_image * image, int type) {
+  if (!image) return NULL;
+  for (struct plum_metadata * metadata = (struct plum_metadata *) image -> metadata; metadata; metadata = metadata -> next)
+    if (metadata -> type == type) return metadata;
+  return NULL;
+}
+
+void add_color_depth_metadata (struct context * context, unsigned red, unsigned green, unsigned blue, unsigned alpha, unsigned gray) {
+  unsigned char counts[] = {red, green, blue, alpha, gray};
+  unsigned result = plum_append_metadata(context -> image, PLUM_METADATA_COLOR_DEPTH, counts, sizeof counts);
+  if (result) throw(context, result);
+}
+
+void add_background_color_metadata (struct context * context, uint64_t color, unsigned flags) {
+  color = plum_convert_color(color, PLUM_COLOR_64, flags);
+  size_t size = plum_color_buffer_size(1, flags);
+  struct plum_metadata * metadata = plum_allocate_metadata(context -> image, size);
+  if (!metadata) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  metadata -> type = PLUM_METADATA_BACKGROUND;
+  if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    *(uint64_t *) (metadata -> data) = color;
+  else if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    *(uint16_t *) (metadata -> data) = color;
+  else
+    *(uint32_t *) (metadata -> data) = color;
+  metadata -> next = context -> image -> metadata;
+  context -> image -> metadata = metadata;
+}
+
+void add_loop_count_metadata (struct context * context, uint32_t count) {
+  unsigned result = plum_append_metadata(context -> image, PLUM_METADATA_LOOP_COUNT, &count, sizeof count);
+  if (result) throw(context, result);
+}
+
+void add_animation_metadata (struct context * context, uint64_t ** restrict durations, uint8_t ** restrict disposals) {
+  struct plum_metadata * durations_metadata = plum_allocate_metadata(context -> image, sizeof **durations * context -> image -> frames);
+  struct plum_metadata * disposals_metadata = plum_allocate_metadata(context -> image, sizeof **disposals * context -> image -> frames);
+  if (!(durations_metadata && disposals_metadata)) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  memset(*durations = durations_metadata -> data, 0, durations_metadata -> size);
+  memset(*disposals = disposals_metadata -> data, 0, disposals_metadata -> size);
+  durations_metadata -> type = PLUM_METADATA_FRAME_DURATION;
+  disposals_metadata -> type = PLUM_METADATA_FRAME_DISPOSAL;
+  durations_metadata -> next = disposals_metadata;
+  disposals_metadata -> next = context -> image -> metadata;
+  context -> image -> metadata = durations_metadata;
+}
+
+struct plum_rectangle * add_frame_area_metadata (struct context * context) {
+  if (context -> image -> frames > SIZE_MAX / sizeof(struct plum_rectangle)) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  struct plum_metadata * metadata = plum_allocate_metadata(context -> image, sizeof(struct plum_rectangle) * context -> image -> frames);
+  if (!metadata) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  metadata -> type = PLUM_METADATA_FRAME_AREA;
+  metadata -> next = context -> image -> metadata;
+  context -> image -> metadata = metadata;
+  return metadata -> data;
+}
+
+uint64_t get_empty_color (const struct plum_image * image) {
+  uint64_t result, mask = alpha_component_masks[image -> color_format & PLUM_COLOR_MASK];
+  const struct plum_metadata * background = plum_find_metadata(image, PLUM_METADATA_BACKGROUND);
+  if (!background)
+    result = 0;
+  else if ((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    result = *(const uint64_t *) background -> data;
+  else if ((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    result = *(const uint16_t *) background -> data;
+  else
+    result = *(const uint32_t *) background -> data;
+  return (image -> color_format & PLUM_ALPHA_INVERT) ? result & ~mask : result | mask;
+}
+
+unsigned plum_validate_image (const struct plum_image * image) {
+  if (!image) return PLUM_ERR_INVALID_ARGUMENTS;
+  if (!(image -> width && image -> height && image -> frames && image -> data)) return PLUM_ERR_NO_DATA;
+  if (!plum_check_valid_image_size(image -> width, image -> height, image -> frames)) return PLUM_ERR_IMAGE_TOO_LARGE;
+  if (image -> type >= PLUM_NUM_IMAGE_TYPES) return PLUM_ERR_INVALID_FILE_FORMAT;
+  bool found[PLUM_NUM_METADATA_TYPES - 1] = {0};
+  for (const struct plum_metadata * metadata = image -> metadata; metadata; metadata = metadata -> next) {
+    if (metadata -> size && !metadata -> data) return PLUM_ERR_INVALID_METADATA;
+    if (metadata -> type <= 0) continue;
+    if (metadata -> type >= PLUM_NUM_METADATA_TYPES || found[metadata -> type - 1]) return PLUM_ERR_INVALID_METADATA;
+    found[metadata -> type - 1] = true;
+    switch (metadata -> type) {
+      case PLUM_METADATA_COLOR_DEPTH:
+        if (metadata -> size < 3 || metadata -> size > 5) return PLUM_ERR_INVALID_METADATA;
+        break;
+      case PLUM_METADATA_BACKGROUND:
+        if (metadata -> size != plum_color_buffer_size(1, image -> color_format)) return PLUM_ERR_INVALID_METADATA;
+        break;
+      case PLUM_METADATA_LOOP_COUNT:
+        if (metadata -> size != sizeof(uint32_t)) return PLUM_ERR_INVALID_METADATA;
+        break;
+      case PLUM_METADATA_FRAME_DURATION:
+        if (metadata -> size % sizeof(uint64_t)) return PLUM_ERR_INVALID_METADATA;
+        break;
+      case PLUM_METADATA_FRAME_DISPOSAL:
+        for (size_t p = 0; p < metadata -> size; p ++) if (p[(const uint8_t *) metadata -> data] >= PLUM_NUM_DISPOSAL_METHODS) return PLUM_ERR_INVALID_METADATA;
+        break;
+      case PLUM_METADATA_FRAME_AREA: {
+        const struct plum_rectangle * rectangles = metadata -> data;
+        if (metadata -> size % sizeof *rectangles) return PLUM_ERR_INVALID_METADATA;
+        uint_fast32_t frames = (image -> frames > metadata -> size / sizeof *rectangles) ? metadata -> size / sizeof *rectangles : image -> frames;
+        for (uint_fast32_t frame = 0; frame < frames; frame ++) {
+          if (!(rectangles[frame].width && rectangles[frame].height)) return PLUM_ERR_INVALID_METADATA;
+          uint32_t right = rectangles[frame].left + rectangles[frame].width, bottom = rectangles[frame].top + rectangles[frame].height;
+          if (right < rectangles[frame].left || right > image -> width || bottom < rectangles[frame].top || bottom > image -> height)
+            return PLUM_ERR_INVALID_METADATA;
+        }
+      }
+    }
+  }
+  return 0;
+}
+
+const char * plum_get_error_text (unsigned error) {
+  static const char * const messages[PLUM_NUM_ERRORS] = {
+    [PLUM_OK]                      = "success",
+    [PLUM_ERR_INVALID_ARGUMENTS]   = "invalid argument for function",
+    [PLUM_ERR_INVALID_FILE_FORMAT] = "invalid image data or unknown format",
+    [PLUM_ERR_INVALID_METADATA]    = "invalid image metadata",
+    [PLUM_ERR_INVALID_COLOR_INDEX] = "invalid palette index",
+    [PLUM_ERR_TOO_MANY_COLORS]     = "too many colors in image",
+    [PLUM_ERR_UNDEFINED_PALETTE]   = "image palette not defined",
+    [PLUM_ERR_IMAGE_TOO_LARGE]     = "image dimensions too large",
+    [PLUM_ERR_NO_DATA]             = "image contains no image data",
+    [PLUM_ERR_NO_MULTI_FRAME]      = "multiple frames not supported",
+    [PLUM_ERR_FILE_INACCESSIBLE]   = "could not access file",
+    [PLUM_ERR_FILE_ERROR]          = "file input/output error",
+    [PLUM_ERR_OUT_OF_MEMORY]       = "out of memory"
+  };
+  if (error >= PLUM_NUM_ERRORS) return NULL;
+  return messages[error];
+}
+
+const char * plum_get_file_format_name (unsigned format) {
+  static const char * const formats[PLUM_NUM_IMAGE_TYPES] = {
+    [PLUM_IMAGE_NONE] = NULL, // default for invalid formats
+    [PLUM_IMAGE_BMP]  = "BMP",
+    [PLUM_IMAGE_GIF]  = "GIF",
+    [PLUM_IMAGE_PNG]  = "PNG",
+    [PLUM_IMAGE_APNG] = "APNG",
+    [PLUM_IMAGE_JPEG] = "JPEG",
+    [PLUM_IMAGE_PNM]  = "PNM"
+  };
+  if (format >= PLUM_NUM_IMAGE_TYPES) format = PLUM_IMAGE_NONE;
+  return formats[format];
+}
+
+uint32_t plum_get_version_number (void) {
+  return PLUM_VERSION;
+}
+
+struct plum_image * plum_new_image (void) {
+  struct allocator_node * allocator = NULL;
+  struct plum_image * image = allocate(&allocator, sizeof *image);
+  if (image) *image = (struct plum_image) {.allocator = allocator}; // zero-initialize all other members
+  return image;
+}
+
+struct plum_image * plum_copy_image (const struct plum_image * image) {
+  if (!(image && image -> data)) return NULL;
+  struct plum_image * copy = plum_new_image();
+  if (!copy) return NULL;
+  copy -> type = image -> type;
+  copy -> max_palette_index = image -> max_palette_index;
+  copy -> color_format = image -> color_format;
+  copy -> frames = image -> frames;
+  copy -> height = image -> height;
+  copy -> width = image -> width;
+  copy -> userdata = image -> userdata;
+  if (image -> metadata) {
+    const struct plum_metadata * current = image -> metadata;
+    struct plum_metadata * allocated = plum_allocate_metadata(copy, current -> size);
+    if (!allocated) goto fail;
+    allocated -> type = current -> type;
+    memcpy(allocated -> data, current -> data, current -> size);
+    struct plum_metadata * last = copy -> metadata = allocated;
+    while (current = current -> next) {
+      allocated = plum_allocate_metadata(copy, current -> size);
+      if (!allocated) goto fail;
+      allocated -> type = current -> type;
+      memcpy(allocated -> data, current -> data, current -> size);
+      last -> next = allocated;
+      last = allocated;
+    }
+  }
+  if (image -> width && image -> height && image -> frames) {
+    size_t size = plum_pixel_buffer_size(image);
+    if (!size) goto fail;
+    void * buffer = plum_malloc(copy, size);
+    if (!buffer) goto fail;
+    memcpy(buffer, image -> data, size);
+    copy -> data = buffer;
+  }
+  if (image -> palette) {
+    size_t size = plum_palette_buffer_size(image);
+    void * buffer = plum_malloc(copy, size);
+    if (!buffer) goto fail;
+    memcpy(buffer, image -> palette, size);
+    copy -> palette = buffer;
+  }
+  return copy;
+  fail:
+  plum_destroy_image(copy);
+  return NULL;
+}
+
+void plum_destroy_image (struct plum_image * image) {
+  if (!image) return;
+  struct allocator_node * allocator = image -> allocator;
+  image -> allocator = NULL;
+  destroy_allocator_list(allocator);
+}
+
+struct context * create_context (void) {
+  struct allocator_node * allocator = NULL;
+  struct context * context = NULL;
+  if (alignof(jmp_buf) > alignof(max_align_t)) {
+    // this is the odd case where jmp_buf requires a stricter alignment than malloc is guaranteed to enforce
+    size_t skip = (alignof(jmp_buf) - 1) / sizeof *allocator + 1;
+    allocator = aligned_alloc(alignof(jmp_buf), skip * sizeof *allocator + sizeof *context);
+    if (allocator) {
+      allocator -> next = allocator -> previous = NULL;
+      // due to the special offset, the context itself cannot be ctxrealloc'd or ctxfree'd, but that never happens
+      context = (struct context *) (allocator -> data + (skip - 1) * sizeof *allocator);
+    }
+  } else
+    // normal case: malloc already returns a suitably-aligned pointer
+    context = allocate(&allocator, sizeof *context);
+  if (context) *context = (struct context) {.allocator = allocator};
+  return context;
+}
+
+void generate_palette (struct context * context, unsigned flags) {
+  size_t count = (size_t) context -> image -> width * context -> image -> height * context -> image -> frames;
+  void * palette = plum_malloc(context -> image, plum_color_buffer_size(0x100, context -> image -> color_format));
+  uint8_t * indexes = plum_malloc(context -> image, count);
+  if (!(palette || indexes)) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  int result = plum_convert_colors_to_indexes(indexes, context -> image -> data, palette, count, flags);
+  if (result >= 0) {
+    plum_free(context -> image, context -> image -> data);
+    context -> image -> data = indexes;
+    context -> image -> max_palette_index = result;
+    context -> image -> palette = plum_realloc(context -> image, palette, plum_color_buffer_size(result + 1, context -> image -> color_format));
+    if (!context -> image -> palette) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  } else if (result == -PLUM_ERR_TOO_MANY_COLORS) {
+    plum_free(context -> image, palette);
+    plum_free(context -> image, indexes);
+  } else
+    throw(context, -result);
+}
+
+void remove_palette (struct context * context) {
+  size_t count = (size_t) context -> image -> width * context -> image -> height * context -> image -> frames;
+  void * buffer = plum_malloc(context -> image, plum_color_buffer_size(count, context -> image -> color_format));
+  if (!buffer) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+  plum_convert_indexes_to_colors(buffer, context -> image -> data8, context -> image -> palette, count, context -> image -> color_format);
+  plum_free(context -> image, context -> image -> data8);
+  plum_free(context -> image, context -> image -> palette);
+  context -> image -> data = buffer;
+  context -> image -> palette = NULL;
+  context -> image -> max_palette_index = 0;
+}
+
+unsigned plum_sort_palette (struct plum_image * image, unsigned flags) {
+  unsigned result = check_image_palette(image);
+  if (!result) sort_palette(image, image -> color_format | (flags & PLUM_SORT_DARK_FIRST));
+  return result;
+}
+
+unsigned plum_sort_palette_custom (struct plum_image * image, uint64_t (* callback) (void *, uint64_t), void * argument, unsigned flags) {
+  if (!callback) return PLUM_ERR_INVALID_ARGUMENTS;
+  unsigned error = check_image_palette(image);
+  if (error) return error;
+  struct pair sortdata[0x100];
+  #define filldata(bits) do                                                                                         \
+    for (uint_fast16_t p = 0; p <= image -> max_palette_index; p ++) sortdata[p] = (struct pair) {                  \
+      .value = p,                                                                                                   \
+      .index = callback(argument, plum_convert_color(image -> palette ## bits[p], image -> color_format, flags))    \
+    };                                                                                                              \
+  while (false)
+  if ((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    filldata(64);
+  else if ((image -> color_format & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    filldata(16);
+  else
+    filldata(32);
+  #undef filldata
+  sort_pairs(sortdata, image -> max_palette_index + 1);
+  uint8_t sorted[0x100];
+  for (uint_fast16_t p = 0; p <= image -> max_palette_index; p ++) sorted[sortdata[p].value] = p;
+  apply_sorted_palette(image, image -> color_format, sorted);
+  return 0;
+}
+
+void sort_palette (struct plum_image * image, unsigned flags) {
+  uint8_t indexes[0x100];
+  plum_sort_colors(image -> palette, image -> max_palette_index, flags, indexes);
+  uint8_t sorted[0x100];
+  for (uint_fast16_t p = 0; p <= image -> max_palette_index; p ++) sorted[indexes[p]] = p;
+  apply_sorted_palette(image, flags, sorted);
+}
+
+void apply_sorted_palette (struct plum_image * image, unsigned flags, const uint8_t * sorted) {
+  size_t limit = (size_t) image -> width * image -> height * image -> frames;
+  for (size_t p = 0; p < limit; p ++) image -> data8[p] = sorted[image -> data8[p]];
+  #define sortpalette(bits) do {                                                                                         \
+    uint ## bits ## _t colors[0x100];                                                                                    \
+    for (uint_fast16_t p = 0; p <= image -> max_palette_index; p ++) colors[sorted[p]] = image -> palette ## bits[p];    \
+    memcpy(image -> palette ## bits, colors, (image -> max_palette_index + 1) * sizeof *colors);                         \
+  } while (false)
+  if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    sortpalette(64);
+  else if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    sortpalette(16);
+  else
+    sortpalette(32);
+  #undef sortpalette
+}
+
+unsigned plum_reduce_palette (struct plum_image * image) {
+  unsigned result = check_image_palette(image);
+  if (!result) reduce_palette(image);
+  return result;
+}
+
+void reduce_palette (struct plum_image * image) {
+  // convert all colors to 64-bit for consistent handling: converting up to 64-bit and later back to the original format is lossless
+  uint64_t colors[0x100];
+  plum_convert_colors(colors, image -> palette, image -> max_palette_index + 1, PLUM_COLOR_64, image -> color_format);
+  // expand from an array of colors to an interleaved array of indexes and colors (for sorting)
+  struct pair sorted[0x100];
+  for (uint_fast16_t p = 0; p <= image -> max_palette_index; p ++) sorted[p] = (struct pair) {.value = p, .index = colors[p]};
+  // mark all colors in the image as in use
+  bool used[0x100] = {0};
+  size_t size = (size_t) image -> width * image -> height * image -> frames;
+  for (size_t p = 0; p < size; p ++) used[image -> data8[p]] = true;
+  // sort the colors and check for duplicates; if duplicates are found, mark the duplicates as unused and the originals as in use
+  sort_pairs(sorted, image -> max_palette_index + 1);
+  for (uint_fast8_t p = image -> max_palette_index; p; p --) if (sorted[p].index == sorted[p - 1].index) {
+    used[sorted[p - 1].value] |= used[sorted[p].value];
+    used[sorted[p].value] = false;
+  }
+  // create a mapping of colors (in the colors array) to indexes; colors in use (after duplicates were marked unused) get their own index
+  // colors marked unused point to the previous color in use; this will deduplicate the colors, as duplicates come right after the originals
+  // actually unused colors will get mapped to nonsensical indexes, but they don't matter, since they don't appear in the image
+  uint8_t map[0x100];
+  uint_fast8_t ref = 0; // initialize it to avoid reading an uninitialized variable in the loop (even though the copied value will never be used)
+  for (uint_fast16_t p = 0; p <= image -> max_palette_index; p ++)
+    if (used[sorted[p].value])
+      ref = map[sorted[p].value] = sorted[p].value;
+    else
+      map[sorted[p].value] = ref;
+  // update the mapping table to preserve the order of the colors in the original palette, and generate the reduced palette (in the colors array)
+  ref = 0;
+  for (uint_fast16_t p = 0; p <= image -> max_palette_index; p ++)
+    if (used[p]) {
+      map[p] = ref;
+      colors[ref ++] = colors[p];
+    } else
+      map[p] = map[map[p]];
+  // update the image's palette (including the max_palette_index member) and data
+  image -> max_palette_index = ref - 1;
+  plum_convert_colors(image -> palette, colors, ref, image -> color_format, PLUM_COLOR_64);
+  for (size_t p = 0; p < size; p ++) image -> data8[p] = map[image -> data8[p]];
+}
+
+unsigned check_image_palette (const struct plum_image * image) {
+  unsigned result = plum_validate_image(image);
+  if (result) return result;
+  if (!image -> palette) return PLUM_ERR_UNDEFINED_PALETTE;
+  if (plum_validate_palette_indexes(image)) return PLUM_ERR_INVALID_COLOR_INDEX;
+  return 0;
+}
+
+const uint8_t * plum_validate_palette_indexes (const struct plum_image * image) {
+  // NULL if OK, address of first error if failed
+  if (!(image && image -> palette)) return NULL;
+  if (image -> max_palette_index == 0xff) return NULL;
+  size_t count = (size_t) image -> width * image -> height * image -> frames;
+  for (const uint8_t * ptr = image -> data8; count; ptr ++, count --) if (*ptr > image -> max_palette_index) return ptr;
+  return NULL;
+}
+
+int plum_get_highest_palette_index (const struct plum_image * image) {
+  int result = plum_validate_image(image);
+  if (result) return -result;
+  if (!image -> palette) return -PLUM_ERR_UNDEFINED_PALETTE;
+  // result is already initialized to 0
+  size_t count = (size_t) image -> width * image -> height * image -> frames;
+  for (size_t p = 0; p < count; p ++) if (image -> data8[p] > result) result = image -> data8[p];
+  return result;
+}
+
+int plum_convert_colors_to_indexes (uint8_t * restrict destination, const void * restrict source, void * restrict palette, size_t count, unsigned flags) {
+  if (!(destination && source && palette && count)) return -PLUM_ERR_INVALID_ARGUMENTS;
+  uint64_t * colors = malloc(0x800 * sizeof *colors);
+  uint64_t * sorted = malloc(0x100 * sizeof *sorted);
+  uint8_t * counts = calloc(0x100, sizeof *counts);
+  uint16_t * indexes = malloc(count * sizeof *indexes);
+  int result = -PLUM_ERR_TOO_MANY_COLORS; // default result (which will be returned if generating the color table fails)
+  if (!(colors && sorted && counts && indexes)) {
+    result = -PLUM_ERR_OUT_OF_MEMORY;
+    goto fail;
+  }
+  const unsigned char * sp = source;
+  unsigned total = 0, offset = plum_color_buffer_size(1, flags);
+  // first, store each color in a temporary hash table, and store the index into that table for each pixel
+  for (size_t pos = 0; pos < count; pos ++, sp += offset) {
+    uint64_t color;
+    if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+      color = *(const uint64_t *) sp;
+    else if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+      color = *(const uint16_t *) sp;
+    else
+      color = *(const uint32_t *) sp;
+    uint_fast16_t index;
+    unsigned char slot, hash = 0;
+    for (uint_fast8_t p = 0; p < sizeof color; p ++) hash += (color >> (p * 8)) * (6 * p + 17);
+    for (slot = 0; slot < (counts[hash] & 7); slot ++) {
+      index = (hash << 3) | slot;
+      if (colors[index] == color) goto found;
+    }
+    if (slot < 7)
+      counts[hash] ++; // that hash code doesn't have all seven slots occupied: use the next free one and increase the count for the hash code
+    else {
+      // all seven slots for that hash code are occupied: check the overflow section, and if the color is not there either, store it there
+      // the hash now becomes the index into the overflow section (must be unsigned char for its overflow behavior)
+      for (; counts[hash] & 0x80; hash ++) {
+        index = (hash << 3) | 7; // slot == 7 here
+        if (colors[index] == color) goto found;
+      }
+      counts[hash] |= 0x80; // mark the overflow slot for that hash code as in use
+    }
+    if (total >= 0x100) goto fail;
+    total ++;
+    index = (hash << 3) | slot;
+    colors[index] = color;
+    found:
+    indexes[pos] = index;
+  }
+  // then, compute a sorted color list (without gaps) to build the actual palette
+  uint64_t * cc = sorted;
+  for (uint_fast16_t pos = 0; pos < 0x100; pos ++) {
+    uint_fast16_t index = pos << 3;
+    for (uint_fast8_t p = 0; p < (counts[pos] & 7); p ++, index ++)
+      *(cc ++) = (get_color_sorting_score(colors[index], flags) << 11) | index;
+    if (counts[pos] & 0x80) {
+      index |= 7;
+      *(cc ++) = (get_color_sorting_score(colors[index], flags) << 11) | index;
+    }
+  }
+  sort_values(sorted, total);
+  // afterwards, write the actual palette, and replace the colors with indexes into it
+  #define copypalette(bits) do {                   \
+    uint ## bits ## _t * pp = palette;             \
+    for (size_t pos = 0; pos < total; pos ++) {    \
+      *(pp ++) = colors[sorted[pos] & 0x7ff];      \
+      colors[sorted[pos] & 0x7ff] = pos;           \
+    }                                              \
+  } while (false)
+  if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    copypalette(64);
+  else if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    copypalette(16);
+  else
+    copypalette(32);
+  #undef copypalette
+  // and finally, write out the color indexes to the frame buffer
+  for (size_t pos = 0; pos < count; pos ++) destination[pos] = colors[indexes[pos]];
+  result = total - 1;
+  fail:
+  free(indexes);
+  free(counts);
+  free(sorted);
+  free(colors);
+  return result;
+}
+
+uint64_t get_color_sorting_score (uint64_t color, unsigned flags) {
+  color = plum_convert_color(color, flags, PLUM_COLOR_64 | PLUM_ALPHA_INVERT);
+  uint64_t red = color & 0xffffu, green = (color >> 16) & 0xffffu, blue = (color >> 32) & 0xffffu, alpha = color >> 48;
+  uint64_t luminance = red * 299 + green * 587 + blue * 114; // 26 bits
+  if (flags & PLUM_SORT_DARK_FIRST) luminance ^= 0x3ffffffu;
+  uint64_t sum = red + green + blue; // 18 bits
+  return ~((luminance << 27) | (sum << 9) | (alpha >> 7)); // total: 53 bits
+}
+
+void plum_convert_indexes_to_colors (void * restrict destination, const uint8_t * restrict source, const void * restrict palette, size_t count, unsigned flags) {
+  if (!(destination && source && palette)) return;
+  if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_16) {
+    uint16_t * dp = destination;
+    const uint16_t * pal = palette;
+    while (count --) *(dp ++) = pal[*(source ++)];
+  } else if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64) {
+    uint64_t * dp = destination;
+    const uint64_t * pal = palette;
+    while (count --) *(dp ++) = pal[*(source ++)];
+  } else {
+    uint32_t * dp = destination;
+    const uint32_t * pal = palette;
+    while (count --) *(dp ++) = pal[*(source ++)];
+  }
+}
+
+void plum_sort_colors (const void * restrict colors, uint8_t max_index, unsigned flags, uint8_t * restrict result) {
+  // returns the ordered color indexes
+  if (!(colors && result)) return;
+  uint64_t keys[0x100]; // allocate on stack to avoid dealing with malloc() failure
+  if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_64)
+    for (uint_fast16_t p = 0; p <= max_index; p ++) keys[p] = p | (get_color_sorting_score(p[(const uint64_t *) colors], flags) << 8);
+  else if ((flags & PLUM_COLOR_MASK) == PLUM_COLOR_16)
+    for (uint_fast16_t p = 0; p <= max_index; p ++) keys[p] = p | (get_color_sorting_score(p[(const uint16_t *) colors], flags) << 8);
+  else
+    for (uint_fast16_t p = 0; p <= max_index; p ++) keys[p] = p | (get_color_sorting_score(p[(const uint32_t *) colors], flags) << 8);
+  sort_values(keys, max_index + 1);
+  for (uint_fast16_t p = 0; p <= max_index; p ++) result[p] = keys[p];
+}
+
+#define PNG_MAX_LOOKBACK_COUNT 64
+
+unsigned char * compress_PNG_data (struct context * context, const unsigned char * restrict data, size_t size, size_t extra, size_t * restrict output_size) {
+  // extra is the number of zero bytes inserted before the compressed data; they are not included in the size
+  unsigned char * output = ctxmalloc(context, extra + 8); // two bytes extra to handle leftover bits in dataword
+  memset(output, 0, extra);
+  size_t inoffset = 0, outoffset = extra + byteappend(output + extra, 0x78, 0x5e);
+  uint16_t * references = ctxmalloc(context, sizeof *references * 0x8000u * PNG_MAX_LOOKBACK_COUNT);
+  for (size_t p = 0; p < (size_t) 0x8000u * PNG_MAX_LOOKBACK_COUNT; p ++) references[p] = 0xffffu;
+  uint32_t dataword = 0;
+  uint8_t bits = 0;
+  bool force = false;
+  while (inoffset < size) {
+    size_t blocksize, count;
+    struct compressed_PNG_code * compressed = generate_compressed_PNG_block(context, data, inoffset, size, references, &blocksize, &count, force);
+    force = false;
+    if (compressed) {
+      inoffset += blocksize;
+      if (inoffset == size) dataword |= 1u << bits;
+      bits ++;
+      unsigned char * compressed_data = emit_PNG_compressed_block(context, compressed, count, count >= 16, &blocksize, &dataword, &bits);
+      if (SIZE_MAX - outoffset < blocksize + 6) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+      output = ctxrealloc(context, output, outoffset + blocksize + 6);
+      memcpy(output + outoffset, compressed_data, blocksize);
+      ctxfree(context, compressed_data);
+      outoffset += blocksize;
+    }
+    if (inoffset >= size) break;
+    blocksize = compute_uncompressed_PNG_block_size(data, inoffset, size, references);
+    if (blocksize >= 32) {
+      if (blocksize > 0xffffu) blocksize = 0xffffu;
+      if (inoffset + blocksize == size) dataword |= 1u << bits;
+      bits += 3;
+      while (bits) {
+        output[outoffset ++] = dataword;
+        dataword >>= 8;
+        bits = (bits >= 8) ? bits - 8 : 0;
+      }
+      if (SIZE_MAX - outoffset < blocksize + 10) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+      output = ctxrealloc(context, output, outoffset + blocksize + 10);
+      write_le16_unaligned(output + outoffset, blocksize);
+      write_le16_unaligned(output + outoffset + 2, 0xffffu - blocksize);
+      memcpy(output + outoffset + 4, data + inoffset, blocksize);
+      outoffset += blocksize + 4;
+      inoffset += blocksize;
+    } else
+      force = true;
+  }
+  ctxfree(context, references);
+  while (bits) {
+    output[outoffset ++] = dataword;
+    dataword >>= 8;
+    bits = (bits >= 8) ? bits - 8 : 0;
+  }
+  write_be32_unaligned(output + outoffset, compute_Adler32_checksum(data, size));
+  *output_size = outoffset + 4 - extra;
+  return output;
+}
+
+struct compressed_PNG_code * generate_compressed_PNG_block (struct context * context, const unsigned char * restrict data, size_t offset, size_t size,
+                                                            uint16_t * restrict references, size_t * restrict blocksize, size_t * restrict count, bool force) {
+  size_t backref, current_offset = offset, allocated = 256;
+  struct compressed_PNG_code * codes = ctxmalloc(context, allocated * sizeof *codes);
+  *count = 0;
+  int literals = 0, score = 0;
+  while (size - current_offset >= 3 && size - current_offset < (SIZE_MAX >> 4)) {
+    unsigned length = find_PNG_reference(data, references, current_offset, size, &backref);
+    if (length) {
+      // we found a matching back reference, so emit any pending literals and the reference
+      for (; literals; literals --) emit_PNG_code(context, &codes, &allocated, count, data[current_offset - literals], 0);
+      emit_PNG_code(context, &codes, &allocated, count, -(int) length, current_offset - backref);
+      score -= length - 1;
+      if (score < 0) score = 0;
+      for (; length; length --) append_PNG_reference(data, current_offset ++, references);
+    } else {
+      // no back reference: increase the pending literal count, and stop compressing data if a threshold is exceeded
+      literals ++;
+      score ++;
+      append_PNG_reference(data, current_offset ++, references);
+      if (score >= 64)
+        if (force && *count < 16)
+          score = 0;
+        else
+          break;
+    }
+  }
+  if (size - current_offset < 3) {
+    literals += size - current_offset;
+    current_offset = size;
+  }
+  *blocksize = current_offset - offset;
+  if ((force && *blocksize < 32) || (*blocksize >= 32 && score < 64))
+    for (; literals; literals --) emit_PNG_code(context, &codes, &allocated, count, data[current_offset - literals], 0);
+  else
+    *blocksize -= literals;
+  if (*blocksize < 32 && !force) {
+    ctxfree(context, codes);
+    return NULL;
+  }
+  return codes;
+}
+
+size_t compute_uncompressed_PNG_block_size (const unsigned char * restrict data, size_t offset, size_t size, uint16_t * restrict references) {
+  size_t current_offset = offset;
+  for (unsigned score = 0; size - current_offset >= 3 && size - current_offset < 0xffffu; current_offset ++) {
+    unsigned length = find_PNG_reference(data, references, current_offset, size, NULL);
+    if (length) {
+      score += length - 1;
+      if (score >= 16) break;
+    } else if (score > 0)
+      score --;
+    append_PNG_reference(data, current_offset, references);
+  }
+  if (size - current_offset < 3) current_offset = size;
+  return current_offset - offset;
+}
+
+unsigned find_PNG_reference (const unsigned char * restrict data, const uint16_t * restrict references, size_t current_offset, size_t size,
+                             size_t * restrict reference_offset) {
+  uint_fast32_t search = compute_PNG_reference_key(data + current_offset) * (uint_fast32_t) PNG_MAX_LOOKBACK_COUNT;
+  unsigned best = 0;
+  for (uint_fast8_t p = 0; p < PNG_MAX_LOOKBACK_COUNT && references[search + p] != 0xffffu; p ++) {
+    size_t backref = (current_offset & bitnegate(0x7fff)) | references[search + p];
+    if (backref >= current_offset)
+      if (current_offset < 0x8000u)
+        continue;
+      else
+        backref -= 0x8000u;
+    if (!memcmp(data + current_offset, data + backref, 3)) {
+      uint_fast16_t length;
+      for (length = 3; length < 258 && current_offset + length < size; length ++) if (data[current_offset + length] != data[backref + length]) break;
+      if (length > best) {
+        if (reference_offset) *reference_offset = backref;
+        best = length;
+        if (best == 258) break;
+      }
+    }
+  }
+  return best;
+}
+
+void append_PNG_reference (const unsigned char * restrict data, size_t offset, uint16_t * restrict references) {
+  uint_fast32_t key = compute_PNG_reference_key(data + offset) * (uint_fast32_t) PNG_MAX_LOOKBACK_COUNT;
+  memmove(references + key + 1, references + key, (PNG_MAX_LOOKBACK_COUNT - 1) * sizeof *references);
+  references[key] = offset & 0x7fff;
+}
+
+uint16_t compute_PNG_reference_key (const unsigned char * data) {
+  // should return a value between 0 and 0x7fff computed from the first three bytes of data
+  uint_fast32_t key = (uint_fast32_t) *data | ((uint_fast32_t) data[1] << 8) | ((uint_fast32_t) data[2] << 16);
+  // easy way out of a hash code: do a few iterations of a simple linear congruential RNG and return the upper bits of the final state
+  for (uint_fast8_t p = 0; p < 3; p ++) key = 0x41c64e6du * key + 12345;
+  return (key >> 17) & 0x7fff;
+}
+
+#undef PNG_MAX_LOOKBACK_COUNT
+
+void emit_PNG_code (struct context * context, struct compressed_PNG_code ** codes, size_t * restrict allocated, size_t * restrict count, int code, unsigned ref) {
+  // code >= 0 = literal; code < 0 = -length
+  if (*count >= *allocated) {
+    *allocated <<= 1;
+    *codes = ctxrealloc(context, *codes, *allocated * sizeof **codes);
+  }
+  struct compressed_PNG_code result;
+  if (code >= 0)
+    result = (struct compressed_PNG_code) {.datacode = code};
+  else {
+    code = -code;
+    // one extra entry to make looking codes up easier
+    for (result.datacode = 0; compressed_PNG_base_lengths[result.datacode + 1] <= code; result.datacode ++);
+    result.dataextra = code - compressed_PNG_base_lengths[result.datacode];
+    result.datacode += 0x101;
+    for (result.distcode = 0; compressed_PNG_base_distances[result.distcode + 1] <= ref; result.distcode ++);
+    result.distextra = ref - compressed_PNG_base_distances[result.distcode];
+  }
+  (*codes)[(*count) ++] = result;
+}
+
+unsigned char * emit_PNG_compressed_block (struct context * context, const struct compressed_PNG_code * restrict codes, size_t count, bool custom_tree,
+                                           size_t * restrict blocksize, uint32_t * restrict dataword, uint8_t * restrict bits) {
+  // emit the code identifying whether the block is compressed with a fixed or custom tree
+  *dataword |= (custom_tree + 1) << *bits;
+  *bits += 2;
+  // count up the frequency of each code; this will be used to generate a custom tree (if needed) and to precalculate the output size
+  size_t codecounts[0x120] = {[0x100] = 1}; // other entries will be zero-initialized
+  size_t distcounts[0x20] = {0};
+  for (size_t p = 0; p < count; p ++) {
+    codecounts[codes[p].datacode] ++;
+    if (codes[p].datacode > 0x100) distcounts[codes[p].distcode] ++;
+  }
+  unsigned char * output = NULL;
+  *blocksize = 0;
+  // ensure that we have the proper tree: use the documented tree if fixed, or generate (and output) a custom tree if custom
+  unsigned char lengthbuffer[0x140];
+  const unsigned char * codelengths;
+  if (custom_tree) {
+    output = generate_PNG_Huffman_trees(context, dataword, bits, blocksize, codecounts, distcounts, lengthbuffer, lengthbuffer + 0x120);
+    codelengths = lengthbuffer;
+  } else
+    codelengths = default_PNG_Huffman_table_lengths;
+  const unsigned char * distlengths = codelengths + 0x120;
+  // precalculate the output size and allocate enough space for the output (and a little extra); this must account for parameter size too
+  size_t outsize = 7; // for rounding up
+  for (uint_fast16_t p = 0; p < 0x11e; p ++) {
+    uint_fast8_t valuesize = codelengths[p];
+    if (p >= 0x109 && p < 0x11d) valuesize += (p - 0x105) >> 2;
+    if (!valuesize) continue;
+    if (codecounts[p] * valuesize / valuesize != codecounts[p] || SIZE_MAX - outsize < codecounts[p] * valuesize) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+    outsize += codecounts[p] * valuesize;
+  }
+  for (uint_fast8_t p = 0; p < 30; p ++) {
+    uint_fast8_t valuesize = distlengths[p];
+    if (p >= 4) valuesize += (p - 2) >> 1;
+    if (!valuesize) continue;
+    if (distcounts[p] * valuesize / valuesize != distcounts[p] || SIZE_MAX - outsize < distcounts[p] * valuesize) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+    outsize += distcounts[p] * valuesize;
+  }
+  outsize >>= 3;
+  output = ctxrealloc(context, output, *blocksize + outsize + 4);
+  // build the actual encoded values from the tree lengths, properly sorted
+  unsigned short outcodes[0x120];
+  unsigned short outdists[0x20];
+  generate_Huffman_codes(outcodes, sizeof outcodes / sizeof *outcodes, codelengths, true);
+  generate_Huffman_codes(outdists, sizeof outdists / sizeof *outdists, distlengths, true);
+  // and output all of the codes in order, ending with a 0x100 code
+  #define flush while (*bits >= 8) output[(*blocksize) ++] = *dataword, *dataword >>= 8, *bits -= 8
+  while (count --) {
+    *dataword |= (size_t) outcodes[codes -> datacode] << *bits;
+    *bits += codelengths[codes -> datacode];
+    flush;
+    if (codes -> datacode > 0x100) {
+      if (codes -> datacode >= 0x109 && codes -> datacode < 0x11d) {
+        *dataword |= (size_t) codes -> dataextra << *bits;
+        *bits += (codes -> datacode - 0x105) >> 2;
+        // defer the flush because it can't overflow yet
+      }
+      *dataword |= (size_t) outdists[codes -> distcode] << *bits;
+      *bits += distlengths[codes -> distcode];
+      flush;
+      if (codes -> distcode >= 4) {
+        *dataword |= (size_t) codes -> distextra << *bits;
+        *bits += (codes -> distcode - 2) >> 1;
+        flush;
+      }
+    }
+    codes ++;
+  }
+  *dataword |= (size_t) outcodes[0x100] << *bits;
+  *bits += codelengths[0x100];
+  flush;
+  #undef flush
+  return output;
+}
+
+unsigned char * generate_PNG_Huffman_trees (struct context * context, uint32_t * restrict dataword, uint8_t * restrict bits, size_t * restrict size,
+                                            const size_t codecounts[restrict static 0x120], const size_t distcounts[restrict static 0x20],
+                                            unsigned char codelengths[restrict static 0x120], unsigned char distlengths[restrict static 0x20]) {
+  // this function will generate trees, discard them and only preserve the lengths; that way, the real (properly ordered) trees can be rebuilt later
+  // also outputs the tree length data to the output stream and returns it
+  generate_Huffman_tree(context, codecounts, codelengths, 0x120, 15);
+  generate_Huffman_tree(context, distcounts, distlengths, 0x20, 15);
+  unsigned char lengths[0x140];
+  unsigned char encoded[0x140];
+  unsigned repcount, maxcode, maxdist, encodedlength = 0, code = 0;
+  for (maxcode = 0x11f; !codelengths[maxcode]; maxcode --);
+  for (maxdist = 0x1f; maxdist && !distlengths[maxdist]; maxdist --);
+  memcpy(lengths, codelengths, maxcode + 1);
+  memcpy(lengths + maxcode + 1, distlengths, maxdist + 1);
+  while (code < maxcode + maxdist + 2)
+    if (!lengths[code]) {
+      for (repcount = 1; repcount < 0x8a && code + repcount < maxcode + maxdist + 2 && !lengths[code + repcount]; repcount ++);
+      if (repcount < 3) {
+        encoded[encodedlength ++] = 0;
+        code ++;
+      } else {
+        code += repcount;
+        encoded[encodedlength ++] = 17 + (repcount > 10);
+        encoded[encodedlength ++] = repcount - ((repcount >= 11) ? 11 : 3);
+      }
+    } else if (code && lengths[code] == lengths[code - 1]) {
+      for (repcount = 1; repcount < 6 && code + repcount < maxcode + maxdist + 2 && lengths[code + repcount] == lengths[code - 1]; repcount ++);
+      if (repcount < 3)
+        encoded[encodedlength ++] = lengths[code ++];
+      else {
+        encoded[encodedlength ++] = 16;
+        encoded[encodedlength ++] = repcount - 3;
+        code += repcount;
+      }
+    } else
+      encoded[encodedlength ++] = lengths[code ++];
+  size_t encodedcounts[19] = {0};
+  for (uint_fast16_t p = 0; p < encodedlength; p ++) {
+    encodedcounts[encoded[p]] ++;
+    if (encoded[p] >= 16) p ++;
+  }
+  generate_Huffman_tree(context, encodedcounts, lengths, 19, 7);
+  unsigned short codes[19];
+  for (repcount = 18; repcount > 3 && !lengths[compressed_PNG_code_table_order[repcount]]; repcount --);
+  generate_Huffman_codes(codes, 19, lengths, true);
+  *dataword |= (maxcode & 0x1f) << *bits;
+  *bits += 5;
+  *dataword |= maxdist << *bits;
+  *bits += 5;
+  *dataword |= (repcount - 3) << *bits;
+  *bits += 4;
+  unsigned char * result = ctxmalloc(context, 0x100);
+  unsigned char * current = result;
+  #define flush while (*bits >= 8) *(current ++) = *dataword, *dataword >>= 8, *bits -= 8
+  flush;
+  for (uint_fast8_t p = 0; p <= repcount; p ++) {
+    *dataword |= lengths[compressed_PNG_code_table_order[p]] << *bits;
+    *bits += 3;
+    flush;
+  }
+  for (uint_fast16_t p = 0; p < encodedlength; p ++) {
+    *dataword |= codes[encoded[p]] << *bits;
+    *bits += lengths[encoded[p]];
+    if (encoded[p] >= 16) {
+      uint_fast8_t repeattype = encoded[p] - 16;
+      *dataword |= encoded[++ p] << *bits;
+      *bits += (2 << repeattype) - !!repeattype; // 0, 1, 2 maps to 2, 3, 7
+    }
+    flush;
+  }
+  #undef flush
+  *size = current - result;
+  return result;
+}
+
+void * decompress_PNG_data (struct context * context, const unsigned char * compressed, size_t size, size_t expected) {
+  if (size <= 6) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if ((*compressed & 0x8f) != 8 || (compressed[1] & 0x20) || read_be16_unaligned(compressed) % 31) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  // ignore the window size - treat it as 0x8000 for simpler code (everything will be in memory anyway)
+  compressed += 2;
+  size -= 6; // pretend the checksum is not part of the data
+  unsigned char * decompressed = ctxmalloc(context, expected);
+  size_t current = 0;
+  bool last_block;
+  uint32_t dataword = 0;
+  uint8_t bits = 0;
+  do {
+    last_block = shift_in_left(context, 1, &dataword, &bits, &compressed, &size);
+    switch (shift_in_left(context, 2, &dataword, &bits, &compressed, &size)) {
+      case 0: {
+        dataword >>= bits & 7;
+        bits &= ~7;
+        uint32_t literalcount = shift_in_left(context, 32, &dataword, &bits, &compressed, &size);
+        if (((literalcount >> 16) ^ (literalcount & 0xffffu)) != 0xffffu) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        literalcount &= 0xffffu;
+        if (literalcount > expected - current) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        if (literalcount > size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        memcpy(decompressed + current, compressed, literalcount);
+        current += literalcount;
+        compressed += literalcount;
+        size -= literalcount;
+      } break;
+      case 1:
+        decompress_PNG_block(context, &compressed, decompressed, &size, &current, expected, &dataword, &bits, default_PNG_Huffman_table_lengths);
+        break;
+      case 2: {
+        unsigned char codesizes[0x140];
+        extract_PNG_code_table(context, &compressed, &size, codesizes, &dataword, &bits);
+        decompress_PNG_block(context, &compressed, decompressed, &size, &current, expected, &dataword, &bits, codesizes);
+      } break;
+      default:
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+  } while (!last_block);
+  if (size || current != expected) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (compute_Adler32_checksum(decompressed, expected) != read_be32_unaligned(compressed)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  return decompressed;
+}
+
+void extract_PNG_code_table (struct context * context, const unsigned char ** compressed, size_t * restrict size, unsigned char codesizes[restrict static 0x140],
+                             uint32_t * restrict dataword, uint8_t * restrict bits) {
+  uint_fast16_t header = shift_in_left(context, 14, dataword, bits, compressed, size);
+  unsigned literals = 0x101 + (header & 0x1f);
+  unsigned distances = 1 + ((header >> 5) & 0x1f);
+  unsigned lengths = 4 + (header >> 10);
+  unsigned char internal_sizes[19] = {0};
+  for (uint_fast8_t p = 0; p < lengths; p ++) internal_sizes[compressed_PNG_code_table_order[p]] = shift_in_left(context, 3, dataword, bits, compressed, size);
+  short * tree = decode_PNG_Huffman_tree(context, internal_sizes, sizeof internal_sizes);
+  if (!tree) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint_fast16_t index = 0;
+  while (index < literals + distances) {
+    uint_fast8_t code = next_PNG_Huffman_code(context, tree, compressed, size, dataword, bits);
+    switch (code) {
+      case 16: {
+        if (!index) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        uint_fast8_t codesize = codesizes[index - 1], count = 3 + shift_in_left(context, 2, dataword, bits, compressed, size);
+        if (index + count > literals + distances) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        while (count --) codesizes[index ++] = codesize;
+      } break;
+      case 17: case 18: {
+        uint_fast8_t count = ((code == 18) ? 11 : 3) + shift_in_left(context, (code == 18) ? 7 : 3, dataword, bits, compressed, size);
+        if (index + count > literals + distances) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        while (count --) codesizes[index ++] = 0;
+      } break;
+      default:
+        codesizes[index ++] = code;
+    }
+  }
+  ctxfree(context, tree);
+  if (literals < 0x120) memmove(codesizes + 0x120, codesizes + literals, distances);
+  memset(codesizes + literals, 0, 0x120 - literals);
+  memset(codesizes + 0x120 + distances, 0, 0x20 - distances);
+}
+
+void decompress_PNG_block (struct context * context, const unsigned char ** compressed, unsigned char * restrict decompressed, size_t * restrict size,
+                           size_t * restrict current, size_t expected, uint32_t * restrict dataword, uint8_t * restrict bits,
+                           const unsigned char codesizes[restrict static 0x140]) {
+  // a single list of codesizes for all codes: 0x00-0xff for literals, 0x100 for end of codes, 0x101-0x11d for lengths, 0x120-0x13d for distances
+  short * codetree = decode_PNG_Huffman_tree(context, codesizes, 0x120);
+  if (!codetree) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  short * disttree = decode_PNG_Huffman_tree(context, codesizes + 0x120, 0x20);
+  while (true) {
+    uint_fast16_t code = next_PNG_Huffman_code(context, codetree, compressed, size, dataword, bits);
+    if (code >= 0x11e) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (code == 0x100) break;
+    if (code < 0x100) {
+      if (*current >= expected) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      decompressed[(*current) ++] = code;
+      continue;
+    }
+    if (!disttree) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    code -= 0x101;
+    uint_fast16_t length = compressed_PNG_base_lengths[code];
+    uint_fast8_t lengthbits = compressed_PNG_length_bits[code];
+    if (lengthbits) length += shift_in_left(context, lengthbits, dataword, bits, compressed, size);
+    uint_fast8_t distcode = next_PNG_Huffman_code(context, disttree, compressed, size, dataword, bits);
+    if (distcode > 29) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    uint_fast16_t distance = compressed_PNG_base_distances[distcode];
+    uint_fast8_t distbits = compressed_PNG_distance_bits[distcode];
+    if (distbits) distance += shift_in_left(context, distbits, dataword, bits, compressed, size);
+    if (distance > *current) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (*current + length > expected || *current + length < *current) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    for (; length; -- length, ++ *current) decompressed[*current] = decompressed[*current - distance];
+  }
+  ctxfree(context, disttree);
+  ctxfree(context, codetree);
+}
+
+short * decode_PNG_Huffman_tree (struct context * context, const unsigned char * codesizes, unsigned count) {
+  // root at index 0; each non-leaf node takes two entries (index for the 0 branch, index+1 for the 1 branch)
+  // non-negative value: branch points to a leaf node; negative value: branch points to another non-leaf at -index
+  // -1 is used as an invalid value, since -1 cannot ever occur (as index 1 would overlap with the root)
+  uint_fast16_t total = 0;
+  uint_fast8_t codelength = 0;
+  for (uint_fast16_t p = 0; p < count; p ++) if (codesizes[p]) {
+    total ++;
+    if (codesizes[p] > codelength) codelength = codesizes[p];
+  }
+  if (!total) return NULL;
+  uint_fast16_t maxlength = count * 2 * codelength;
+  short * result = ctxmalloc(context, maxlength * sizeof *result);
+  for (uint_fast16_t p = 0; p < maxlength; p ++) result[p] = -1;
+  uint_fast16_t code = 0;
+  short last = 2;
+  for (uint_fast8_t curlength = 1; curlength <= codelength; curlength ++) {
+    code <<= 1;
+    for (uint_fast16_t p = 0; p < count; p ++) if (codesizes[p] == curlength) {
+      if (code >= (1u << curlength)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      uint_fast16_t index = 0;
+      for (uint_fast8_t bit = curlength - 1; bit; bit --) {
+        if (code & (1u << bit)) index ++;
+        if (result[index] == -1) {
+          result[index] = -last;
+          last += 2;
+        }
+        index = -result[index];
+      }
+      if (code & 1) index ++;
+      result[index] = p;
+      code ++;
+    }
+  }
+  if (code > (1u << codelength)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  return ctxrealloc(context, result, last * sizeof *result);
+}
+
+uint16_t next_PNG_Huffman_code (struct context * context, const short * restrict tree, const unsigned char ** compressed, size_t * restrict size,
+                                uint32_t * restrict dataword, uint8_t * restrict bits) {
+  for (uint_fast16_t index = 0; index != 1; index = -tree[index]) {
+    index += shift_in_left(context, 1, dataword, bits, compressed, size);
+    if (tree[index] >= 0) return tree[index];
+  }
+  throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+}
+
+void load_PNG_data (struct context * context, unsigned flags, size_t limit) {
+  struct PNG_chunk_locations * chunks = load_PNG_chunk_locations(context); // also sets context -> image -> frames for APNGs
+  // load basic header data
+  if (chunks -> animation) {
+    context -> image -> type = PLUM_IMAGE_APNG;
+    if (*chunks -> data < *chunks -> frameinfo) context -> image -> frames ++; // first frame is not part of the animation
+  } else {
+    context -> image -> type = PLUM_IMAGE_PNG;
+    context -> image -> frames = 1;
+  }
+  context -> image -> width = read_be32_unaligned(context -> data + 16);
+  context -> image -> height = read_be32_unaligned(context -> data + 20);
+  if (context -> image -> width > 0x7fffffffu || context -> image -> height > 0x7fffffffu) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  validate_image_size(context, limit);
+  int interlaced = context -> data[28];
+  unsigned char bitdepth = context -> data[24], imagetype = context -> data[25];
+  if (context -> data[26] || context -> data[27] || interlaced > 1 || imagetype > 6 || imagetype == 1 || imagetype == 5 || !bitdepth ||
+      (bitdepth & (bitdepth - 1)) || bitdepth > 16 || (imagetype == 3 && bitdepth == 16) || (imagetype && imagetype != 3 && bitdepth < 8))
+    throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  // load palette and color-related metadata
+  uint64_t * palette = NULL;
+  uint8_t max_palette_index = 0;
+  if (chunks -> palette && (!imagetype || imagetype == 4)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (imagetype == 3) {
+    palette = ctxcalloc(context, 256 * sizeof *palette);
+    max_palette_index = load_PNG_palette(context, chunks, bitdepth, palette);
+  }
+  add_PNG_bit_depth_metadata(context, chunks, imagetype, bitdepth);
+  uint64_t background = add_PNG_background_metadata(context, chunks, palette, imagetype, bitdepth, max_palette_index, flags);
+  uint64_t transparent = 0xffffffffffffffffu;
+  if (chunks -> transparency)
+    if (imagetype <= 2)
+      transparent = load_PNG_transparent_color(context, chunks -> transparency, imagetype, bitdepth);
+    else if (imagetype >= 4)
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  // if there are no reduced APNG frames (i.e., frames that are smaller than the image), and we have a palette, load it into the struct
+  if (palette && !(chunks -> animation && check_PNG_reduced_frames(context, chunks))) {
+    context -> image -> max_palette_index = max_palette_index;
+    context -> image -> palette = plum_malloc(context -> image, plum_color_buffer_size(max_palette_index + 1, flags));
+    if (!context -> image -> palette) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+    plum_convert_colors(context -> image -> palette, palette, max_palette_index + 1, flags, PLUM_COLOR_64);
+  }
+  // allocate space for the image data and load the main image; for a PNG file, we're done here
+  allocate_framebuffers(context, flags, context -> image -> palette);
+  load_PNG_frame(context, chunks -> data, 0, palette, max_palette_index, imagetype, bitdepth, interlaced, background, transparent);
+  if (!chunks -> animation) return;
+  // load the animation control chunk and duration and disposal metadata
+  uint32_t loops = read_be32_unaligned(context -> data + chunks -> animation + 4);
+  if (loops > 0x7fffffffu) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  add_loop_count_metadata(context, loops);
+  uint64_t * durations;
+  uint8_t * disposals;
+  add_animation_metadata(context, &durations, &disposals);
+  struct plum_rectangle * frameareas = add_frame_area_metadata(context);
+  const size_t * frameinfo = chunks -> frameinfo;
+  const size_t * const * framedata = (const size_t * const *) chunks -> framedata;
+  // handle the first frame's metadata, which is special and may or may not be part of the animation (the frame data will have already been loaded)
+  bool replace_last = false;
+  if (!*frameinfo) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (*frameinfo < *chunks -> data) {
+    if (
+      read_be32_unaligned(context -> data + *frameinfo + 4) != context -> image -> width ||
+      read_be32_unaligned(context -> data + *frameinfo + 8) != context -> image -> height ||
+      !bytematch(context -> data + *frameinfo + 12, 0, 0, 0, 0, 0, 0, 0, 0)
+    ) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (**framedata) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    replace_last = load_PNG_animation_frame_metadata(context, *frameinfo, durations, disposals);
+    frameinfo ++;
+    framedata ++;
+  } else {
+    *disposals = PLUM_DISPOSAL_PREVIOUS;
+    *durations = 0;
+  }
+  *frameareas = (struct plum_rectangle) {.left = 0, .top = 0, .width = context -> image -> width, .height = context -> image -> height};
+  // actually load animation frames
+  if (*frameinfo && *frameinfo < *chunks -> data) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  for (uint_fast32_t frame = 1; frame < context -> image -> frames; frame ++) {
+    bool replace = load_PNG_animation_frame_metadata(context, *frameinfo, durations + frame, disposals + frame);
+    if (replace) disposals[frame - 1] += PLUM_DISPOSAL_REPLACE;
+    uint_fast32_t width = read_be32_unaligned(context -> data + *frameinfo + 4);
+    uint_fast32_t height = read_be32_unaligned(context -> data + *frameinfo + 8);
+    uint_fast32_t left = read_be32_unaligned(context -> data + *frameinfo + 12);
+    uint_fast32_t top = read_be32_unaligned(context -> data + *frameinfo + 16);
+    if ((width | height | left | top) & 0x80000000u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (width + left > context -> image -> width || height + top > context -> image -> height) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    frameareas[frame] = (struct plum_rectangle) {.left = left, .top = top, .width = width, .height = height};
+    if (width == context -> image -> width && height == context -> image -> height)
+      load_PNG_frame(context, *framedata, frame, palette, max_palette_index, imagetype, bitdepth, interlaced, background, transparent);
+    else {
+      uint64_t * output = ctxmalloc(context, sizeof *output * context -> image -> width * context -> image -> height);
+      uint64_t * current = output;
+      size_t index = 0;
+      if (palette) {
+        uint8_t * pixels = load_PNG_frame_part(context, *framedata, max_palette_index, imagetype, bitdepth, interlaced, width, height, 4);
+        for (size_t row = 0; row < context -> image -> height; row ++) for (size_t col = 0; col < context -> image -> width; col ++)
+          if (row < top || col < left || row >= top + height || col >= left + width)
+            *(current ++) = background | 0xffff000000000000u;
+          else
+            *(current ++) = palette[pixels[index ++]];
+        ctxfree(context, pixels);
+      } else {
+        uint64_t * pixels = load_PNG_frame_part(context, *framedata, -1, imagetype, bitdepth, interlaced, width, height, 4);
+        for (size_t row = 0; row < context -> image -> height; row ++) for (size_t col = 0; col < context -> image -> width; col ++)
+          if (row < top || col < left || row >= top + height || col >= left + width)
+            *(current ++) = background | 0xffff000000000000u;
+          else {
+            *current = pixels[index ++];
+            if (transparent != 0xffffffffffffffffu && *current == transparent) *current = background | 0xffff000000000000u;
+            current ++;
+          }
+        ctxfree(context, pixels);
+      }
+      write_framebuffer_to_image(context -> image, output, frame, flags);
+      ctxfree(context, output);
+    }
+    frameinfo ++;
+    framedata ++;
+  }
+  if (replace_last || (*chunks -> frameinfo >= *chunks -> data && *disposals >= PLUM_DISPOSAL_REPLACE))
+    disposals[context -> image -> frames - 1] += PLUM_DISPOSAL_REPLACE;
+  // we're done; a few things will be leaked here (chunk data, palette data...), but they are small and will be collected later
+}
+
+struct PNG_chunk_locations * load_PNG_chunk_locations (struct context * context) {
+  if (context -> size < 45) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (!bytematch(context -> data + 12, 0x49, 0x48, 0x44, 0x52)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  size_t offset = 8;
+  uint32_t chunk_type = 0;
+  struct PNG_chunk_locations * result = ctxmalloc(context, sizeof *result);
+  *result = (struct PNG_chunk_locations) {0}; // ensure that integers and pointers are properly zero-initialized
+  size_t data_count = 0, frameinfo_count = 0, framedata_count = 0;
+  size_t * framedata = NULL;
+  bool invalid_animation = false;
+  while (offset <= context -> size - 12) {
+    uint32_t length = read_be32_unaligned(context -> data + offset);
+    chunk_type = read_be32_unaligned(context -> data + offset + 4);
+    offset += 8;
+    if (length > 0x7fffffffu) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (offset + length + 4 < offset || offset + length + 4 > context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (read_be32_unaligned(context -> data + offset + length) != compute_PNG_CRC(context -> data + offset - 4, length + 4))
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    switch (chunk_type) {
+      case 0x49484452u: // IHDR
+        if (offset != 16 || length != 13) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        break;
+      case 0x49454e44u: // IEND
+        if (length) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        offset += 4;
+        goto done;
+      case 0x504c5445u: // PLTE
+        if (result -> palette || length % 3 || length > 0x300 || !length) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        result -> palette = offset;
+        break;
+      case 0x49444154u: // IDAT
+        // we don't really care if they are consecutive or not; this error is easy to tolerate
+        append_PNG_chunk_location(context, &result -> data, offset, &data_count);
+        break;
+      case 0x73424954u: // sBIT
+        if (result -> bits || !length || length > 4) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        result -> bits = offset;
+        break;
+      case 0x624b4744u: // bKGD
+        if (result -> background || !length || length > 6) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        result -> background = offset;
+        break;
+      case 0x74524e53u: // tRNS
+        if (result -> transparency || length > 256) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        result -> transparency = offset;
+        break;
+      case 0x6163544cu: // acTL
+        if (!invalid_animation)
+          if (result -> data || result -> animation || length != 8)
+            invalid_animation = true;
+          else
+            result -> animation = offset;
+        break;
+      case 0x6663544cu: // fcTL
+        if (!invalid_animation)
+          if (length == 26)
+            append_PNG_chunk_location(context, &result -> frameinfo, offset, &frameinfo_count);
+          else
+            invalid_animation = true;
+        break;
+      case 0x66644154u: // fdAT
+        if (!invalid_animation)
+          if (length >= 4)
+            append_PNG_chunk_location(context, &framedata, offset, &framedata_count);
+          else
+            invalid_animation = true;
+        break;
+      default:
+        if ((chunk_type & 0xe0c0c0c0u) != 0x60404040u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT); // invalid or critical
+        while (chunk_type) {
+          if (!(chunk_type & 0x1f) || (chunk_type & 0x1f) > 26) throw(context, PLUM_ERR_INVALID_FILE_FORMAT); // invalid
+          chunk_type >>= 8;
+        }
+    }
+    offset += length + 4;
+  }
+  done:
+  if (offset != context -> size || chunk_type != 0x49454e44u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (!result -> data) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  append_PNG_chunk_location(context, &result -> data, 0, &data_count);
+  append_PNG_chunk_location(context, &result -> frameinfo, 0, &frameinfo_count);
+  frameinfo_count --;
+  if (invalid_animation) {
+    ctxfree(context, result -> frameinfo);
+    result -> animation = 0;
+    result -> frameinfo = NULL;
+  } else if (result -> animation) {
+    // validate and initialize frame counts here to avoid having to count them up later
+    if (frameinfo_count != read_be32_unaligned(context -> data + result -> animation)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    sort_PNG_animation_chunks(context, result, framedata, frameinfo_count, framedata_count);
+    context -> image -> frames = frameinfo_count;
+  }
+  ctxfree(context, framedata);
+  return result;
+}
+
+void append_PNG_chunk_location (struct context * context, size_t ** locations, size_t location, size_t * restrict count) {
+  *locations = ctxrealloc(context, *locations, sizeof **locations * (*count + 1));
+  (*locations)[(*count) ++] = location;
+}
+
+void sort_PNG_animation_chunks (struct context * context, struct PNG_chunk_locations * restrict locations, const size_t * restrict framedata,
+                                size_t frameinfo_count, size_t framedata_count) {
+  if ((frameinfo_count + framedata_count) > 0x80000000u) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (!frameinfo_count || (frameinfo_count > 1 && !framedata_count)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint64_t * indexes = ctxmalloc(context, sizeof *indexes * (frameinfo_count + framedata_count));
+  for (uint_fast32_t p = 0; p < frameinfo_count; p ++)
+    indexes[p] = ((uint64_t) read_be32_unaligned(context -> data + locations -> frameinfo[p]) << 32) | 0x80000000u | p;
+  for (uint_fast32_t p = 0; p < framedata_count; p ++)
+    indexes[p + frameinfo_count] = ((uint64_t) read_be32_unaligned(context -> data + framedata[p]) << 32) | p;
+  sort_values(indexes, frameinfo_count + framedata_count);
+  if (!(*indexes & 0x80000000u)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT); // fdAT before fcTL
+  size_t * frames = ctxmalloc(context, sizeof *frames * frameinfo_count);
+  locations -> framedata = ctxmalloc(context, sizeof *locations -> framedata * frameinfo_count);
+  uint_fast32_t infoindex = 0, datacount = 0;
+  // special handling for the first entry
+  if (*indexes >> 32) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  *locations -> framedata = NULL;
+  *frames = locations -> frameinfo[*indexes & 0x7fffffffu];
+  for (uint_fast32_t p = 1; p < frameinfo_count + framedata_count; p ++) {
+    if ((indexes[p] >> 32) != p) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    locations -> framedata[infoindex] = ctxrealloc(context, locations -> framedata[infoindex], sizeof **locations -> framedata * (datacount + 1));
+    if (indexes[p] & 0x80000000u) {
+      locations -> framedata[infoindex ++][datacount] = 0;
+      locations -> framedata[infoindex] = NULL;
+      frames[infoindex] = locations -> frameinfo[indexes[p] & 0x7fffffffu];
+      datacount = 0;
+    } else
+      locations -> framedata[infoindex][datacount ++] = framedata[indexes[p] & 0x7fffffffu];
+  }
+  locations -> framedata[infoindex] = ctxrealloc(context, locations -> framedata[infoindex], sizeof **locations -> framedata * (datacount + 1));
+  locations -> framedata[infoindex][datacount] = 0;
+  memcpy(locations -> frameinfo, frames, sizeof *frames * frameinfo_count);
+  ctxfree(context, frames);
+  ctxfree(context, indexes);
+}
+
+uint8_t load_PNG_palette (struct context * context, const struct PNG_chunk_locations * restrict chunks, uint8_t bitdepth, uint64_t * restrict palette) {
+  if (!chunks -> palette) throw(context, PLUM_ERR_UNDEFINED_PALETTE);
+  uint_fast32_t count = read_be32_unaligned(context -> data + chunks -> palette - 8) / 3;
+  if (count > (1 << bitdepth)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  const unsigned char * data = context -> data + chunks -> palette;
+  for (uint_fast32_t p = 0; p < count; p ++) palette[p] = (data[p * 3] | ((uint64_t) data[p * 3 + 1] << 16) | ((uint64_t) data[p * 3 + 2] << 32)) * 0x101;
+  if (chunks -> transparency) {
+    uint_fast32_t transparency_count = read_be32_unaligned(context -> data + chunks -> transparency - 8);
+    if (transparency_count > count) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    data = context -> data + chunks -> transparency;
+    for (uint_fast32_t p = 0; p < transparency_count; p ++) palette[p] |= 0x101000000000000u * (0xff ^ *(data ++));
+  }
+  return count - 1;
+}
+
+void add_PNG_bit_depth_metadata (struct context * context, const struct PNG_chunk_locations * chunks, uint8_t imagetype, uint8_t bitdepth) {
+  uint8_t red, green, blue, alpha, gray;
+  switch (imagetype) {
+    case 0:
+      red = green = blue = 0;
+      alpha = !!chunks -> transparency;
+      gray = bitdepth;
+      if (chunks -> bits) {
+        if (read_be32_unaligned(context -> data + chunks -> bits - 8) != 1) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        gray = context -> data[chunks -> bits];
+        if (gray > bitdepth) gray = bitdepth;
+      }
+      break;
+    case 2:
+      red = green = blue = bitdepth;
+      alpha = !!chunks -> transparency;
+      gray = 0;
+      if (chunks -> bits) {
+        if (read_be32_unaligned(context -> data + chunks -> bits - 8) != 3) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        red = context -> data[chunks -> bits];
+        if (red > bitdepth) red = bitdepth;
+        green = context -> data[chunks -> bits + 1];
+        if (green > bitdepth) green = bitdepth;
+        blue = context -> data[chunks -> bits + 2];
+        if (blue > bitdepth) blue = bitdepth;
+      }
+      break;
+    case 3:
+      red = green = blue = 8;
+      alpha = chunks -> transparency ? 8 : 0;
+      gray = 0;
+      if (chunks -> bits) {
+        if (read_be32_unaligned(context -> data + chunks -> bits - 8) != 3) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        red = context -> data[chunks -> bits];
+        if (red > 8) red = 8;
+        green = context -> data[chunks -> bits + 1];
+        if (green > 8) green = 8;
+        blue = context -> data[chunks -> bits + 2];
+        if (blue > 8) blue = 8;
+      }
+      break;
+    case 4:
+      red = green = blue = 0;
+      gray = alpha = bitdepth;
+      if (chunks -> bits) {
+        if (read_be32_unaligned(context -> data + chunks -> bits - 8) != 2) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        gray = context -> data[chunks -> bits];
+        if (gray > bitdepth) gray = bitdepth;
+        alpha = context -> data[chunks -> bits + 1];
+        if (alpha > bitdepth) alpha = bitdepth;
+      }
+      break;
+    case 6:
+      red = green = blue = alpha = bitdepth;
+      gray = 0;
+      if (chunks -> bits) {
+        if (read_be32_unaligned(context -> data + chunks -> bits - 8) != 4) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        red = context -> data[chunks -> bits];
+        if (red > bitdepth) red = bitdepth;
+        green = context -> data[chunks -> bits + 1];
+        if (green > bitdepth) green = bitdepth;
+        blue = context -> data[chunks -> bits + 2];
+        if (blue > bitdepth) blue = bitdepth;
+        alpha = context -> data[chunks -> bits + 3];
+        if (alpha > bitdepth) alpha = bitdepth;
+      }
+  }
+  add_color_depth_metadata(context, red, green, blue, alpha, gray);
+}
+
+uint64_t add_PNG_background_metadata (struct context * context, const struct PNG_chunk_locations * chunks, const uint64_t * palette, uint8_t imagetype,
+                                      uint8_t bitdepth, uint8_t max_palette_index, unsigned flags) {
+  if (!chunks -> background) return 0;
+  uint64_t color;
+  const unsigned char * data = context -> data + chunks -> background;
+  switch (imagetype) {
+    case 0: case 4:
+      if (read_be32_unaligned(data - 8) != 2) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      color = read_le16_unaligned(data);
+      if (color >> bitdepth) return 0;
+      color = 0x100010001u * (uint64_t) bitextend16(color, bitdepth);
+      break;
+    case 3:
+      if (read_be32_unaligned(data - 8) != 1) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      if (*data > max_palette_index) return 0;
+      color = palette[*data];
+      break;
+    default:
+      if (read_be32_unaligned(data - 8) != 6) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      if (bitdepth == 8) {
+        if (*data || data[2] || data[4]) return 0;
+        color = ((uint64_t) data[1] | ((uint64_t) data[3] << 16) | ((uint64_t) data[5] << 32)) * 0x101;
+      } else
+        color = read_be16_unaligned(data) | ((uint64_t) read_be16_unaligned(data + 2) << 16) | ((uint64_t) read_be16_unaligned(data + 4) << 32);
+  }
+  add_background_color_metadata(context, color, flags);
+  return color;
+}
+
+uint64_t load_PNG_transparent_color (struct context * context, size_t offset, uint8_t imagetype, uint8_t bitdepth) {
+  // only for image types 0 or 2
+  const unsigned char * data = context -> data + offset;
+  if (read_be32_unaligned(data - 8) != (imagetype ? 6 : 2)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (!imagetype) {
+    uint_fast32_t color = read_be16_unaligned(data); // cannot be 16-bit because of the potential >> 16 in the next line
+    if (color >> bitdepth) return 0xffffffffffffffffu;
+    return 0x100010001u * (uint64_t) bitextend16(color, bitdepth);
+  } else if (bitdepth == 8) {
+    if (*data || data[2] || data[4]) return 0xffffffffffffffffu;
+    return ((uint64_t) data[1] | ((uint64_t) data[3] << 16) | ((uint64_t) data[5] << 32)) * 0x101;
+  } else
+    return (uint64_t) read_be16_unaligned(data) | ((uint64_t) read_be16_unaligned(data + 2) << 16) | ((uint64_t) read_be16_unaligned(data + 4) << 32);
+}
+
+bool check_PNG_reduced_frames (struct context * context, const struct PNG_chunk_locations * chunks) {
+  for (const size_t * frameinfo = chunks -> frameinfo; *frameinfo; frameinfo ++) {
+    uint_fast32_t width = read_be32_unaligned(context -> data + *frameinfo + 4);
+    uint_fast32_t height = read_be32_unaligned(context -> data + *frameinfo + 8);
+    uint_fast32_t left = read_be32_unaligned(context -> data + *frameinfo + 12);
+    uint_fast32_t top = read_be32_unaligned(context -> data + *frameinfo + 16);
+    if (top || left || width != context -> image -> width || height != context -> image -> height) return true;
+  }
+  return false;
+}
+
+bool load_PNG_animation_frame_metadata (struct context * context, size_t offset, uint64_t * restrict duration, uint8_t * restrict disposal) {
+  // returns if the previous frame should be replaced
+  uint_fast16_t numerator = read_be16_unaligned(context -> data + offset + 20), denominator = read_be16_unaligned(context -> data + offset + 22);
+  if ((*disposal = context -> data[offset + 24]) > 2) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  uint_fast8_t blend = context -> data[offset + 25];
+  if (blend > 1) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (numerator) {
+    if (!denominator) denominator = 100;
+    *duration = ((uint64_t) numerator * 1000000000 + denominator / 2) / denominator;
+  } else
+    *duration = 1;
+  return !blend;
+}
+
+void load_PNG_frame (struct context * context, const size_t * chunks, uint32_t frame, const uint64_t * palette, uint8_t max_palette_index,
+                     uint8_t imagetype, uint8_t bitdepth, bool interlaced, uint64_t background, uint64_t transparent) {
+  void * data = load_PNG_frame_part(context, chunks, palette ? max_palette_index : -1, imagetype, bitdepth, interlaced,
+                                    context -> image -> width, context -> image -> height, frame ? 4 : 0);
+  if (palette)
+    write_palette_framebuffer_to_image(context, data, palette, frame, context -> image -> color_format, 0xff); // 0xff to avoid a redundant range check
+  else {
+    if (transparent != 0xffffffffffffffffu) {
+      size_t count = (size_t) context -> image -> width * context -> image -> height;
+      for (uint64_t * current = data; count; count --, current ++) if (*current == transparent) *current = background | 0xffff000000000000u;
+    }
+    write_framebuffer_to_image(context -> image, data, frame, context -> image -> color_format);
+  }
+  ctxfree(context, data);
+}
+
+void * load_PNG_frame_part (struct context * context, const size_t * chunks, int max_palette_index, uint8_t imagetype, uint8_t bitdepth, bool interlaced,
+                            uint32_t width, uint32_t height, size_t chunkoffset) {
+  // max_palette_index < 0: no palette (return uint64_t *); otherwise, use a palette (return uint8_t *)
+  size_t p = 0, total_compressed_size = 0;
+  for (const size_t * chunk = chunks; *chunk; chunk ++) total_compressed_size += read_be32_unaligned(context -> data + *chunk - 8) - chunkoffset;
+  unsigned char * compressed = ctxmalloc(context, total_compressed_size);
+  for (const size_t * chunk = chunks; *chunk; chunk ++) {
+    size_t current = read_be32_unaligned(context -> data + *chunk - 8) - chunkoffset;
+    memcpy(compressed + p, context -> data + *chunk + chunkoffset, current);
+    p += current;
+  }
+  void * result;
+  if (max_palette_index < 0)
+    result = load_PNG_raw_frame(context, compressed, total_compressed_size, width, height, imagetype, bitdepth, interlaced);
+  else
+    result = load_PNG_palette_frame(context, compressed, total_compressed_size, width, height, bitdepth, max_palette_index, interlaced);
+  ctxfree(context, compressed);
+  return result;
+}
+
+uint8_t * load_PNG_palette_frame (struct context * context, const void * compressed, size_t compressed_size, uint32_t width, uint32_t height, uint8_t bitdepth,
+                                  uint8_t max_palette_index, bool interlaced) {
+  // imagetype must be 3 here
+  uint8_t * result = ctxmalloc(context, (size_t) width * height);
+  unsigned char * decompressed;
+  if (interlaced) {
+    size_t widths[] = {(width + 7) / 8, (width + 3) / 8, (width + 3) / 4, (width + 1) / 4, (width + 1) / 2, width / 2, width};
+    size_t heights[] = {(height + 7) / 8, (height + 7) / 8, (height + 3) / 8, (height + 3) / 4, (height + 1) / 4, (height + 1) / 2, height / 2};
+    size_t rowsizes[7];
+    size_t cumulative_size = 0;
+    for (uint_fast8_t pass = 0; pass < 7; pass ++) if (widths[pass] && heights[pass]) {
+      rowsizes[pass] = ((size_t) widths[pass] * bitdepth + 7) / 8 + 1;
+      cumulative_size += heights[pass] * rowsizes[pass];
+    }
+    decompressed = decompress_PNG_data(context, compressed, compressed_size, cumulative_size);
+    unsigned char * current = decompressed;
+    unsigned char * rowdata = ctxmalloc(context, width);
+    for (uint_fast8_t pass = 0; pass < 7; pass ++) if (widths[pass] && heights[pass]) {
+      remove_PNG_filter(context, current, widths[pass], heights[pass], 3, bitdepth);
+      for (size_t row = 0; row < heights[pass]; row ++) {
+        expand_bitpacked_PNG_data(rowdata, current + 1, widths[pass], bitdepth);
+        current += rowsizes[pass];
+        for (size_t col = 0; col < widths[pass]; col ++)
+          result[(row * interlaced_PNG_pass_step[pass] + interlaced_PNG_pass_start[pass]) * width +
+                 col * interlaced_PNG_pass_step[pass + 1] + interlaced_PNG_pass_start[pass + 1]] = rowdata[col];
+      }
+    }
+    ctxfree(context, rowdata);
+  } else {
+    size_t rowsize = ((size_t) width * bitdepth + 7) / 8 + 1;
+    decompressed = decompress_PNG_data(context, compressed, compressed_size, rowsize * height);
+    remove_PNG_filter(context, decompressed, width, height, 3, bitdepth);
+    for (size_t row = 0; row < height; row ++) expand_bitpacked_PNG_data(result + row * width, decompressed + row * rowsize + 1, width, bitdepth);
+  }
+  ctxfree(context, decompressed);
+  for (size_t p = 0; p < (size_t) width * height; p ++) if (result[p] > max_palette_index) throw(context, PLUM_ERR_INVALID_COLOR_INDEX);
+  return result;
+}
+
+uint64_t * load_PNG_raw_frame (struct context * context, const void * compressed, size_t compressed_size, uint32_t width, uint32_t height, uint8_t imagetype,
+                               uint8_t bitdepth, bool interlaced) {
+  // imagetype is not 3 here
+  uint64_t * result = ctxmalloc(context, sizeof *result * width * height);
+  unsigned char * decompressed;
+  size_t pixelsize = bitdepth / 8 * channels_per_pixel_PNG[imagetype]; // 0 will be treated as a special value
+  if (interlaced) {
+    size_t widths[] = {(width + 7) / 8, (width + 3) / 8, (width + 3) / 4, (width + 1) / 4, (width + 1) / 2, width / 2, width};
+    size_t heights[] = {(height + 7) / 8, (height + 7) / 8, (height + 3) / 8, (height + 3) / 4, (height + 1) / 4, (height + 1) / 2, height / 2};
+    size_t rowsizes[7];
+    size_t cumulative_size = 0;
+    for (uint_fast8_t pass = 0; pass < 7; pass ++) if (widths[pass] && heights[pass]) {
+      rowsizes[pass] = pixelsize ? pixelsize * widths[pass] + 1 : (((size_t) widths[pass] * bitdepth + 7) / 8 + 1);
+      cumulative_size += rowsizes[pass] * heights[pass];
+    }
+    decompressed = decompress_PNG_data(context, compressed, compressed_size, cumulative_size);
+    unsigned char * current = decompressed;
+    for (uint_fast8_t pass = 0; pass < 7; pass ++) if (widths[pass] && heights[pass]) {
+      load_PNG_raw_frame_pass(context, current, result, heights[pass], widths[pass], width, imagetype, bitdepth, interlaced_PNG_pass_start[pass + 1],
+                              interlaced_PNG_pass_start[pass], interlaced_PNG_pass_step[pass + 1], interlaced_PNG_pass_step[pass], rowsizes[pass]);
+      current += rowsizes[pass] * heights[pass];
+    }
+  } else {
+    size_t rowsize = pixelsize ? pixelsize * width + 1 : (((size_t) width * bitdepth + 7) / 8 + 1);
+    decompressed = decompress_PNG_data(context, compressed, compressed_size, rowsize * height);
+    load_PNG_raw_frame_pass(context, decompressed, result, height, width, width, imagetype, bitdepth, 0, 0, 1, 1, rowsize);
+  }
+  ctxfree(context, decompressed);
+  return result;
+}
+
+void load_PNG_raw_frame_pass (struct context * context, unsigned char * restrict data, uint64_t * restrict output, uint32_t height, uint32_t width,
+                              uint32_t fullwidth, uint8_t imagetype, uint8_t bitdepth, unsigned char coordH, unsigned char coordV, unsigned char offsetH,
+                              unsigned char offsetV, size_t rowsize) {
+  remove_PNG_filter(context, data, width, height, imagetype, bitdepth);
+  for (size_t row = 0; row < height; row ++) {
+    uint64_t * rowoutput = output + (row * offsetV + coordV) * fullwidth;
+    unsigned char * rowdata = data + 1;
+    switch (bitdepth + imagetype) {
+      // since bitdepth must be 8 or 16 here unless imagetype is 0, all combinations are unique
+      case 8: // imagetype = 0, bitdepth = 8
+        for (size_t col = 0; col < width; col ++) rowoutput[col * offsetH + coordH] = (uint64_t) rowdata[col] * 0x10101010101u;
+        break;
+      case 10: // imagetype = 2, bitdepth = 8
+        for (size_t col = 0; col < width; col ++)
+          rowoutput[col * offsetH + coordH] = (rowdata[3 * col] | ((uint64_t) rowdata[3 * col + 1] << 16) | ((uint64_t) rowdata[3 * col + 2] << 32)) * 0x101;
+        break;
+      case 12: // imagetype = 4, bitdepth = 8
+        for (size_t col = 0; col < width; col ++)
+          rowoutput[col * offsetH + coordH] = ((uint64_t) rowdata[2 * col] * 0x10101010101u) | ((uint64_t) (rowdata[2 * col + 1] ^ 0xff) * 0x101000000000000u);
+        break;
+      case 14: // imagetype = 6, bitdepth = 8
+        for (size_t col = 0; col < width; col ++)
+          rowoutput[col * offsetH + coordH] = 0x101 * (rowdata[4 * col] | ((uint64_t) rowdata[4 * col + 1] << 16) |
+                                                       ((uint64_t) rowdata[4 * col + 2] << 32) | ((uint64_t) (rowdata[4 * col + 3] ^ 0xff) << 48));
+        break;
+      case 16: // imagetype = 0, bitdepth = 16
+        for (size_t col = 0; col < width; col ++) rowoutput[col * offsetH + coordH] = (uint64_t) read_be16_unaligned(rowdata + 2 * col) * 0x100010001u;
+        break;
+      case 18: // imagetype = 2, bitdepth = 16
+        for (size_t col = 0; col < width; col ++)
+          rowoutput[col * offsetH + coordH] = read_be16_unaligned(rowdata + 6 * col) | ((uint64_t) read_be16_unaligned(rowdata + 6 * col + 2) << 16) |
+                                              ((uint64_t) read_be16_unaligned(rowdata + 6 * col + 4) << 32);
+        break;
+      case 20: // imagetype = 4, bitdepth = 16
+        for (size_t col = 0; col < width; col ++)
+          rowoutput[col * offsetH + coordH] = ((uint64_t) read_be16_unaligned(rowdata + 4 * col) * 0x100010001u) |
+                                              ((uint64_t) ~read_be16_unaligned(rowdata + 4 * col + 2) << 48);
+        break;
+      case 22: // imagetype = 6, bitdepth = 16
+        for (size_t col = 0; col < width; col ++)
+          rowoutput[col * offsetH + coordH] = read_be16_unaligned(rowdata + 8 * col) | ((uint64_t) read_be16_unaligned(rowdata + 8 * col + 2) << 16) |
+                                              ((uint64_t) read_be16_unaligned(rowdata + 8 * col + 4) << 32) |
+                                              ((uint64_t) ~read_be16_unaligned(rowdata + 8 * col + 6) << 48);
+        break;
+      default: { // imagetype = 0, bitdepth < 8
+        unsigned char * buffer = ctxmalloc(context, width);
+        expand_bitpacked_PNG_data(buffer, rowdata, width, bitdepth);
+        for (size_t col = 0; col < width; col ++) rowoutput[col * offsetH + coordH] = (uint64_t) bitextend16(buffer[col], bitdepth) * 0x100010001u;
+        ctxfree(context, buffer);
+      }
+    }
+    data += rowsize;
+  }
+}
+
+void expand_bitpacked_PNG_data (unsigned char * restrict result, const unsigned char * restrict source, size_t count, uint8_t bitdepth) {
+  switch (bitdepth) {
+    case 1:
+      for (; count > 7; count -= 8) {
+        *(result ++) = !!(*source & 0x80);
+        *(result ++) = !!(*source & 0x40);
+        *(result ++) = !!(*source & 0x20);
+        *(result ++) = !!(*source & 0x10);
+        *(result ++) = !!(*source & 8);
+        *(result ++) = !!(*source & 4);
+        *(result ++) = !!(*source & 2);
+        *(result ++) = *(source ++) & 1;
+      }
+      if (count) for (unsigned char remainder = *source; count; count --, remainder <<= 1) *(result ++) = remainder >> 7;
+      break;
+    case 2:
+      for (; count > 3; count -= 4) {
+        *(result ++) = *source >> 6;
+        *(result ++) = (*source >> 4) & 3;
+        *(result ++) = (*source >> 2) & 3;
+        *(result ++) = *(source ++) & 3;
+      }
+      if (count) for (unsigned char remainder = *source; count; count --, remainder <<= 2) *(result ++) = remainder >> 6;
+      break;
+    case 4:
+      for (; count > 1; count -= 2) {
+        *(result ++) = *source >> 4;
+        *(result ++) = *(source ++) & 15;
+      }
+      if (count) *result = *source >> 4;
+      break;
+    default:
+      memcpy(result, source, count);
+  }
+}
+
+void remove_PNG_filter (struct context * context, unsigned char * restrict data, uint32_t width, uint32_t height, uint8_t imagetype, uint8_t bitdepth) {
+  ptrdiff_t pixelsize = bitdepth / 8 * channels_per_pixel_PNG[imagetype];
+  if (!pixelsize) {
+    pixelsize = 1;
+    width = ((size_t) width * bitdepth + 7) / 8;
+  }
+  if ((size_t) pixelsize * width + 1 > PTRDIFF_MAX) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  ptrdiff_t rowsize = pixelsize * width + 1;
+  for (uint_fast32_t row = 0; row < height; row ++) {
+    unsigned char * rowdata = data + 1;
+    switch (*data) {
+      case 4:
+        for (ptrdiff_t p = 0; p < pixelsize * width; p ++) {
+          int top = row ? rowdata[p - rowsize] : 0, left = (p < pixelsize) ? 0 : rowdata[p - pixelsize];
+          int diagonal = (row && p >= pixelsize) ? rowdata[p - pixelsize - rowsize] : 0;
+          int topdiff = absolute_value(left - diagonal), leftdiff = absolute_value(top - diagonal), diagdiff = absolute_value(left + top - diagonal * 2);
+          rowdata[p] += (leftdiff <= topdiff && leftdiff <= diagdiff) ? left : (topdiff <= diagdiff) ? top : diagonal;
+        }
+        break;
+      case 3:
+        if (row) {
+          for (ptrdiff_t p = 0; p < pixelsize; p ++) rowdata[p] += rowdata[p - rowsize] >> 1;
+          for (ptrdiff_t p = pixelsize; p < pixelsize * width; p ++) rowdata[p] += (rowdata[p - pixelsize] + rowdata[p - rowsize]) >> 1;
+        } else
+          for (ptrdiff_t p = pixelsize; p < pixelsize * width; p ++) rowdata[p] += rowdata[p - pixelsize] >> 1;
+        break;
+      case 2:
+        if (row) for (ptrdiff_t p = 0; p < pixelsize * width; p ++) rowdata[p] += rowdata[p - rowsize];
+        break;
+      case 1:
+        for (ptrdiff_t p = pixelsize; p < pixelsize * width; p ++) rowdata[p] += rowdata[p - pixelsize];
+      case 0:
+        break;
+      default:
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+    data += rowsize;
+  }
+}
+
+void generate_PNG_data (struct context * context) {
+  if (context -> source -> frames > 1) throw(context, PLUM_ERR_NO_MULTI_FRAME);
+  unsigned type = generate_PNG_header(context, NULL);
+  append_PNG_image_data(context, context -> source -> data, type, NULL, NULL);
+  output_PNG_chunk(context, 0x49454e44u, 0, NULL); // IEND
+}
+
+void generate_APNG_data (struct context * context) {
+  if (context -> source -> frames > 0x40000000u) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  struct plum_rectangle * boundaries = get_frame_boundaries(context, false);
+  unsigned type = generate_PNG_header(context, boundaries);
+  uint32_t loops = 1;
+  const struct plum_metadata * metadata = plum_find_metadata(context -> source, PLUM_METADATA_LOOP_COUNT);
+  if (metadata) {
+    loops = *(uint32_t *) metadata -> data;
+    if (loops > 0x7fffffffu) loops = 0; // too many loops, so just make it loop forever
+  }
+  const uint64_t * durations = NULL;
+  const uint8_t * disposals = NULL;
+  size_t duration_count = 0, disposal_count = 0;
+  if (metadata = plum_find_metadata(context -> source, PLUM_METADATA_FRAME_DURATION)) {
+    durations = metadata -> data;
+    duration_count = metadata -> size / sizeof(uint64_t);
+  }
+  if (metadata = plum_find_metadata(context -> source, PLUM_METADATA_FRAME_DISPOSAL)) {
+    disposals = metadata -> data;
+    disposal_count = metadata -> size;
+  }
+  uint32_t chunkID = 0;
+  uint_fast8_t last_disposal = (disposal_count >= context -> source -> frames) ? disposals[context -> source -> frames - 1] : 0;
+  unsigned char animation_data[8];
+  write_be32_unaligned(animation_data + 4, loops);
+  int64_t duration_remainder = 0;
+  if ((duration_count && *durations) || context -> source -> frames == 1) {
+    write_be32_unaligned(animation_data, context -> source -> frames);
+    output_PNG_chunk(context, 0x6163544cu, sizeof animation_data, animation_data); // acTL
+    uint_fast8_t disposal = disposal_count ? *disposals : 0;
+    append_APNG_frame_header(context, duration_count ? *durations : 0, disposal, last_disposal, &chunkID, &duration_remainder, NULL);
+    last_disposal = disposal;
+  } else {
+    write_be32_unaligned(animation_data, context -> source -> frames - 1);
+    output_PNG_chunk(context, 0x6163544cu, sizeof animation_data, animation_data); // acTL
+  }
+  append_PNG_image_data(context, context -> source -> data, type, NULL, NULL);
+  size_t framesize = (size_t) context -> source -> width * context -> source -> height;
+  if (!context -> source -> palette) framesize = plum_color_buffer_size(framesize, context -> source -> color_format);
+  for (uint_fast32_t frame = 1; frame < context -> source -> frames; frame ++) {
+    const struct plum_rectangle * rectangle = boundaries ? boundaries + frame : NULL;
+    uint_fast8_t disposal = (disposal_count > frame) ? disposals[frame] : 0;
+    append_APNG_frame_header(context, (duration_count > frame) ? durations[frame] : 0, disposal, last_disposal, &chunkID, &duration_remainder, rectangle);
+    last_disposal = disposal;
+    append_PNG_image_data(context, context -> source -> data8 + framesize * frame, type, &chunkID, rectangle);
+  }
+  ctxfree(context, boundaries);
+  output_PNG_chunk(context, 0x49454e44u, 0, NULL); // IEND
+}
+
+unsigned generate_PNG_header (struct context * context, struct plum_rectangle * restrict boundaries) {
+  // returns the selected type of image: 0, 1, 2, 3: paletted (1 << type bits), 4, 5: 8-bit RGB (without and with alpha), 6, 7: 16-bit RGB
+  // also updates the frame boundaries for APNG images (ensuring that frame 0 and frames with nonempty pixels outside their boundaries become full size)
+  byteoutput(context, 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a);
+  bool transparency;
+  if (boundaries) {
+    *boundaries = (struct plum_rectangle) {.top = 0, .left = 0, .width = context -> source -> width, .height = context -> source -> height};
+    adjust_frame_boundaries(context -> source, boundaries);
+    transparency = image_rectangles_have_transparency(context -> source, boundaries);
+  } else
+    transparency = image_has_transparency(context -> source);
+  uint32_t depth = get_color_depth(context -> source);
+  if (!transparency) depth &= 0xffffffu;
+  uint_fast8_t type;
+  if (context -> source -> palette)
+    if (context -> source -> max_palette_index < 2)
+      type = 0;
+    else if (context -> source -> max_palette_index < 4)
+      type = 1;
+    else if (context -> source -> max_palette_index < 16)
+      type = 2;
+    else
+      type = 3;
+  else if (bit_depth_less_than(depth, 0x8080808u))
+    type = 4 + transparency;
+  else
+    type = 6 + transparency;
+  append_PNG_header_chunks(context, type, depth);
+  if (type < 4) append_PNG_palette_data(context, transparency);
+  const struct plum_metadata * background = plum_find_metadata(context -> source, PLUM_METADATA_BACKGROUND);
+  if (background) append_PNG_background_chunk(context, background -> data, type);
+  return type;
+}
+
+void append_PNG_header_chunks (struct context * context, unsigned type, uint32_t depth) {
+  if (context -> source -> width > 0x7fffffffu || context -> source -> height > 0x7fffffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  unsigned char header[13];
+  write_be32_unaligned(header, context -> image -> width);
+  write_be32_unaligned(header + 4, context -> image -> height);
+  header[8] = (type < 4) ? 1 << type : (8 << (type >= 6));
+  header[9] = (type >= 4) ? 2 + 4 * (type & 1) : 3;
+  bytewrite(header + 10, 0, 0, 0);
+  output_PNG_chunk(context, 0x49484452u, sizeof header, header); // IHDR
+  unsigned char depthdata[4];
+  write_le32_unaligned(depthdata, depth); // this will write each byte of depth in the expected position
+  if (type < 4) {
+    if (*depthdata > 8) *depthdata = 8;
+    if (depthdata[1] > 8) depthdata[1] = 8;
+    if (depthdata[2] > 8) depthdata[2] = 8;
+  }
+  output_PNG_chunk(context, 0x73424954u, 3 + ((type & 5) == 5), depthdata); // sBIT
+}
+
+void append_PNG_palette_data (struct context * context, bool use_alpha) {
+  uint32_t color_buffer[256];
+  plum_convert_colors(color_buffer, context -> source -> palette, context -> source -> max_palette_index + 1, PLUM_COLOR_32 | PLUM_ALPHA_INVERT,
+                      context -> source -> color_format);
+  unsigned char data[768];
+  for (uint_fast16_t index = 0; index <= context -> source -> max_palette_index; index ++)
+    bytewrite(data + index * 3, color_buffer[index], color_buffer[index] >> 8, color_buffer[index] >> 16);
+  output_PNG_chunk(context, 0x504c5445u, 3 * (context -> source -> max_palette_index + 1), data); // PLTE
+  if (use_alpha) {
+    unsigned char alpha[256];
+    for (uint_fast16_t index = 0; index <= context -> source -> max_palette_index; index ++) alpha[index] = color_buffer[index] >> 24;
+    output_PNG_chunk(context, 0x74524e53u, context -> source -> max_palette_index + 1, alpha); // tRNS
+  }
+}
+
+void append_PNG_background_chunk (struct context * context, const void * restrict data, unsigned type) {
+  if (type >= 4) {
+    unsigned char chunkdata[6];
+    uint64_t color;
+    plum_convert_colors(&color, data, 1, PLUM_COLOR_64, context -> source -> color_format);
+    if (type < 6) color = (color >> 8) & 0xff00ff00ffu;
+    write_be16_unaligned(chunkdata, color);
+    write_be16_unaligned(chunkdata + 2, color >> 16);
+    write_be16_unaligned(chunkdata + 4, color >> 32);
+    output_PNG_chunk(context, 0x624b4744u, sizeof chunkdata, chunkdata); // bKGD
+  } else {
+    size_t size = plum_color_buffer_size(1, context -> source -> color_format);
+    const unsigned char * current = context -> source -> palette;
+    for (uint_fast16_t pos = 0; pos <= context -> source -> max_palette_index; pos ++, current += size) if (!memcmp(current, data, size)) {
+      unsigned char byte = pos;
+      output_PNG_chunk(context, 0x624b4744u, 1, &byte); // bKGD
+      return;
+    }
+  }
+}
+
+void append_PNG_image_data (struct context * context, const void * restrict data, unsigned type, uint32_t * restrict chunkID,
+                            const struct plum_rectangle * boundaries) {
+  // chunkID counts animation data chunks (fcTL, fdAT); if chunkID is null, emit IDAT chunks instead
+  size_t raw, size;
+  unsigned char * uncompressed = generate_PNG_frame_data(context, data, type, &raw, boundaries);
+  // if chunkID is non-null, compress_PNG_data will insert four bytes of padding before the compressed data so this function can write a chunk ID there
+  unsigned char * compressed = compress_PNG_data(context, uncompressed, raw, chunkID ? 4 : 0, &size);
+  ctxfree(context, uncompressed);
+  unsigned char * current = compressed;
+  if (chunkID) {
+    current += 4;
+    while (size > 0x7ffffffbu) {
+      if (*chunkID > 0x7fffffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+      write_be32_unaligned(current - 4, (*chunkID) ++);
+      output_PNG_chunk(context, 0x66644154u, 0x7ffffffcu, current - 4); // fdAT
+      current += 0x7ffffff8u;
+      size -= 0x7ffffff8u;
+    }
+    if (size) {
+      if (*chunkID > 0x7fffffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+      write_be32_unaligned(current - 4, (*chunkID) ++);
+      output_PNG_chunk(context, 0x66644154u, size + 4, current - 4); // fdAT
+    }
+  } else {
+    while (size > 0x7fffffffu) {
+      output_PNG_chunk(context, 0x49444154u, 0x7ffffffcu, current); // IDAT
+      current += 0x7ffffffcu;
+      size -= 0x7ffffffcu;
+    }
+    if (size) output_PNG_chunk(context, 0x49444154u, size, current); // IDAT
+  }
+  ctxfree(context, compressed);
+}
+
+void append_APNG_frame_header (struct context * context, uint64_t duration, uint8_t disposal, uint8_t previous, uint32_t * restrict chunkID,
+                               int64_t * restrict duration_remainder, const struct plum_rectangle * boundaries) {
+  if (*chunkID > 0x7fffffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  uint32_t numerator = 0, denominator = 0;
+  if (duration) {
+    if (duration == 1) duration = 0;
+    duration = adjust_frame_duration(duration, duration_remainder);
+    calculate_frame_duration_fraction(duration, 0xffffu, &numerator, &denominator);
+    if (!numerator) {
+      denominator = 0;
+      update_frame_duration_remainder(duration, 0, duration_remainder);
+    } else {
+      if (!denominator) {
+        // duration too large (calculation returned infinity), so max it out
+        numerator = 0xffffu;
+        denominator = 1;
+      }
+      update_frame_duration_remainder(duration, ((uint64_t) 1000000000u * numerator + denominator / 2) / denominator, duration_remainder);
+    }
+  }
+  unsigned char data[26];
+  write_be32_unaligned(data, (*chunkID) ++);
+  if (boundaries) {
+    write_be32_unaligned(data + 4, boundaries -> width);
+    write_be32_unaligned(data + 8, boundaries -> height);
+    write_be32_unaligned(data + 12, boundaries -> left);
+    write_be32_unaligned(data + 16, boundaries -> top);
+  } else {
+    write_be32_unaligned(data + 4, context -> source -> width);
+    write_be32_unaligned(data + 8, context -> source -> height);
+    memset(data + 12, 0, 8);
+  }
+  write_be16_unaligned(data + 20, numerator);
+  write_be16_unaligned(data + 22, denominator);
+  bytewrite(data + 24, disposal % PLUM_DISPOSAL_REPLACE, previous < PLUM_DISPOSAL_REPLACE);
+  output_PNG_chunk(context, 0x6663544cu, sizeof data, data); // fcTL
+}
+
+void output_PNG_chunk (struct context * context, uint32_t type, uint32_t size, const void * restrict data) {
+  unsigned char * node = append_output_node(context, size + 12);
+  write_be32_unaligned(node, size);
+  write_be32_unaligned(node + 4, type);
+  if (size) memcpy(node + 8, data, size);
+  write_be32_unaligned(node + size + 8, compute_PNG_CRC(node + 4, size + 4));
+}
+
+unsigned char * generate_PNG_frame_data (struct context * context, const void * restrict data, unsigned type, size_t * restrict size,
+                                         const struct plum_rectangle * boundaries) {
+  struct plum_rectangle framearea;
+  if (boundaries)
+    framearea = *boundaries;
+  else
+    framearea = (const struct plum_rectangle) {.left = 0, .top = 0, .width = context -> source -> width, .height = context -> source -> height};
+  size_t rowsize, pixelsize = bytes_per_channel_PNG[type];
+  if (pixelsize)
+    rowsize = framearea.width * pixelsize + 1;
+  else
+    rowsize = (((size_t) framearea.width << type) + 7) / 8 + 1;
+  *size = rowsize * framearea.height;
+  if (*size > SIZE_MAX - 2 || rowsize > SIZE_MAX / 6 || *size / rowsize != framearea.height) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  // allocate and initialize two extra bytes so the compressor can operate safely
+  unsigned char * result = ctxcalloc(context, *size + 2);
+  unsigned char * rowbuffer = ctxcalloc(context, 6 * rowsize);
+  size_t rowoffset = (type >= 4) ? plum_color_buffer_size(context -> source -> width, context -> source -> color_format) : context -> source -> width;
+  size_t dataoffset = (type >= 4) ? plum_color_buffer_size(framearea.left, context -> source -> color_format) : framearea.left;
+  dataoffset += rowoffset * framearea.top;
+  for (uint_fast32_t row = 0; row < framearea.height; row ++) {
+    generate_PNG_row_data(context, (const unsigned char *) data + dataoffset + rowoffset * row, rowbuffer, framearea.width, type);
+    filter_PNG_rows(rowbuffer, rowbuffer + 5 * rowsize, framearea.width, type);
+    memcpy(rowbuffer + 5 * rowsize, rowbuffer, rowsize);
+    memcpy(result + rowsize * row, rowbuffer + rowsize * select_PNG_filtered_row(rowbuffer, rowsize), rowsize);
+  }
+  ctxfree(context, rowbuffer);
+  return result;
+}
+
+void generate_PNG_row_data (struct context * context, const void * restrict data, unsigned char * restrict output, size_t width, unsigned type) {
+  *(output ++) = 0;
+  switch (type) {
+    case 0: case 1: case 2: {
+      const unsigned char * indexes = data;
+      uint_fast8_t dataword = 0, bits = 0, pixelbits = 1 << type;
+      for (uint_fast32_t p = 0; p < width; p ++) {
+        dataword = (dataword << pixelbits) | *(indexes ++);
+        bits += pixelbits;
+        if (bits == 8) {
+          *(output ++) = dataword;
+          bits = 0;
+        }
+      }
+      if (bits) *output = dataword << (8 - bits);
+    } break;
+    case 3:
+      memcpy(output, data, width);
+      break;
+    case 4: case 5: {
+      uint32_t * pixels = ctxmalloc(context, sizeof *pixels * width);
+      plum_convert_colors(pixels, data, width, PLUM_COLOR_32 | PLUM_ALPHA_INVERT, context -> source -> color_format);
+      if (type == 5)
+        for (uint_fast32_t p = 0; p < width; p ++) write_le32_unaligned(output + 4 * p, pixels[p]);
+      else
+        for (uint_fast32_t p = 0; p < width; p ++) output += byteappend(output, pixels[p], pixels[p] >> 8, pixels[p] >> 16);
+      ctxfree(context, pixels);
+    } break;
+    case 6: case 7: {
+      uint64_t * pixels = ctxmalloc(context, sizeof *pixels * width);
+      plum_convert_colors(pixels, data, width, PLUM_COLOR_64 | PLUM_ALPHA_INVERT, context -> source -> color_format);
+      if (type == 7)
+        for (uint_fast32_t p = 0; p < width; p ++)
+          output += byteappend(output, pixels[p] >> 8, pixels[p], pixels[p] >> 24, pixels[p] >> 16, pixels[p] >> 40, pixels[p] >> 32,
+                               pixels[p] >> 56, pixels[p] >> 48);
+      else
+        for (uint_fast32_t p = 0; p < width; p ++)
+          output += byteappend(output, pixels[p] >> 8, pixels[p], pixels[p] >> 24, pixels[p] >> 16, pixels[p] >> 40, pixels[p] >> 32);
+      ctxfree(context, pixels);
+    }
+  }
+}
+
+void filter_PNG_rows (unsigned char * restrict rowdata, const unsigned char * restrict previous, size_t count, unsigned type) {
+  ptrdiff_t rowsize, pixelsize = bytes_per_channel_PNG[type];
+  // rowsize doesn't include the filter type byte
+  if (pixelsize)
+    rowsize = count * pixelsize;
+  else {
+    rowsize = ((count << type) + 7) / 8;
+    pixelsize = 1; // treat packed bits as a single pixel
+  }
+  rowdata ++;
+  previous ++;
+  unsigned char * output = rowdata + rowsize;
+  *(output ++) = 1;
+  for (ptrdiff_t p = 0; p < pixelsize; p ++) *(output ++) = rowdata[p];
+  for (ptrdiff_t p = pixelsize; p < rowsize; p ++) *(output ++) = rowdata[p] - rowdata[p - pixelsize];
+  *(output ++) = 2;
+  for (ptrdiff_t p = 0; p < rowsize; p ++) *(output ++) = rowdata[p] - previous[p];
+  *(output ++) = 3;
+  for (ptrdiff_t p = 0; p < pixelsize; p ++) *(output ++) = rowdata[p] - (previous[p] >> 1);
+  for (ptrdiff_t p = pixelsize; p < rowsize; p ++) *(output ++) = rowdata[p] - ((previous[p] + rowdata[p - pixelsize]) >> 1);
+  *(output ++) = 4;
+  for (ptrdiff_t p = 0; p < rowsize; p ++) {
+    int top = previous[p], left = (p >= pixelsize) ? rowdata[p - pixelsize] : 0, diagonal = (p >= pixelsize) ? previous[p - pixelsize] : 0;
+    int topdiff = absolute_value(left - diagonal), leftdiff = absolute_value(top - diagonal), diagdiff = absolute_value(left + top - diagonal * 2);
+    *(output ++) = rowdata[p] - ((leftdiff <= topdiff && leftdiff <= diagdiff) ? left : (topdiff <= diagdiff) ? top : diagonal);
+  }
+}
+
+unsigned char select_PNG_filtered_row (const unsigned char * rowdata, size_t rowsize) {
+  // recommended by the standard: treat each byte as signed and pick the filter that results in the smallest sum of absolute values
+  // ties are broken by smallest filter number, because lower-numbered filters are simpler than higher-numbered filters
+  uint_fast64_t best_score = 0;
+  for (size_t p = 0; p < rowsize; p ++, rowdata ++) best_score += (*rowdata >= 0x80) ? 0x100 - *rowdata : *rowdata;
+  uint_fast8_t best = 0;
+  for (uint_fast8_t current = 1; current < 5; current ++) {
+    uint_fast64_t current_score = 0;
+    for (size_t p = 0; p < rowsize; p ++, rowdata ++) current_score += (*rowdata >= 0x80) ? 0x100 - *rowdata : *rowdata;
+    if (current_score < best_score) {
+      best = current;
+      best_score = current_score;
+    }
+  }
+  return best;
+}
+
+void load_PNM_data (struct context * context, unsigned flags, size_t limit) {
+  struct PNM_image_header * headers = NULL;
+  size_t offset = 0;
+  context -> image -> type = PLUM_IMAGE_PNM;
+  // all image fields are zero-initialized, so the sizes are set to 0
+  do {
+    if (context -> image -> frames == 0xffffffffu) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+    headers = ctxrealloc(context, headers, (context -> image -> frames + 1) * sizeof *headers);
+    struct PNM_image_header * header = headers + (context -> image -> frames ++);
+    load_PNM_header(context, offset, header);
+    if (context -> size - header -> datastart < header -> datalength) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    if (header -> width > context -> image -> width) context -> image -> width = header -> width;
+    if (header -> height > context -> image -> height) context -> image -> height = header -> height;
+    validate_image_size(context, limit);
+    offset = header -> datastart + header -> datalength;
+    skip_PNM_whitespace(context, &offset);
+  } while (offset < context -> size);
+  allocate_framebuffers(context, flags, false);
+  add_PNM_bit_depth_metadata(context, headers);
+  struct plum_rectangle * frameareas = add_frame_area_metadata(context);
+  uint64_t * buffer = ctxmalloc(context, sizeof *buffer * context -> image -> width * context -> image -> height);
+  offset = plum_color_buffer_size((size_t) context -> image -> width * context -> image -> height, flags);
+  for (uint_fast32_t frame = 0; frame < context -> image -> frames; frame ++) {
+    load_PNM_frame(context, headers + frame, buffer);
+    frameareas[frame] = (struct plum_rectangle) {.left = 0, .top = 0, .width = headers[frame].width, .height = headers[frame].height};
+    plum_convert_colors(context -> image -> data8 + offset * frame, buffer, (size_t) context -> image -> width * context -> image -> height, flags,
+                        PLUM_COLOR_64 | PLUM_ALPHA_INVERT);
+  }
+  ctxfree(context, buffer);
+  ctxfree(context, headers);
+}
+
+void load_PNM_header (struct context * context, size_t offset, struct PNM_image_header * restrict header) {
+  if (context -> size - offset < 8) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (bytematch(context -> data, 0xef, 0xbb, 0xbf)) offset += 3; // if a broken text editor somehow inserted a UTF-8 BOM, skip it
+  if (context -> data[offset ++] != 0x50) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  header -> type = context -> data[offset ++] - 0x30;
+  if (!header -> type || header -> type > 7 || !is_whitespace(context -> data[offset])) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (header -> type == 7) {
+    load_PAM_header(context, offset, header);
+    return;
+  }
+  uint32_t dimensions[3];
+  dimensions[2] = 1;
+  read_PNM_numbers(context, &offset, dimensions, 2 + (header -> type != 1 && header -> type != 4));
+  if (!(*dimensions && dimensions[1] && dimensions[2]) || dimensions[2] > 0xffffu || offset == context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  header -> width = *dimensions;
+  header -> height = dimensions[1];
+  header -> maxvalue = dimensions[2];
+  header -> datastart = ++ offset;
+  if (!plum_check_valid_image_size(header -> width, header -> height, 1)) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  switch (header -> type) {
+    case 5: case 6:
+      header -> datalength = (size_t) header -> width * header -> height * (1 + (header -> maxvalue > 0xff));
+      if (header -> type == 6) header -> datalength *= 3;
+      break;
+    case 4:
+      header -> datalength = (size_t) ((header -> width - 1) / 8 + 1) * header -> height;
+      break;
+    default: {
+      header -> datalength = context -> size - offset;
+      size_t minchars = (header -> type == 3) ? 6 : header -> type; // minimum characters per pixel for each type
+      if (header -> datalength < minchars * header -> width * header -> height - 1) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+  }
+}
+
+void load_PAM_header (struct context * context, size_t offset, struct PNM_image_header * restrict header) {
+  unsigned fields = 15; // bits 0-3: width, height, max, depth (bit set indicates the field hasn't been read yet)
+  uint32_t value, depth;
+  while (true) {
+    skip_PNM_line(context, &offset);
+    skip_PNM_whitespace(context, &offset);
+    unsigned length = next_PNM_token_length(context, offset);
+    if (length == 6 && bytematch(context -> data + offset, 0x45, 0x4e, 0x44, 0x48, 0x44, 0x52)) { // ENDHDR
+      offset += 6;
+      break;
+    } else if (length == 5 && bytematch(context -> data + offset, 0x57, 0x49, 0x44, 0x54, 0x48)) { // WIDTH
+      offset += 5;
+      if (!(fields & 1)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      read_PNM_numbers(context, &offset, &value, 1);
+      if (!value) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      header -> width = value;
+      fields &= ~1u;
+    } else if (length == 6 && bytematch(context -> data + offset, 0x48, 0x45, 0x49, 0x47, 0x48, 0x54)) { // HEIGHT
+      offset += 6;
+      if (!(fields & 2)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      read_PNM_numbers(context, &offset, &value, 1);
+      if (!value) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      header -> height = value;
+      fields &= ~2u;
+    } else if (length == 6 && bytematch(context -> data + offset, 0x4d, 0x41, 0x58, 0x56, 0x41, 0x4c)) { // MAXVAL
+      offset += 6;
+      if (!(fields & 4)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      read_PNM_numbers(context, &offset, &value, 1);
+      if (!value || value > 0xffffu) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      header -> maxvalue = value;
+      fields &= ~4u;
+    } else if (length == 5 && bytematch(context -> data + offset, 0x44, 0x45, 0x50, 0x54, 0x48)) { // DEPTH
+      offset += 5;
+      if (!(fields & 8)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      read_PNM_numbers(context, &offset, &depth, 1);
+      fields &= ~8u;
+    } else if (length == 8 && bytematch(context -> data + offset, 0x54, 0x55, 0x50, 0x4c, 0x54, 0x59, 0x50, 0x45)) { // TUPLTYPE
+      if (header -> type != 7) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      offset += 8;
+      skip_PNM_whitespace(context, &offset);
+      // while the TUPLTYPE line is, by the spec, not tokenized, the only recognized tuple types are a single token
+      length = next_PNM_token_length(context, offset);
+      if (length == 13 && bytematch(context -> data + offset, 0x42, 0x4c, 0x41, 0x43, 0x4b, 0x41, 0x4e, 0x44, 0x57, 0x48, 0x49, 0x54, 0x45)) // BLACKANDWHITE
+        header -> type = 11;
+      else if (length == 9 && bytematch(context -> data + offset, 0x47, 0x52, 0x41, 0x59, 0x53, 0x43, 0x41, 0x4c, 0x45)) // GRAYSCALE
+        header -> type = 12;
+      else if (length == 3 && bytematch(context -> data + offset, 0x52, 0x47, 0x42)) // RGB
+        header -> type = 13;
+      else if (length == 19 && bytematch(context -> data + offset, 0x42, 0x4c, 0x41, 0x43, 0x4b, 0x41, 0x4e, 0x44, 0x57, 0x48,
+                                                                   0x49, 0x54, 0x45, 0x5f, 0x41, 0x4c, 0x50, 0x48, 0x41)) // BLACKANDWHITE_ALPHA
+        header -> type = 14;
+      else if (length == 15 && bytematch(context -> data + offset, 0x47, 0x52, 0x41, 0x59, 0x53, 0x43, 0x41, 0x4c, 0x45, 0x5f,
+                                                                   0x41, 0x4c, 0x50, 0x48, 0x41)) // GRAYSCALE_ALPHA
+        header -> type = 15;
+      else if (length == 9 && bytematch(context -> data + offset, 0x52, 0x47, 0x42, 0x5f, 0x41, 0x4c, 0x50, 0x48, 0x41)) // RGB_ALPHA
+        header -> type = 16;
+      else
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+      offset += length;
+    } else
+      throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  }
+  if (fields || header -> type == 7) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (!plum_check_valid_image_size(header -> width, header -> height, 1)) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+  static const unsigned char components[] = {1, 1, 3, 2, 2, 4};
+  if (depth != components[header -> type - 11]) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (header -> maxvalue != 1 && (header -> type == 11 || header -> type == 14)) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  skip_PNM_line(context, &offset);
+  header -> datastart = offset;
+  header -> datalength = (size_t) header -> width * header -> height * depth;
+  if (header -> maxvalue > 0xff) header -> datalength *= 2;
+}
+
+void skip_PNM_whitespace (struct context * context, size_t * restrict offset) {
+  while (*offset < context -> size)
+    if (context -> data[*offset] == 0x23) // '#'
+      while (*offset < context -> size && context -> data[*offset] != 10) ++ *offset;
+    else if (is_whitespace(context -> data[*offset]))
+      ++ *offset;
+    else
+      break;
+}
+
+void skip_PNM_line (struct context * context, size_t * restrict offset) {
+  for (bool comment = false; *offset < context -> size && context -> data[*offset] != 10; ++ *offset)
+    if (!comment)
+      if (context -> data[*offset] == 0x23) // '#'
+        comment = true;
+      else if (!is_whitespace(context -> data[*offset]))
+        throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+  if (*offset < context -> size) ++ *offset;
+}
+
+unsigned next_PNM_token_length (struct context * context, size_t offset) {
+  // stops at 20 because the longest recognized token is 19 characters long
+  unsigned result = 0;
+  while (offset < context -> size && result < 20 && !is_whitespace(context -> data[offset])) result ++, offset ++;
+  return (result == 20) ? 0 : result;
+}
+
+void read_PNM_numbers (struct context * context, size_t * restrict offset, uint32_t * restrict result, size_t count) {
+  while (count --) {
+    skip_PNM_whitespace(context, offset);
+    if (*offset >= context -> size || context -> data[*offset] < 0x30 || context -> data[*offset] > 0x39) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    uint_fast64_t current = context -> data[(*offset) ++] - 0x30; // 64-bit so it can catch overflows
+    while (*offset < context -> size && context -> data[*offset] >= 0x30 && context -> data[*offset] <= 0x39) {
+      current = current * 10 + context -> data[(*offset) ++] - 0x30;
+      if (current > 0xffffffffu) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+    if (*offset < context -> size && !is_whitespace(context -> data[*offset])) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    *(result ++) = current;
+  }
+}
+
+void add_PNM_bit_depth_metadata (struct context * context, const struct PNM_image_header * headers) {
+  uint_fast8_t colordepth = 0, alphadepth = 0;
+  bool colored = false;
+  for (uint_fast32_t frame = 0; frame < context -> image -> frames; frame ++) {
+    uint_fast8_t depth = bit_width(headers[frame].maxvalue);
+    if (headers[frame].type == 3 || headers[frame].type == 6 || headers[frame].type == 13 || headers[frame].type == 16) colored = true;
+    if (colordepth < depth) colordepth = depth;
+    if (headers[frame].type >= 14 && alphadepth < depth) alphadepth = depth;
+  }
+  if (colored)
+    add_color_depth_metadata(context, colordepth, colordepth, colordepth, alphadepth, 0);
+  else
+    add_color_depth_metadata(context, 0, 0, 0, alphadepth, colordepth);
+}
+
+void load_PNM_frame (struct context * context, const struct PNM_image_header * restrict header, uint64_t * restrict buffer) {
+  size_t offset = header -> datastart, imagewidth = context -> image -> width, imageheight = context -> image -> height;
+  if (header -> width < imagewidth)
+    for (uint_fast32_t row = 0; row < header -> height; row ++)
+      for (size_t p = imagewidth * row + header -> width; p < imagewidth * (row + 1); p ++) buffer[p] = 0;
+  if (header -> height < imageheight)
+    for (size_t p = imagewidth * header -> height; p < imagewidth * imageheight; p ++) buffer[p] = 0;
+  if (header -> type == 4) {
+    load_PNM_bit_frame(context, header -> width, header -> height, offset, buffer);
+    return;
+  }
+  uint32_t values[4];
+  values[3] = header -> maxvalue;
+  uint_fast8_t bits = bit_width(header -> maxvalue);
+  if (((header -> maxvalue + 1) >> (bits - 1)) == 1) bits = 0; // check if header -> maxvalue isn't (1 << bits) - 1, avoiding UB
+  for (uint_fast32_t row = 0; row < header -> height; row ++) for (size_t p = imagewidth * row; p < imagewidth * row + header -> width; p ++) {
+    switch (header -> type) {
+      case 1:
+        // sometimes the 0s and 1s are not delimited at all here, so it needs a special parser
+        while (offset < context -> size && (context -> data[offset] & ~1u) != 0x30) offset ++;
+        if (offset >= context -> size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+        values[2] = values[1] = *values = ~context -> data[offset ++] & 1;
+        break;
+      case 2:
+        read_PNM_numbers(context, &offset, values, 1);
+        values[2] = values[1] = *values;
+        break;
+      case 3:
+        read_PNM_numbers(context, &offset, values, 3);
+        break;
+      case 6: case 13: case 16:
+        if (header -> maxvalue > 0xff) {
+          *values = read_be16_unaligned(context -> data + offset);
+          offset += 2;
+          values[1] = read_be16_unaligned(context -> data + offset);
+          offset += 2;
+          values[2] = read_be16_unaligned(context -> data + offset);
+          offset += 2;
+          if (header -> type >= 14) {
+            values[3] = read_be16_unaligned(context -> data + offset);
+            offset += 2;
+          }
+        } else {
+          *values = context -> data[offset ++];
+          values[1] = context -> data[offset ++];
+          values[2] = context -> data[offset ++];
+          if (header -> type >= 14) values[3] = context -> data[offset ++];
+        }
+        break;
+      default:
+        if (header -> maxvalue > 0xff) {
+          *values = read_be16_unaligned(context -> data + offset);
+          offset += 2;
+          if (header -> type >= 14) {
+            values[3] = read_be16_unaligned(context -> data + offset);
+            offset += 2;
+          }
+        } else {
+          *values = context -> data[offset ++];
+          if (header -> type >= 14) values[3] = context -> data[offset ++];
+        }
+        values[2] = values[1] = *values;
+    }
+    buffer[p] = 0;
+    for (uint_fast8_t color = 0; color < 4; color ++) {
+      uint64_t converted;
+      if (bits)
+        converted = bitextend16(values[color], bits);
+      else
+        converted = (values[color] * 0xffffu + header -> maxvalue / 2) / header -> maxvalue;
+      buffer[p] |= converted << (color * 16);
+    }
+  }
+}
+
+void load_PNM_bit_frame (struct context * context, size_t width, size_t height, size_t offset, uint64_t * restrict buffer) {
+  for (size_t row = 0; row < height; row ++) {
+    size_t p = row * context -> image -> width;
+    for (size_t col = 0; col < (width & bitnegate(7)); col += 8) {
+      uint_fast8_t value = context -> data[offset ++];
+      for (uint_fast8_t bit = 0; bit < 8; bit ++) {
+        buffer[p ++] = (value & 0x80) ? 0xffff000000000000u : 0xffffffffffffffffu;
+        value <<= 1;
+      }
+    }
+    if (width & 7) {
+      uint_fast8_t value = context -> data[offset ++];
+      for (uint_fast8_t bit = 0; bit < (width & 7); bit ++) {
+        buffer[p ++] = (value & 0x80) ? 0xffff000000000000u : 0xffffffffffffffffu;
+        value <<= 1;
+      }
+    }
+  }
+}
+
+void generate_PNM_data (struct context * context) {
+  struct plum_rectangle * boundaries = get_frame_boundaries(context, true);
+  uint32_t depth = get_color_depth(context -> source);
+  bool transparency;
+  if (boundaries) {
+    adjust_frame_boundaries(context -> source, boundaries);
+    bool extendwidth = true, extendheight = true;
+    for (uint_fast32_t frame = 0; (extendwidth || extendheight) && frame < context -> source -> frames; frame ++) {
+      if (boundaries[frame].width == context -> source -> width) extendwidth = false;
+      if (boundaries[frame].height == context -> source -> height) extendheight = false;
+    }
+    if (extendwidth) boundaries -> width = context -> source -> width;
+    if (extendheight) boundaries -> height = context -> source -> height;
+    transparency = image_rectangles_have_transparency(context -> source, boundaries);
+  } else
+    transparency = image_has_transparency(context -> source);
+  if (!transparency) depth &= 0xffffffu;
+  uint_fast8_t max = 0;
+  for (uint_fast8_t p = 0; p < 32; p += 8) if (((depth >> p) & 0xff) > max) max = (depth >> p) & 0xff;
+  uint64_t * buffer;
+  if (context -> source -> palette) {
+    buffer = ctxmalloc(context, sizeof *buffer * (context -> source -> max_palette_index + 1));
+    plum_convert_colors(buffer, context -> source -> palette, context -> source -> max_palette_index + 1, PLUM_COLOR_64 | PLUM_ALPHA_INVERT,
+                        context -> source -> color_format);
+  } else
+    buffer = ctxmalloc(context, sizeof *buffer * context -> source -> width * context -> source -> height);
+  uint32_t * sizes = NULL;
+  if (boundaries) {
+    sizes = ctxmalloc(context, sizeof *sizes * 2 * context -> source -> frames);
+    for (size_t frame = 0; frame < context -> source -> frames; frame ++) {
+      sizes[frame * 2] = boundaries[frame].width;
+      sizes[frame * 2 + 1] = boundaries[frame].height;
+    }
+    ctxfree(context, boundaries);
+  } else if (transparency && context -> source -> frames > 1) {
+    sizes = get_true_PNM_frame_sizes(context);
+    transparency = !sizes;
+  }
+  if (transparency)
+    generate_PAM_data(context, sizes, max, buffer);
+  else
+    generate_PPM_data(context, sizes, max, buffer);
+  ctxfree(context, sizes);
+  ctxfree(context, buffer);
+}
+
+uint32_t * get_true_PNM_frame_sizes (struct context * context) {
+  // returns width, height pairs for each frame if the only transparency in those frames is an empty border on the bottom and right edges
+  unsigned char format = context -> source -> color_format & PLUM_COLOR_MASK;
+  uint64_t mask = alpha_component_masks[format], check = 0, color = get_empty_color(context -> source);
+  if (context -> source -> color_format & PLUM_ALPHA_INVERT) check = mask;
+  uint32_t * result = ctxmalloc(context, sizeof *result * 2 * context -> source -> frames);
+  size_t width, height, offset = (size_t) context -> source -> width * context -> source -> height;
+  if (context -> source -> palette) {
+    unsigned char colorclass[0x100]; // 0 for a solid color, 1 for empty pixels (fully transparent background), 2 for everything else
+    #define checkclasses(bits) do                                                     \
+      for (uint_fast16_t p = 0; p <= context -> source -> max_palette_index; p ++)    \
+        if (context -> source -> palette ## bits[p] == color)                         \
+          colorclass[p] = 1;                                                          \
+        else if ((context -> source -> palette ## bits[p] & mask) == check)           \
+          colorclass[p] = 0;                                                          \
+        else                                                                          \
+          colorclass[p] = 2;                                                          \
+    while (false)
+    if (format == PLUM_COLOR_16)
+      checkclasses(16);
+    else if (format == PLUM_COLOR_64)
+      checkclasses(64);
+    else
+      checkclasses(32);
+    #undef checkclasses
+    for (size_t frame = 0; frame < context -> source -> frames; frame ++) {
+      const uint8_t * data = context -> source -> data8 + offset * frame;
+      if (colorclass[*data]) goto fail;
+      for (width = 1; width < context -> source -> width; width ++) if (colorclass[data[width]]) break;
+      for (height = 1; height < context -> source -> height; height ++) if (colorclass[data[height * context -> source -> width]]) break;
+      for (size_t row = 0; row < context -> source -> height; row ++) for (size_t col = 0; col < context -> source -> width; col ++)
+        if (colorclass[data[row * context -> source -> width + col]] != (row >= height || col >= width)) goto fail;
+      result[frame * 2] = width;
+      result[frame * 2 + 1] = height;
+    }
+  } else {
+    #define checkframe(bits) do                                                                                                             \
+      for (uint_fast32_t frame = 0; frame < context -> source -> frames; frame ++) {                                                        \
+        const uint ## bits ## _t * data = context -> source -> data ## bits + offset * frame;                                               \
+        if (*data == color) goto fail;                                                                                                      \
+        for (width = 1; width < context -> source -> width; width ++) if (data[width] == color) break;                                      \
+        for (height = 1; height < context -> source -> height; height ++) if (data[height * context -> source -> width] == color) break;    \
+        for (size_t row = 0; row < height; row ++) for (size_t col = 0; col < width; col ++)                                                \
+          if ((data[row * context -> source -> width + col] & mask) != check) goto fail;                                                    \
+        for (size_t row = 0; row < context -> source -> height; row ++)                                                                     \
+          for (size_t col = (row < height) ? width : 0; col < context -> source -> width; col ++)                                           \
+            if (data[row * context -> source -> width + col] != color) goto fail;                                                           \
+        result[frame * 2] = width;                                                                                                          \
+        result[frame * 2 + 1] = height;                                                                                                     \
+      }                                                                                                                                     \
+    while (false)
+    if (format == PLUM_COLOR_16)
+      checkframe(16);
+    else if (format == PLUM_COLOR_64)
+      checkframe(64);
+    else
+      checkframe(32);
+    #undef checkframe
+  }
+  bool fullwidth = false, fullheight = false;
+  for (size_t frame = 0; frame < context -> source -> frames && !(fullwidth && fullheight); frame ++) {
+    if (result[frame * 2] == context -> source -> width) fullwidth = true;
+    if (result[frame * 2 + 1] == context -> source -> height) fullheight = true;
+  }
+  if (fullwidth && fullheight) return result;
+  fail:
+  ctxfree(context, result);
+  return NULL;
+}
+
+void generate_PPM_data (struct context * context, const uint32_t * restrict sizes, unsigned bitdepth, uint64_t * restrict buffer) {
+  size_t offset = (size_t) context -> source -> width * context -> source -> height;
+  if (!context -> source -> palette) offset = plum_color_buffer_size(offset, context -> source -> color_format);
+  for (size_t frame = 0; frame < context -> source -> frames; frame ++) {
+    size_t width = sizes ? sizes[frame * 2] : context -> source -> width;
+    size_t height = sizes ? sizes[frame * 2 + 1] : context -> source -> height;
+    generate_PPM_header(context, width, height, bitdepth);
+    if (context -> source -> palette)
+      generate_PNM_frame_data_from_palette(context, context -> source -> data8 + offset * frame, buffer, width, height, bitdepth, false);
+    else {
+      plum_convert_colors(buffer, context -> source -> data8 + offset * frame, height * context -> source -> width, PLUM_COLOR_64 | PLUM_ALPHA_INVERT,
+                          context -> source -> color_format);
+      generate_PNM_frame_data(context, buffer, width, height, bitdepth, false);
+    }
+  }
+}
+
+void generate_PPM_header (struct context * context, uint32_t width, uint32_t height, unsigned bitdepth) {
+  unsigned char * node = append_output_node(context, 32);
+  size_t offset = byteappend(node, 0x50, 0x36, 0x0a); // P6<newline>
+  offset += write_PNM_number(node + offset, width);
+  node[offset ++] = 0x20; // space
+  offset += write_PNM_number(node + offset, height);
+  node[offset ++] = 0x0a; // newline
+  offset += write_PNM_number(node + offset, ((uint32_t) 1 << bitdepth) - 1);
+  node[offset ++] = 0x0a; // newline
+  context -> output -> size = offset;
+}
+
+void generate_PAM_data (struct context * context, const uint32_t * restrict sizes, unsigned bitdepth, uint64_t * restrict buffer) {
+  size_t size = (size_t) context -> source -> width * context -> source -> height, offset = plum_color_buffer_size(size, context -> source -> color_format);
+  for (uint_fast32_t frame = 0; frame < context -> source -> frames; frame ++) {
+    size_t width = sizes ? sizes[frame * 2] : context -> source -> width;
+    size_t height = sizes ? sizes[frame * 2 + 1] : context -> source -> height;
+    generate_PAM_header(context, width, height, bitdepth);
+    if (context -> source -> palette)
+      generate_PNM_frame_data_from_palette(context, context -> source -> data8 + size * frame, buffer, width, height, bitdepth, true);
+    else {
+      plum_convert_colors(buffer, context -> source -> data8 + offset * frame, size, PLUM_COLOR_64 | PLUM_ALPHA_INVERT, context -> source -> color_format);
+      generate_PNM_frame_data(context, buffer, width, height, bitdepth, true);
+    }
+  }
+}
+
+void generate_PAM_header (struct context * context, uint32_t width, uint32_t height, unsigned bitdepth) {
+  unsigned char * node = append_output_node(context, 96);
+  size_t offset = byteappend(node,
+                             0x50, 0x37, 0x0a, // P7<newline>
+                             0x54, 0x55, 0x50, 0x4c, 0x54, 0x59, 0x50, 0x45, 0x20, // TUPLTYPE<space>
+                             0x52, 0x47, 0x42, 0x5f, 0x41, 0x4c, 0x50, 0x48, 0x41, 0x0a, // RGB_ALPHA<newline>
+                             0x57, 0x49, 0x44, 0x54, 0x48, 0x20 // WIDTH<space>
+                            );
+  offset += write_PNM_number(node + offset, width);
+  offset += byteappend(node + offset, 0x0a, 0x48, 0x45, 0x49, 0x47, 0x48, 0x54, 0x20); // <newline>HEIGHT<space>
+  offset += write_PNM_number(node + offset, height);
+  offset += byteappend(node + offset, 0x0a, 0x4d, 0x41, 0x58, 0x56, 0x41, 0x4c, 0x20); // <newline>MAXVAL<space>
+  offset += write_PNM_number(node + offset, ((uint32_t) 1 << bitdepth) - 1);
+  offset += byteappend(node + offset,
+                       0x0a, // <newline>
+                       0x44, 0x45, 0x50, 0x54, 0x48, 0x20, 0x34, 0x0a, // DEPTH 4<newline>
+                       0x45, 0x4e, 0x44, 0x48, 0x44, 0x52, 0x0a // ENDHDR<newline>
+                      );
+  context -> output -> size = offset;
+}
+
+size_t write_PNM_number (unsigned char * restrict buffer, uint32_t number) {
+  // won't work for 0, but there's no need to write a 0 anywhere
+  unsigned char data[10];
+  uint_fast8_t size = 0;
+  while (number) {
+    data[size ++] = 0x30 + number % 10;
+    number /= 10;
+  }
+  for (uint_fast8_t p = size; p; *(buffer ++) = data[-- p]);
+  return size;
+}
+
+void generate_PNM_frame_data (struct context * context, const uint64_t * data, uint32_t width, uint32_t height, unsigned bitdepth, bool alpha) {
+  uint_fast8_t shift = 16 - bitdepth, mask = (1 << ((bitdepth > 8) ? bitdepth - 8 : bitdepth)) - 1;
+  const uint64_t * rowdata = data;
+  unsigned char * output = append_output_node(context, (size_t) (3 + alpha) * ((bitdepth + 7) / 8) * width * height);
+  if (shift >= 8)
+    for (uint_fast32_t row = 0; row < height; row ++, rowdata += context -> source -> width) for (uint_fast32_t col = 0; col < width; col ++) {
+      output += byteappend(output, (rowdata[col] >> shift) & mask, (rowdata[col] >> (shift + 16)) & mask, (rowdata[col] >> (shift + 32)) & mask);
+      if (alpha) *(output ++) = rowdata[col] >> (shift + 48);
+    }
+  else
+    for (uint_fast32_t row = 0; row < height; row ++, rowdata += context -> source -> width) for (uint_fast32_t col = 0; col < width; col ++) {
+      output += byteappend(output, (rowdata[col] >> (shift + 8)) & mask, rowdata[col] >> shift, (rowdata[col] >> (shift + 24)) & mask,
+                                   rowdata[col] >> (shift + 16), (rowdata[col] >> (shift + 40)) & mask, rowdata[col] >> (shift + 32));
+      if (alpha) output += byteappend(output, rowdata[col] >> (shift + 56), rowdata[col] >> (shift + 48));
+    }
+}
+
+void generate_PNM_frame_data_from_palette (struct context * context, const uint8_t * data, const uint64_t * palette, uint32_t width, uint32_t height,
+                                           unsigned bitdepth, bool alpha) {
+  // very similar to the previous function, but adjusted to use the color from the palette and to read 8-bit data
+  uint_fast8_t shift = 16 - bitdepth, mask = (1 << ((bitdepth > 8) ? bitdepth - 8 : bitdepth)) - 1;
+  const uint8_t * rowdata = data;
+  unsigned char * output = append_output_node(context, (size_t) (3 + alpha) * ((bitdepth + 7) / 8) * width * height);
+  if (shift >= 8)
+    for (uint_fast32_t row = 0; row < height; row ++, rowdata += context -> source -> width) for (uint_fast32_t col = 0; col < width; col ++) {
+      uint64_t color = palette[rowdata[col]];
+      output += byteappend(output, (color >> shift) & mask, (color >> (shift + 16)) & mask, (color >> (shift + 32)) & mask);
+      if (alpha) *(output ++) = color >> (shift + 48);
+    }
+  else
+    for (uint_fast32_t row = 0; row < height; row ++, rowdata += context -> source -> width) for (uint_fast32_t col = 0; col < width; col ++) {
+      uint64_t color = palette[rowdata[col]];
+      output += byteappend(output, (color >> (shift + 8)) & mask, color >> shift, (color >> (shift + 24)) & mask, color >> (shift + 16),
+                                   (color >> (shift + 40)) & mask, color >> (shift + 32));
+      if (alpha) output += byteappend(output, color >> (shift + 56), color >> (shift + 48));
+    }
+}
+
+void sort_values (uint64_t * restrict data, uint64_t count) {
+  #define THRESHOLD 16
+  uint64_t * buffer;
+  if (count < THRESHOLD || !(buffer = malloc(count * sizeof *buffer))) {
+    quicksort_values(data, count);
+    return;
+  }
+  uint_fast64_t start = 0, runsize = 2;
+  bool descending = data[1] < *data;
+  for (uint_fast64_t current = 2; current < count; current ++)
+    if (descending ? data[current] <= data[current - 1] : data[current] >= data[current - 1])
+      runsize ++;
+    else {
+      if (descending && runsize >= THRESHOLD)
+        for (uint_fast64_t p = 0; p < runsize / 2; p ++) swap(uint_fast64_t, data[start + p], data[start + runsize - 1 - p]);
+      buffer[start] = runsize;
+      start = current ++;
+      if (current == count)
+        runsize = 1;
+      else {
+        descending = data[current] < data[current - 1];
+        runsize = 2;
+      }
+    }
+  if (descending && runsize >= THRESHOLD)
+    for (uint_fast64_t p = 0; p < runsize / 2; p ++) swap(uint_fast64_t, data[start + p], data[start + runsize - 1 - p]);
+  buffer[start] = runsize;
+  start = 0;
+  for (uint_fast64_t current = 0; current < count; current += buffer[current])
+    if (buffer[current] >= THRESHOLD) {
+      if (start != current) quicksort_values(data + start, buffer[start] = current - start);
+      start = current + buffer[current];
+    }
+  #undef THRESHOLD
+  if (start != count) quicksort_values(data + start, buffer[start] = count - start);
+  while (*buffer != count) {
+    merge_sorted_values(data, count, buffer);
+    merge_sorted_values(buffer, count, data);
+  }
+  free(buffer);
+}
+
+void quicksort_values (uint64_t * restrict data, uint64_t count) {
+  switch (count) {
+    case 3:
+      if (*data > data[2]) swap(uint_fast64_t, *data, data[2]);
+      if (data[1] > data[2]) swap(uint_fast64_t, data[1], data[2]);
+    case 2:
+      if (*data > data[1]) swap(uint_fast64_t, *data, data[1]);
+    case 0: case 1:
+      return;
+  }
+  uint_fast64_t pivot = data[count / 2], left = UINT_FAST64_MAX, right = count;
+  while (true) {
+    while (data[++ left] < pivot);
+    while (data[-- right] > pivot);
+    if (left >= right) break;
+    swap(uint_fast64_t, data[left], data[right]);
+  }
+  right ++;
+  if (right > 1) quicksort_values(data, right);
+  if (count - right > 1) quicksort_values(data + right, count - right);
+}
+
+void merge_sorted_values (uint64_t * restrict data, uint64_t count, uint64_t * restrict runs) {
+  // in: data = data to sort, runs = run lengths; out: flipped
+  uint_fast64_t length;
+  for (uint_fast64_t current = 0; current < count; current += length) {
+    length = runs[current];
+    if (current + length == count)
+      memcpy(runs + current, data + current, length * sizeof *data);
+    else {
+      // index1, index2 point to the END of the respective runs
+      uint_fast64_t remaining1 = length, index1 = current + remaining1, remaining2 = runs[index1], index2 = index1 + remaining2;
+      length = remaining1 + remaining2;
+      for (uint64_t p = 0; p < length; p ++)
+        if (!remaining2 || (remaining1 && data[index1 - remaining1] <= data[index2 - remaining2]))
+          runs[current + p] = data[index1 - (remaining1 --)];
+        else
+          runs[current + p] = data[index2 - (remaining2 --)];
+    }
+    data[current] = length;
+  }
+}
+
+#define comparepairs(first, op, second) (((first).index == (second).index) ? ((first).value op (second).value) : ((first).index op (second).index))
+
+void sort_pairs (struct pair * restrict data, uint64_t count) {
+  // this function and its helpers implement essentially the same algorithm as above, but adapter for index/value pairs instead of just values
+  #define THRESHOLD 16
+  struct pair * buffer;
+  if (count < THRESHOLD || !(buffer = malloc(count * sizeof *buffer))) {
+    quicksort_pairs(data, count);
+    return;
+  }
+  uint_fast64_t start = 0, runsize = 2;
+  bool descending = comparepairs(data[1], <, *data);
+  for (uint_fast64_t current = 2; current < count; current ++)
+    if (descending ? comparepairs(data[current], <=, data[current - 1]) : comparepairs(data[current], >=, data[current - 1]))
+      runsize ++;
+    else {
+      if (descending && runsize >= THRESHOLD)
+        for (uint_fast64_t p = 0; p < runsize / 2; p ++) swap(struct pair, data[start + p], data[start + runsize - 1 - p]);
+      buffer[start].index = runsize;
+      start = current ++;
+      if (current == count)
+        runsize = 1;
+      else {
+        descending = comparepairs(data[current], <, data[current - 1]);
+        runsize = 2;
+      }
+    }
+  if (descending && runsize >= THRESHOLD)
+    for (uint_fast64_t p = 0; p < runsize / 2; p ++) swap(struct pair, data[start + p], data[start + runsize - 1 - p]);
+  buffer[start].index = runsize;
+  start = 0;
+  for (uint_fast64_t current = 0; current < count; current += buffer[current].index)
+    if (buffer[current].index >= THRESHOLD) {
+      if (start != current) quicksort_pairs(data + start, buffer[start].index = current - start);
+      start = current + buffer[current].index;
+    }
+  #undef THRESHOLD
+  if (start != count) quicksort_pairs(data + start, buffer[start].index = count - start);
+  while (buffer -> index != count) {
+    merge_sorted_pairs(data, count, buffer);
+    merge_sorted_pairs(buffer, count, data);
+  }
+  free(buffer);
+}
+
+void quicksort_pairs (struct pair * restrict data, uint64_t count) {
+  switch (count) {
+    case 3:
+      if (comparepairs(*data, >, data[2])) swap(struct pair, *data, data[2]);
+      if (comparepairs(data[1], >, data[2])) swap(struct pair, data[1], data[2]);
+    case 2:
+      if (comparepairs(*data, >, data[1])) swap(struct pair, *data, data[1]);
+    case 0: case 1:
+      return;
+  }
+  struct pair pivot = data[count / 2];
+  uint_fast64_t left = UINT_FAST64_MAX, right = count;
+  while (true) {
+    do left ++; while (comparepairs(data[left], <, pivot));
+    do right --; while (comparepairs(data[right], >, pivot));
+    if (left >= right) break;
+    swap(struct pair, data[left], data[right]);
+  }
+  right ++;
+  if (right > 1) quicksort_pairs(data, right);
+  if (count - right > 1) quicksort_pairs(data + right, count - right);
+}
+
+void merge_sorted_pairs (struct pair * restrict data, uint64_t count, struct pair * restrict runs) {
+  // in: data = data to sort, runs = run lengths; out: flipped
+  uint_fast64_t length;
+  for (uint_fast64_t current = 0; current < count; current += length) {
+    length = runs[current].index;
+    if (current + length == count)
+      memcpy(runs + current, data + current, length * sizeof *data);
+    else {
+      // index1, index2 point to the END of the respective runs
+      uint_fast64_t remaining1 = length, index1 = current + remaining1, remaining2 = runs[index1].index, index2 = index1 + remaining2;
+      length = remaining1 + remaining2;
+      for (uint64_t p = 0; p < length; p ++)
+        if (!remaining2 || (remaining1 && comparepairs(data[index1 - remaining1], <=, data[index2 - remaining2])))
+          runs[current + p] = data[index1 - (remaining1 --)];
+        else
+          runs[current + p] = data[index2 - (remaining2 --)];
+    }
+    data[current].index = length;
+  }
+}
+
+#undef comparepairs
+
+size_t plum_store_image (const struct plum_image * image, void * restrict buffer, size_t size_mode, unsigned * restrict error) {
+  struct context * context = create_context();
+  if (!context) {
+    if (error) *error = PLUM_ERR_OUT_OF_MEMORY;
+    return 0;
+  }
+  context -> source = image;
+  if (!setjmp(context -> target)) {
+    if (!(image && buffer && size_mode)) throw(context, PLUM_ERR_INVALID_ARGUMENTS);
+    unsigned rv = plum_validate_image(image);
+    if (rv) throw(context, rv);
+    if (plum_validate_palette_indexes(image)) throw(context, PLUM_ERR_INVALID_COLOR_INDEX);
+    switch (image -> type) {
+      case PLUM_IMAGE_BMP: generate_BMP_data(context); break;
+      case PLUM_IMAGE_GIF: generate_GIF_data(context); break;
+      case PLUM_IMAGE_PNG: generate_PNG_data(context); break;
+      case PLUM_IMAGE_APNG: generate_APNG_data(context); break;
+      case PLUM_IMAGE_JPEG: generate_JPEG_data(context); break;
+      case PLUM_IMAGE_PNM: generate_PNM_data(context); break;
+      default: throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    }
+    size_t output_size = get_total_output_size(context);
+    if (!output_size) throw(context, PLUM_ERR_INVALID_FILE_FORMAT);
+    switch (size_mode) {
+      case PLUM_MODE_FILENAME:
+        write_generated_image_data_to_file(context, buffer);
+        break;
+      case PLUM_MODE_BUFFER: {
+        void * out = malloc(output_size);
+        if (!out) throw(context, PLUM_ERR_OUT_OF_MEMORY);
+        // the function must succeed after reaching this point (otherwise, memory would be leaked)
+        *(struct plum_buffer *) buffer = (struct plum_buffer) {.size = output_size, .data = out};
+        write_generated_image_data(out, context -> output);
+      } break;
+      case PLUM_MODE_CALLBACK:
+        write_generated_image_data_to_callback(context, buffer);
+        break;
+      default:
+        if (output_size > size_mode) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+        write_generated_image_data(buffer, context -> output);
+    }
+    context -> size = output_size;
+  }
+  if (context -> file) fclose(context -> file);
+  if (error) *error = context -> status;
+  size_t result = context -> size;
+  destroy_allocator_list(context -> allocator);
+  return result;
+}
+
+void write_generated_image_data_to_file (struct context * context, const char * filename) {
+  context -> file = fopen(filename, "wb");
+  if (!context -> file) throw(context, PLUM_ERR_FILE_INACCESSIBLE);
+  const struct data_node * node;
+  for (node = context -> output; node -> previous; node = node -> previous);
+  while (node) {
+    const unsigned char * data = node -> data;
+    size_t size = node -> size;
+    while (size) {
+      unsigned count = fwrite(data, 1, (size > 0x4000) ? 0x4000 : size, context -> file);
+      if (ferror(context -> file) || !count) throw(context, PLUM_ERR_FILE_ERROR);
+      data += count;
+      size -= count;
+    }
+    node = node -> next;
+  }
+  fclose(context -> file);
+  context -> file = NULL;
+}
+
+void write_generated_image_data_to_callback (struct context * context, const struct plum_callback * callback) {
+  struct data_node * node;
+  for (node = context -> output; node -> previous; node = node -> previous);
+  while (node) {
+    unsigned char * data = node -> data; // not const because the callback takes an unsigned char *
+    size_t size = node -> size;
+    while (size) {
+      int block = (size > 0x4000) ? 0x4000 : size;
+      int count = callback -> callback(callback -> userdata, data, block);
+      if (count < 0 || count > block) throw(context, PLUM_ERR_FILE_ERROR);
+      data += count;
+      size -= count;
+    }
+    node = node -> next;
+  }
+}
+
+void write_generated_image_data (void * restrict buffer, const struct data_node * data) {
+  const struct data_node * node;
+  for (node = data; node -> previous; node = node -> previous);
+  for (unsigned char * out = buffer; node; node = node -> next) {
+    memcpy(out, node -> data, node -> size);
+    out += node -> size;
+  }
+}
+
+size_t get_total_output_size (struct context * context) {
+  size_t result = 0;
+  for (const struct data_node * node = context -> output; node; node = node -> previous) {
+    if (result + node -> size < result) throw(context, PLUM_ERR_IMAGE_TOO_LARGE);
+    result += node -> size;
+  }
+  return result;
+}
diff --git a/src/libplum.h b/src/libplum.h
new file mode 100644
index 0000000..99c676b
--- /dev/null
+++ b/src/libplum.h
@@ -0,0 +1,394 @@
+#ifndef PLUM_HEADER
+
+#define PLUM_HEADER
+
+#define PLUM_VERSION 10029
+
+#include <stddef.h>
+#ifndef PLUM_NO_STDINT
+#include <stdint.h>
+#endif
+
+#if !defined(__cplusplus) && (__STDC_VERSION__ >= 199901L)
+/* C99 or later, not C++, we can use restrict, and check for VLAs and anonymous struct members (C11) */
+/* indented preprocessor directives and // comments are also allowed here, but we'll avoid them for consistency */
+#define PLUM_RESTRICT restrict
+#define PLUM_ANON_MEMBERS (__STDC_VERSION__ >= 201112L)
+/* protect against really broken preprocessor implementations */
+#if !defined(__STDC_NO_VLA__) || !(__STDC_NO_VLA__ + 0)
+#define PLUM_VLA_SUPPORT 1
+#else
+#define PLUM_VLA_SUPPORT 0
+#endif
+#elif defined(__cplusplus)
+/* C++ allows anonymous unions as struct members, but not restrict or VLAs */
+#define PLUM_RESTRICT
+#define PLUM_ANON_MEMBERS 1
+#define PLUM_VLA_SUPPORT 0
+#else
+/* C89 (or, if we're really unlucky, non-standard C), so don't use any "advanced" C features */
+#define PLUM_RESTRICT
+#define PLUM_ANON_MEMBERS 0
+#define PLUM_VLA_SUPPORT 0
+#endif
+
+#ifdef PLUM_NO_ANON_MEMBERS
+#undef PLUM_ANON_MEMBERS
+#define PLUM_ANON_MEMBERS 0
+#endif
+
+#ifdef PLUM_NO_VLA
+#undef PLUM_VLA_SUPPORT
+#define PLUM_VLA_SUPPORT 0
+#endif
+
+#define PLUM_MODE_FILENAME   ((size_t) -1)
+#define PLUM_MODE_BUFFER     ((size_t) -2)
+#define PLUM_MODE_CALLBACK   ((size_t) -3)
+#define PLUM_MAX_MEMORY_SIZE ((size_t) -4)
+
+/* legacy constants, for compatibility with the v0.4 API */
+#define PLUM_FILENAME PLUM_MODE_FILENAME
+#define PLUM_BUFFER   PLUM_MODE_BUFFER
+#define PLUM_CALLBACK PLUM_MODE_CALLBACK
+
+enum plum_flags {
+  /* color formats */
+  PLUM_COLOR_32     = 0, /* RGBA 8.8.8.8 */
+  PLUM_COLOR_64     = 1, /* RGBA 16.16.16.16 */
+  PLUM_COLOR_16     = 2, /* RGBA 5.5.5.1 */
+  PLUM_COLOR_32X    = 3, /* RGBA 10.10.10.2 */
+  PLUM_COLOR_MASK   = 3,
+  PLUM_ALPHA_INVERT = 4,
+  /* palettes */
+  PLUM_PALETTE_NONE     =     0,
+  PLUM_PALETTE_LOAD     = 0x200,
+  PLUM_PALETTE_GENERATE = 0x400,
+  PLUM_PALETTE_FORCE    = 0x600,
+  PLUM_PALETTE_MASK     = 0x600,
+  /* palette sorting */
+  PLUM_SORT_LIGHT_FIRST =     0,
+  PLUM_SORT_DARK_FIRST  = 0x800,
+  /* other bit flags */
+  PLUM_ALPHA_REMOVE   =  0x100,
+  PLUM_SORT_EXISTING  = 0x1000,
+  PLUM_PALETTE_REDUCE = 0x2000
+};
+
+enum plum_image_types {
+  PLUM_IMAGE_NONE,
+  PLUM_IMAGE_BMP,
+  PLUM_IMAGE_GIF,
+  PLUM_IMAGE_PNG,
+  PLUM_IMAGE_APNG,
+  PLUM_IMAGE_JPEG,
+  PLUM_IMAGE_PNM,
+  PLUM_NUM_IMAGE_TYPES
+};
+
+enum plum_metadata_types {
+  PLUM_METADATA_NONE,
+  PLUM_METADATA_COLOR_DEPTH,
+  PLUM_METADATA_BACKGROUND,
+  PLUM_METADATA_LOOP_COUNT,
+  PLUM_METADATA_FRAME_DURATION,
+  PLUM_METADATA_FRAME_DISPOSAL,
+  PLUM_METADATA_FRAME_AREA,
+  PLUM_NUM_METADATA_TYPES
+};
+
+enum plum_frame_disposal_methods {
+  PLUM_DISPOSAL_NONE,
+  PLUM_DISPOSAL_BACKGROUND,
+  PLUM_DISPOSAL_PREVIOUS,
+  PLUM_DISPOSAL_REPLACE,
+  PLUM_DISPOSAL_BACKGROUND_REPLACE,
+  PLUM_DISPOSAL_PREVIOUS_REPLACE,
+  PLUM_NUM_DISPOSAL_METHODS
+};
+
+enum plum_errors {
+  PLUM_OK,
+  PLUM_ERR_INVALID_ARGUMENTS,
+  PLUM_ERR_INVALID_FILE_FORMAT,
+  PLUM_ERR_INVALID_METADATA,
+  PLUM_ERR_INVALID_COLOR_INDEX,
+  PLUM_ERR_TOO_MANY_COLORS,
+  PLUM_ERR_UNDEFINED_PALETTE,
+  PLUM_ERR_IMAGE_TOO_LARGE,
+  PLUM_ERR_NO_DATA,
+  PLUM_ERR_NO_MULTI_FRAME,
+  PLUM_ERR_FILE_INACCESSIBLE,
+  PLUM_ERR_FILE_ERROR,
+  PLUM_ERR_OUT_OF_MEMORY,
+  PLUM_NUM_ERRORS
+};
+
+#define PLUM_COLOR_VALUE_32(red, green, blue, alpha) ((uint32_t) (((uint32_t) (red) & 0xff) | (((uint32_t) (green) & 0xff) << 8) | \
+                                                                  (((uint32_t) (blue) & 0xff) << 16) | (((uint32_t) (alpha) & 0xff) << 24)))
+#define PLUM_COLOR_VALUE_64(red, green, blue, alpha) ((uint64_t) (((uint64_t) (red) & 0xffffu) | (((uint64_t) (green) & 0xffffu) << 16) | \
+                                                                  (((uint64_t) (blue) & 0xffffu) << 32) | (((uint64_t) (alpha) & 0xffffu) << 48)))
+#define PLUM_COLOR_VALUE_16(red, green, blue, alpha) ((uint16_t) (((uint16_t) (red) & 0x1f) | (((uint16_t) (green) & 0x1f) << 5) | \
+                                                                  (((uint16_t) (blue) & 0x1f) << 10) | (((uint16_t) (alpha) & 1) << 15)))
+#define PLUM_COLOR_VALUE_32X(red, green, blue, alpha) ((uint32_t) (((uint32_t) (red) & 0x3ff) | (((uint32_t) (green) & 0x3ff) << 10) | \
+                                                                   (((uint32_t) (blue) & 0x3ff) << 20) | (((uint32_t) (alpha) & 3) << 30)))
+
+#define PLUM_RED_32(color) ((uint32_t) ((uint32_t) (color) & 0xff))
+#define PLUM_RED_64(color) ((uint64_t) ((uint64_t) (color) & 0xffffu))
+#define PLUM_RED_16(color) ((uint16_t) ((uint16_t) (color) & 0x1f))
+#define PLUM_RED_32X(color) ((uint32_t) ((uint32_t) (color) & 0x3ff))
+#define PLUM_GREEN_32(color) ((uint32_t) (((uint32_t) (color) >> 8) & 0xff))
+#define PLUM_GREEN_64(color) ((uint64_t) (((uint64_t) (color) >> 16) & 0xffffu))
+#define PLUM_GREEN_16(color) ((uint16_t) (((uint16_t) (color) >> 5) & 0x1f))
+#define PLUM_GREEN_32X(color) ((uint32_t) (((uint32_t) (color) >> 10) & 0x3ff))
+#define PLUM_BLUE_32(color) ((uint32_t) (((uint32_t) (color) >> 16) & 0xff))
+#define PLUM_BLUE_64(color) ((uint64_t) (((uint64_t) (color) >> 32) & 0xffffu))
+#define PLUM_BLUE_16(color) ((uint16_t) (((uint16_t) (color) >> 10) & 0x1f))
+#define PLUM_BLUE_32X(color) ((uint32_t) (((uint32_t) (color) >> 20) & 0x3ff))
+#define PLUM_ALPHA_32(color) ((uint32_t) (((uint32_t) (color) >> 24) & 0xff))
+#define PLUM_ALPHA_64(color) ((uint64_t) (((uint64_t) (color) >> 48) & 0xffffu))
+#define PLUM_ALPHA_16(color) ((uint16_t) (((uint16_t) (color) >> 15) & 1))
+#define PLUM_ALPHA_32X(color) ((uint32_t) (((uint32_t) (color) >> 30) & 3))
+
+#define PLUM_RED_MASK_32 ((uint32_t) 0xff)
+#define PLUM_RED_MASK_64 ((uint64_t) 0xffffu)
+#define PLUM_RED_MASK_16 ((uint16_t) 0x1f)
+#define PLUM_RED_MASK_32X ((uint32_t) 0x3ff)
+#define PLUM_GREEN_MASK_32 ((uint32_t) 0xff00u)
+#define PLUM_GREEN_MASK_64 ((uint64_t) 0xffff0000u)
+#define PLUM_GREEN_MASK_16 ((uint16_t) 0x3e0)
+#define PLUM_GREEN_MASK_32X ((uint32_t) 0xffc00u)
+#define PLUM_BLUE_MASK_32 ((uint32_t) 0xff0000u)
+#define PLUM_BLUE_MASK_64 ((uint64_t) 0xffff00000000u)
+#define PLUM_BLUE_MASK_16 ((uint16_t) 0x7c00)
+#define PLUM_BLUE_MASK_32X ((uint32_t) 0x3ff00000u)
+#define PLUM_ALPHA_MASK_32 ((uint32_t) 0xff000000u)
+#define PLUM_ALPHA_MASK_64 ((uint64_t) 0xffff000000000000u)
+#define PLUM_ALPHA_MASK_16 ((uint16_t) 0x8000u)
+#define PLUM_ALPHA_MASK_32X ((uint32_t) 0xc0000000u)
+
+#define PLUM_PIXEL_INDEX(image, col, row, frame) (((size_t) (frame) * (size_t) (image) -> height + (size_t) (row)) * (size_t) (image) -> width + (size_t) (col))
+
+#define PLUM_PIXEL_8(image, col, row, frame) (((uint8_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+#define PLUM_PIXEL_16(image, col, row, frame) (((uint16_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+#define PLUM_PIXEL_32(image, col, row, frame) (((uint32_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+#define PLUM_PIXEL_64(image, col, row, frame) (((uint64_t *) (image) -> data)[PLUM_PIXEL_INDEX(image, col, row, frame)])
+
+#if PLUM_VLA_SUPPORT
+#define PLUM_PIXEL_ARRAY_TYPE(image) ((*)[(image) -> height][(image) -> width])
+#define PLUM_PIXEL_ARRAY(declarator, image) ((* (declarator))[(image) -> height][(image) -> width])
+
+#define PLUM_PIXELS_8(image) ((uint8_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#define PLUM_PIXELS_16(image) ((uint16_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#define PLUM_PIXELS_32(image) ((uint32_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#define PLUM_PIXELS_64(image) ((uint64_t PLUM_PIXEL_ARRAY_TYPE(image)) (image) -> data)
+#endif
+
+struct plum_buffer {
+  size_t size;
+  void * data;
+};
+
+#ifdef __cplusplus
+extern "C" /* function pointer member requires an explicit extern "C" declaration to be passed safely from C++ to C */
+#endif
+struct plum_callback {
+  int (* callback) (void * userdata, void * buffer, int size);
+  void * userdata;
+};
+
+struct plum_metadata {
+  int type;
+  size_t size;
+  void * data;
+  struct plum_metadata * next;
+};
+
+struct plum_image {
+  uint16_t type;
+  uint8_t max_palette_index;
+  uint8_t color_format;
+  uint32_t frames;
+  uint32_t height;
+  uint32_t width;
+  void * allocator;
+  struct plum_metadata * metadata;
+#if PLUM_ANON_MEMBERS
+  union {
+#endif
+    void * palette;
+#if PLUM_ANON_MEMBERS
+    uint16_t * palette16;
+    uint32_t * palette32;
+    uint64_t * palette64;
+  };
+  union {
+#endif
+    void * data;
+#if PLUM_ANON_MEMBERS
+    uint8_t * data8;
+    uint16_t * data16;
+    uint32_t * data32;
+    uint64_t * data64;
+  };
+#endif
+  void * userdata;
+#ifdef __cplusplus
+inline uint8_t & pixel8 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint8_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint8_t & pixel8 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint8_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint16_t & pixel16 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint16_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint16_t & pixel16 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint16_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint32_t & pixel32 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint32_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint32_t & pixel32 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint32_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint64_t & pixel64 (uint32_t col, uint32_t row, uint32_t frame = 0) {
+  return ((uint64_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline const uint64_t & pixel64 (uint32_t col, uint32_t row, uint32_t frame = 0) const {
+  return ((const uint64_t *) this -> data)[PLUM_PIXEL_INDEX(this, col, row, frame)];
+}
+
+inline uint16_t & color16 (uint8_t index) {
+  return ((uint16_t *) this -> palette)[index];
+}
+
+inline const uint16_t & color16 (uint8_t index) const {
+  return ((const uint16_t *) this -> palette)[index];
+}
+
+inline uint32_t & color32 (uint8_t index) {
+  return ((uint32_t *) this -> palette)[index];
+}
+
+inline const uint32_t & color32 (uint8_t index) const {
+  return ((const uint32_t *) this -> palette)[index];
+}
+
+inline uint64_t & color64 (uint8_t index) {
+  return ((uint64_t *) this -> palette)[index];
+}
+
+inline const uint64_t & color64 (uint8_t index) const {
+  return ((const uint64_t *) this -> palette)[index];
+}
+#endif
+};
+
+struct plum_rectangle {
+  uint32_t left;
+  uint32_t top;
+  uint32_t width;
+  uint32_t height;
+};
+
+/* keep declarations readable: redefine the "restrict" keyword, and undefine it later
+   (note that, if this expands to "#define restrict restrict", that will NOT expand recursively) */
+#define restrict PLUM_RESTRICT
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct plum_image * plum_new_image(void);
+struct plum_image * plum_copy_image(const struct plum_image * image);
+void plum_destroy_image(struct plum_image * image);
+struct plum_image * plum_load_image(const void * restrict buffer, size_t size_mode, unsigned flags, unsigned * restrict error);
+struct plum_image * plum_load_image_limited(const void * restrict buffer, size_t size_mode, unsigned flags, size_t limit, unsigned * restrict error);
+size_t plum_store_image(const struct plum_image * image, void * restrict buffer, size_t size_mode, unsigned * restrict error);
+unsigned plum_validate_image(const struct plum_image * image);
+const char * plum_get_error_text(unsigned error);
+const char * plum_get_file_format_name(unsigned format);
+uint32_t plum_get_version_number(void);
+int plum_check_valid_image_size(uint32_t width, uint32_t height, uint32_t frames);
+int plum_check_limited_image_size(uint32_t width, uint32_t height, uint32_t frames, size_t limit);
+size_t plum_color_buffer_size(size_t size, unsigned flags);
+size_t plum_pixel_buffer_size(const struct plum_image * image);
+size_t plum_palette_buffer_size(const struct plum_image * image);
+unsigned plum_rotate_image(struct plum_image * image, unsigned count, int flip);
+void plum_convert_colors(void * restrict destination, const void * restrict source, size_t count, unsigned to, unsigned from);
+uint64_t plum_convert_color(uint64_t color, unsigned from, unsigned to);
+void plum_remove_alpha(struct plum_image * image);
+unsigned plum_sort_palette(struct plum_image * image, unsigned flags);
+unsigned plum_sort_palette_custom(struct plum_image * image, uint64_t (* callback) (void *, uint64_t), void * argument, unsigned flags);
+unsigned plum_reduce_palette(struct plum_image * image);
+const uint8_t * plum_validate_palette_indexes(const struct plum_image * image);
+int plum_get_highest_palette_index(const struct plum_image * image);
+int plum_convert_colors_to_indexes(uint8_t * restrict destination, const void * restrict source, void * restrict palette, size_t count, unsigned flags);
+void plum_convert_indexes_to_colors(void * restrict destination, const uint8_t * restrict source, const void * restrict palette, size_t count, unsigned flags);
+void plum_sort_colors(const void * restrict colors, uint8_t max_index, unsigned flags, uint8_t * restrict result);
+void * plum_malloc(struct plum_image * image, size_t size);
+void * plum_calloc(struct plum_image * image, size_t size);
+void * plum_realloc(struct plum_image * image, void * buffer, size_t size);
+void plum_free(struct plum_image * image, void * buffer);
+struct plum_metadata * plum_allocate_metadata(struct plum_image * image, size_t size);
+unsigned plum_append_metadata(struct plum_image * image, int type, const void * data, size_t size);
+struct plum_metadata * plum_find_metadata(const struct plum_image * image, int type);
+
+#ifdef __cplusplus
+}
+#endif
+
+#undef restrict
+
+/* if PLUM_UNPREFIXED_MACROS is defined, include shorter, unprefixed alternatives for some common macros */
+/* this requires an explicit opt-in because it violates the principle of a library prefix as a namespace */
+#ifdef PLUM_UNPREFIXED_MACROS
+#define PIXEL(image, col, row, frame) PLUM_PIXEL_INDEX(image, col, row, frame)
+
+#define PIXEL8(image, col, row, frame) PLUM_PIXEL_8(image, col, row, frame)
+#define PIXEL16(image, col, row, frame) PLUM_PIXEL_16(image, col, row, frame)
+#define PIXEL32(image, col, row, frame) PLUM_PIXEL_32(image, col, row, frame)
+#define PIXEL64(image, col, row, frame) PLUM_PIXEL_64(image, col, row, frame)
+
+#if PLUM_VLA_SUPPORT
+#define PIXARRAY_T(image) PLUM_PIXEL_ARRAY_TYPE(image)
+#define PIXARRAY(declarator, image) PLUM_PIXEL_ARRAY(declarator, image)
+
+#define PIXELS8(image) PLUM_PIXELS_8(image)
+#define PIXELS16(image) PLUM_PIXELS_16(image)
+#define PIXELS32(image) PLUM_PIXELS_32(image)
+#define PIXELS64(image) PLUM_PIXELS_64(image)
+#endif
+
+#define COLOR32(red, green, blue, alpha) PLUM_COLOR_VALUE_32(red, green, blue, alpha)
+#define COLOR64(red, green, blue, alpha) PLUM_COLOR_VALUE_64(red, green, blue, alpha)
+#define COLOR16(red, green, blue, alpha) PLUM_COLOR_VALUE_16(red, green, blue, alpha)
+#define COLOR32X(red, green, blue, alpha) PLUM_COLOR_VALUE_32X(red, green, blue, alpha)
+
+#define RED32(color) PLUM_RED_32(color)
+#define RED64(color) PLUM_RED_64(color)
+#define RED16(color) PLUM_RED_16(color)
+#define RED32X(color) PLUM_RED_32X(color)
+#define GREEN32(color) PLUM_GREEN_32(color)
+#define GREEN64(color) PLUM_GREEN_64(color)
+#define GREEN16(color) PLUM_GREEN_16(color)
+#define GREEN32X(color) PLUM_GREEN_32X(color)
+#define BLUE32(color) PLUM_BLUE_32(color)
+#define BLUE64(color) PLUM_BLUE_64(color)
+#define BLUE16(color) PLUM_BLUE_16(color)
+#define BLUE32X(color) PLUM_BLUE_32X(color)
+#define ALPHA32(color) PLUM_ALPHA_32(color)
+#define ALPHA64(color) PLUM_ALPHA_64(color)
+#define ALPHA16(color) PLUM_ALPHA_16(color)
+#define ALPHA32X(color) PLUM_ALPHA_32X(color)
+#endif
+
+#endif
diff --git a/src/loader.c b/src/loader.c
new file mode 100644
index 0000000..aa599bd
--- /dev/null
+++ b/src/loader.c
@@ -0,0 +1,368 @@
+#include <stdlib.h> // malloc
+#include <stdio.h> // fprintf, stderr
+#include <string.h> // memcmp
+#include <zlib.h>
+#include <stdbool.h>
+
+#include <SDL2/SDL.h>
+
+#include "libplum.h"
+#include "zip.h"
+#include "loader.h"
+#include "util.h"
+#include "main.h"
+
+static void *inflateWrapped(void *const restrict data, uint32_t const outsize);
+
+typedef enum {
+	FILE_EXT_UNKNOWN,
+	FILE_EXT_TEXTURE,
+	FILE_EXT_MAP,
+} ext_T;
+
+ext_T get_extension_type(char *extension, int length);
+
+struct chaos { // why "chaos"? idk, naming variables is hard
+	name_T name;
+	ext_T extension;
+	void *data;
+	size_t size;
+};
+
+struct blob_collection {
+	size_t count;
+	struct blob_item {
+		struct blob blob;
+		name_T name;
+	} items[];
+} *textures = NULL, *maps = NULL;
+
+static int name_compare(name_T a, name_T b) {
+	return strcmp(a, b);
+	// return (a > b) - (a < b);
+}
+
+// like binary search, but its made for finding where to insert a new member
+void *bsearchinsertposition(const void *key, void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *), int *found) {
+	int L = 0, R = nmemb - 1, result, member;
+	if (nmemb == 0) {
+		if (found != NULL) {
+			*found = 0;
+		}
+		return base;
+	}
+	while (L != R) {
+		member = (L + R + 1) / 2;
+		result = compar(((char *) base) + member * size, key);
+		if (result > 0) {
+			R = member - 1;
+		} else {
+			L = member;
+		}
+	}
+	result = compar(((char *) base) + L * size, key);
+	if (found != NULL) {
+		*found = !result;
+	}
+	if (result < 0) {
+		return ((char *) base) + (L + 1) * size;
+	} else {
+		return ((char *) base) + L * size;
+	}
+}
+
+static struct blob_collection *res_init_blobs(void) {
+	struct blob_collection *collection = malloc(sizeof (struct blob_collection));
+	collection->count = 0;
+	return collection;
+}
+
+void res_init_texture(void) {
+	textures = res_init_blobs();
+}
+
+void res_init_map(void) {
+	maps = res_init_blobs();
+}
+
+void res_free_texture(void) {
+	for (size_t i = 0; i < textures->count; i++) {
+		SDL_DestroyTexture(textures->items[i].blob.data);
+		free(textures->items[i].name);
+	}
+	free(textures);
+	textures = NULL;
+}
+
+static void res_free_blobs(struct blob_collection *collection) {
+	for (size_t i = 0; i < collection->count; i++) {
+		free(collection->items[i].blob.data);
+		free(collection->items[i].name);
+	}
+	free(collection);
+}
+
+void res_free_map(void) {
+	res_free_blobs(maps);
+	maps = NULL;
+}
+
+static int res_compare_blobs(void const *a, void const *b) {
+	struct blob_item const *aa = a, *bb = b;
+	return name_compare(aa->name, bb->name);
+}
+
+static struct blob res_get_blob(name_T const name, struct blob_collection *collection) {
+	struct blob_item new = {.name = name};
+	struct blob_item *dst = bsearch(&new, collection->items, collection->count, sizeof (struct blob_item), res_compare_blobs);
+	#if 0
+	fprintf(stderr, "== chunk %zu ==\n", dst - collection->items);
+	for (int i = 0; i < collection->count; i++) {
+		fprintf(stderr, "= %s, %p\n", collection->items[i].name, collection->items[i].blob.data);
+	}
+	if (dst != NULL)
+		fprintf(stderr, "== value %p ==\n", dst->blob.data);
+	else
+		fprintf(stderr, "== not found ==\n");
+	#endif
+	if (dst == NULL) {
+		return (struct blob) {.data = NULL, .size = 0};
+	}
+	return dst->blob;
+}
+
+struct blob res_get_texture(name_T const name) {
+	return res_get_blob(name, textures);
+}
+
+struct blob res_get_map(name_T const name) {
+	return res_get_blob(name, maps);
+}
+
+static struct blob_collection *res_push_blob(struct blob *blob, name_T name, struct blob_collection *collection, void (*freeFunc)(void *)) {
+	int found;
+	struct blob_item new = {.blob = *blob, .name = name};
+	struct blob_item *dst = bsearchinsertposition(&new, collection->items, collection->count, sizeof (struct blob_item), res_compare_blobs, &found);
+	if (found) {
+		fprintf(stderr, "warn: name collision: %s\n", new.name);
+		freeFunc(dst);
+		memcpy(dst, &new, sizeof (struct blob_item));
+	} else {
+		size_t index = dst - collection->items; // hack
+		collection->count++;
+		collection = realloc(collection, sizeof (struct blob_collection) + sizeof (struct blob_item) * collection->count);
+		dst = collection->items + index; // hack for the struct having been reallocated
+		#if 0
+		fprintf(stderr, "info: no collision: %s\n", new.name);
+		#endif
+		memmove(dst + 1, dst, sizeof (struct blob_item) * (collection->count - 1 - (dst - collection->items)));
+		memcpy(dst, &new, sizeof (struct blob_item));
+	}
+	return collection;
+}
+
+static void texture_free_func(void *ptr) {
+	struct blob_item *a = ptr;
+	SDL_DestroyTexture(a->blob.data);
+	free(a->name);
+}
+
+void res_push_texture(struct blob *blob, name_T name) {
+	textures = res_push_blob(blob, name, textures, texture_free_func);
+}
+
+static void blob_free_func(void *ptr) {
+	struct blob_item *a = ptr;
+	free(a->blob.data);
+	free(a->name);
+}
+
+void res_push_map(struct blob *blob, name_T name) {
+	maps = res_push_blob(blob, name, maps, blob_free_func);
+}
+
+int loadResources(char *filename) {
+	// open the file
+	FILE *zip = fopen(filename, "rb");
+	if (zip == NULL) {
+		perror("fopen");
+		return EXIT_FAILURE;
+	}
+
+	// load it into memory
+	size_t size;
+	char *file = util_loadFile(zip, &size);
+	if (file == NULL) {
+		perror("reading file"); // "opening file: Succeeded"
+		return EXIT_FAILURE; // "process returned with status 1" id love to see that happen
+	}
+	fclose(zip);
+
+	// index the archive
+	int error;
+	struct zip_file *headers = zip_index(file, size, &error);
+	if (headers == NULL) {
+		fprintf(stderr, "%s\n", zip_error(error));
+		return EXIT_FAILURE;
+	}
+
+	size_t const file_count = headers->file_count;
+	struct chaos *files = malloc(sizeof (struct chaos) * file_count);
+	
+	for (size_t i = 0; i < file_count; i++) {
+		struct zip_index const *zip_file = headers->files + i;
+		struct chaos *chaos_file = files + i;
+		
+		size_t j = zip_file->filename_length;
+		for (; j > 0 && zip_file->filename[j] != '.'; j--)
+			;
+		if (j == 0) {
+			continue;
+		}
+		
+		chaos_file->name = malloc(j + 1);
+		memcpy(chaos_file->name, zip_file->filename, j);
+		chaos_file->name[j] = 0;
+		
+		chaos_file->extension = get_extension_type(zip_file->filename + j + 1, zip_file->filename_length - j - 1);
+		
+		#if 0
+		fprintf(stderr, "%s: %i\n", chaos_file->name, chaos_file->extension);
+		#endif
+		
+		switch (zip_file->method) {
+			case 0: // STORE
+				chaos_file->data = malloc(zip_file->original_size);
+				memcpy(chaos_file->data, zip_file->data, zip_file->original_size);
+				chaos_file->size = zip_file->original_size;
+				break;
+			
+			case 8: // DEFLATE
+				chaos_file->data = inflateWrapped(zip_file->data, zip_file->original_size);
+				if (chaos_file->data == NULL)
+					return EXIT_FAILURE;
+				chaos_file->size = zip_file->original_size;
+				break;
+			
+			default:
+				return EXIT_FAILURE;
+		}
+		
+		switch (chaos_file->extension) {
+			struct blob blob;
+			
+			case FILE_EXT_TEXTURE:;
+				unsigned error;
+				struct plum_image *image = plum_load_image(chaos_file->data, chaos_file->size, PLUM_COLOR_32 | PLUM_ALPHA_INVERT, &error);
+				free(chaos_file->data);
+				if (image == NULL) {
+					free(chaos_file->name);
+					fprintf(stderr, "error: libplum: %s\n", plum_get_error_text(error));
+					break;
+				}
+				SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(image->data, image->width, image->height, 32, image->width * 4, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000);
+				if (surface == NULL) {
+					free(chaos_file->name);
+					plum_destroy_image(image);
+					fprintf(stderr, "error: SDL2: %s\n", SDL_GetError());
+					break;
+				}
+				SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
+				SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
+				SDL_FreeSurface(surface);
+				plum_destroy_image(image);
+				if (texture == NULL) {
+					free(chaos_file->name);
+					fprintf(stderr, "error: SDL2: %s\n", SDL_GetError());
+					break;
+				}
+				
+				blob = (struct blob) {.data = texture, .size = 0};
+				res_push_texture(&blob, chaos_file->name);
+				break;
+			
+			case FILE_EXT_MAP:
+				blob = (struct blob) {.data = chaos_file->data, .size = chaos_file->size};
+				res_push_map(&blob, chaos_file->name);
+				break;
+			
+			default:
+				free(chaos_file->data), chaos_file->data = NULL;
+				free(chaos_file->name), chaos_file->name = NULL;
+				break;
+		}
+	}
+	
+	free(file);
+	free(headers);
+	
+	free(files);
+
+	return EXIT_SUCCESS;
+}
+
+ext_T get_extension_type(char *extension, int length) {
+	#define ext(str, en) \
+		if (length == sizeof (str) - 1 && \
+		memcmp(extension, str, sizeof (str) - 1) == 0) \
+		return en
+	ext("png", FILE_EXT_TEXTURE);
+	ext("jpg", FILE_EXT_TEXTURE);
+	ext("gif", FILE_EXT_TEXTURE);
+	ext("map", FILE_EXT_MAP);
+	return FILE_EXT_UNKNOWN;
+	#undef ext
+}
+
+static void *inflateWrapped(void *const restrict data, uint32_t const outsize) {
+	z_stream stream = {
+		.zalloc = Z_NULL,
+		.zfree = Z_NULL,
+		.opaque = Z_NULL,
+	}; // zlib docs require you to set these 3 to Z_NULL, apparently
+	stream.avail_in = outsize; // hope itll work! hehe
+	stream.next_in = data;
+	int ret = inflateInit2(&stream, -15); // -15 is raw DEFLATE, we dont want zlib + DEFLATE :p
+	if (ret != Z_OK) {
+		return NULL;
+	}
+	unsigned char *buf = malloc(outsize);
+	if (buf == NULL) {
+		perror("malloc");
+		goto finally; // would it hurt to change it to error? free(NULL) is defined to be a no-op
+	}
+	stream.avail_out = outsize;
+	stream.next_out = buf;
+	
+	switch ((ret = inflate(&stream, Z_SYNC_FLUSH))) {
+		case Z_NEED_DICT:
+		case Z_DATA_ERROR:
+			//fputs("inflate: data error\n", stderr);
+			goto error;
+		case Z_MEM_ERROR:
+			//fputs("inflate: memory error\n", stderr);
+			goto error;
+		case Z_STREAM_ERROR:
+			//fputs("inflate: stream error\n", stderr);
+			goto error;
+		case Z_OK:
+			//fputs("inflate: stream didnt end", stderr);
+			goto error;
+		
+		case Z_STREAM_END:
+			goto finally;
+		
+		default:
+			//fprintf(stderr, "unrecognised return value %u\n", ret);
+			goto error;
+	}
+	
+	error: // all cases share the error handling code
+	free(buf);
+	buf = NULL; // return value
+	
+	finally:
+	(void) inflateEnd(&stream);
+	//fprintf(stderr, "processed %lu (0x%lx) bytes total, exit status %d\n", stream.total_out, stream.total_out, ret);
+	return buf;
+}
diff --git a/src/loader.h b/src/loader.h
new file mode 100644
index 0000000..45a1ac0
--- /dev/null
+++ b/src/loader.h
@@ -0,0 +1,16 @@
+typedef char * name_T;
+
+struct blob {
+	void *data;
+	size_t size;
+};
+
+void res_init_texture(void);
+void res_free_texture(void);
+struct blob res_get_texture(name_T const name);
+
+void res_init_map(void);
+void res_free_map(void);
+struct blob res_get_map(name_T const name);
+
+int loadResources(char *filename);
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..a304797
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,178 @@
+#define SDL_MAIN_HANDLED
+#include <SDL2/SDL.h>
+
+#include <stdlib.h>
+
+#include "input.h"
+#include "loader.h"
+#include "util.h"
+
+#include "tilemap.h"
+
+SDL_Window *window = NULL;
+SDL_Renderer *renderer = NULL;
+
+#define WINDOW_WIDTH 160
+#define WINDOW_HEIGHT 90
+#define INIT_SUBSYSTEMS SDL_INIT_VIDEO
+
+unsigned input_now = 0;
+SDL_Scancode keybinds[] = {
+	SDL_SCANCODE_UP,
+	SDL_SCANCODE_DOWN,
+	SDL_SCANCODE_LEFT,
+	SDL_SCANCODE_RIGHT,
+	SDL_SCANCODE_A,
+	SDL_SCANCODE_S,
+};
+
+int main(int argc, char **argv) {
+	if (SDL_Init(INIT_SUBSYSTEMS)) {
+		fprintf(stderr, "failed to initialize SDL2: %s\n", SDL_GetError());
+		return EXIT_FAILURE;
+	}
+	SDL_StopTextInput();
+	
+	window = SDL_CreateWindow(":3", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN);
+	if (window == NULL) {
+		goto end;
+	}
+	renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE| SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_PRESENTVSYNC);
+	if (renderer == NULL) {
+		goto end;
+	}
+	SDL_RenderSetLogicalSize(renderer, WINDOW_WIDTH, WINDOW_HEIGHT);
+	
+	{
+		res_init_texture();
+		res_init_map();
+		init_tilemap();
+		void *a = util_executableRelativePath("assets.res", *argv, 0);
+		puts(a);
+		if (loadResources(a)) {
+			fputs("loading resources failed\n", stderr);
+			return 1;
+		}
+		free(a);
+		struct blob map = res_get_map("out");
+		printf("load_tilemap %u\n", load_tilemap(map.data, map.size));
+		SDL_SetRenderTarget(renderer, NULL);
+	}
+
+	SDL_ShowWindow(window);
+	
+	int x = 0, y = 0;
+	while (1) {
+		SDL_Event evt;
+		while (SDL_PollEvent(&evt)) {
+			switch (evt.type) {
+				case SDL_QUIT: // the last window is closed, or the app is asked to terminate
+					fputs("quitting...\n", stderr);
+					goto end;
+				case SDL_KEYDOWN:
+				case SDL_KEYUP: // a key on is pressed or released
+					if (evt.key.repeat)
+						break;
+					
+					// check for ^Q and exit if pressed
+					if (evt.key.state == SDL_PRESSED && evt.key.keysym.sym == SDLK_q && evt.key.keysym.mod & KMOD_CTRL) {
+						goto end;
+					}
+					
+					//static_assert(INPUT_LENGTH <= sizeof(input_now) * CHAR_BIT); // if this trips up, scope creep happened
+					for (unsigned key = 0, bit = 1; key < INPUT_LENGTH; key++, bit <<= 1) {
+						if (evt.key.keysym.scancode == keybinds[key]) {
+							if (evt.key.state == SDL_PRESSED)
+								input_now |= bit;
+							else
+								input_now &= ~bit;
+						}
+					}
+					//fprintf(stderr, "input: %0*b\n", INPUT_LENGTH, input_now);
+					break;
+				case SDL_MOUSEMOTION: // the cursor moves
+					//fprintf(stderr, "mouse at %4i %4i\n", evt.motion.x, evt.motion.y);
+					break;
+				case SDL_MOUSEBUTTONDOWN:
+				case SDL_MOUSEBUTTONUP: // a left click or release
+					if (evt.button.state) {
+						//fprintf(stderr, "click %i\n", evt.button.button);
+					}
+					if (!evt.button.state) {
+						//fprintf(stderr, "unclick %i\n", evt.button.button);
+					}
+					break;
+				case SDL_MOUSEWHEEL: // the scroll wheel gets rolled
+					//fprintf(stderr, "scrolled by %2i %2i %4.1f %4.1f\n", evt.wheel.x, evt.wheel.y, evt.wheel.preciseX, evt.wheel.preciseY);
+					break;
+				case SDL_WINDOWEVENT: // window related events
+					switch (evt.window.event) {
+						case SDL_WINDOWEVENT_SHOWN:
+							//fprintf(stderr, "window %u shown\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_HIDDEN:
+							//fprintf(stderr, "window %u hidden\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_EXPOSED:
+							//fprintf(stderr, "window %u exposed\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_MOVED: // a window is moved
+							//fprintf(stderr, "window %u moved %4i %4i\n", evt.window.windowID, evt.window.data1, evt.window.data2);
+							//wx = evt.window.data1;
+							//wy = evt.window.data2;
+							break;
+						case SDL_WINDOWEVENT_MINIMIZED:
+							//fprintf(stderr, "window %u minimized\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_MAXIMIZED:
+							//fprintf(stderr, "window %u maximized\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_RESTORED:
+							//fprintf(stderr, "window %u restored\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_ENTER:
+						case SDL_WINDOWEVENT_LEAVE: // cursor enters or leaves a window
+						case SDL_WINDOWEVENT_FOCUS_GAINED:
+						case SDL_WINDOWEVENT_FOCUS_LOST: // keyboard
+
+							break;
+						case SDL_WINDOWEVENT_CLOSE: // a window is asked to close
+							//fprintf(stderr, "window %u closed\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_TAKE_FOCUS:
+							//fprintf(stderr, "window %u offered focus\n", evt.window.windowID);
+							break;
+						case SDL_WINDOWEVENT_HIT_TEST:
+							//fprintf(stderr, "window %u hit test\n", evt.window.windowID);
+							break;
+						default:
+							fprintf(stderr, "window %u event %i\n", evt.window.windowID, evt.window.event);
+					}
+					break;
+				default:
+					fprintf(stderr, "unknown event type %i\n", evt.type);
+			}
+		}
+		SDL_SetRenderTarget(renderer, NULL);
+		SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
+		SDL_RenderClear(renderer);
+		SDL_SetRenderDrawColor(renderer, 0, 128, 255, 0);
+		SDL_RenderFillRect(renderer, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT});
+		SDL_SetRenderDrawColor(renderer, 255, 255, 255, SDL_ALPHA_OPAQUE);
+		//SDL_RenderCopy(renderer, tilemap_tileset, &(SDL_Rect) {0, 0, 128, 90}, &(SDL_Rect) {0, 0, 128, 90});
+		SDL_RenderCopy(renderer, tilemap_tilemap, &(SDL_Rect) {x, y, WINDOW_WIDTH, WINDOW_HEIGHT}, &(SDL_Rect) {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT});
+		x += input_right(input_now) - input_left(input_now);
+		y += input_down(input_now) - input_up(input_now);
+		if (x < 0) {x = 0;} else if (x + WINDOW_WIDTH > 29 * 8) {x = 29 * 8 - WINDOW_WIDTH;}
+		if (y < 0) {y = 0;} else if (y + WINDOW_HEIGHT > 19 * 8) {y = 19 * 8 - WINDOW_HEIGHT;}
+		//SDL_RenderCopy(renderer, res_get_texture("meow").data, &(SDL_Rect) {0, 0, 128, 90}, &(SDL_Rect) {0, 0, 128, 90});
+		// then we wait for the next video frame 
+		SDL_RenderPresent(renderer);
+	}
+	
+	end:
+	SDL_DestroyRenderer(renderer);
+	SDL_DestroyWindow(window);
+	//SDL_QuitSubSystem(INIT_SUBSYSTEMS);
+	return 0;
+}
diff --git a/src/main.h b/src/main.h
new file mode 100644
index 0000000..0595b37
--- /dev/null
+++ b/src/main.h
@@ -0,0 +1,3 @@
+//#include <SDL2/SDL.h>
+extern void /*SDL_Window*/ *window;
+extern void /*SDL_Renderer*/ *renderer;
diff --git a/src/tilemap.c b/src/tilemap.c
new file mode 100644
index 0000000..a63cef5
--- /dev/null
+++ b/src/tilemap.c
@@ -0,0 +1,125 @@
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+
+#include <SDL2/SDL.h>
+
+#include "loader.h"
+#include "main.h"
+
+SDL_Texture *tilemap_tileset = NULL, *tilemap_wang_tileset = NULL, *tilemap_tilemap = NULL;
+
+#define PACKED __attribute__((__packed__))
+
+struct PACKED sets {
+	uint32_t tilesets_count;
+	uint32_t wang_count;
+};
+
+struct PACKED map {
+	uint32_t width;
+	uint32_t height;
+	uint32_t layers;
+	uint8_t tiles[];
+};
+
+void init_tilemap(void) {
+	tilemap_tileset = SDL_CreateTexture(renderer, SDL_PIXELTYPE_UNKNOWN, SDL_TEXTUREACCESS_TARGET, 128, 128);
+	tilemap_wang_tileset = SDL_CreateTexture(renderer, SDL_PIXELTYPE_UNKNOWN, SDL_TEXTUREACCESS_TARGET, 128, 128);
+	SDL_SetTextureBlendMode(tilemap_wang_tileset, SDL_BLENDMODE_BLEND);
+}
+
+void free_tilemap(void) {
+	SDL_DestroyTexture(tilemap_tileset), tilemap_tileset = NULL;
+	SDL_DestroyTexture(tilemap_wang_tileset), tilemap_wang_tileset = NULL;
+	SDL_DestroyTexture(tilemap_tilemap), tilemap_tilemap = NULL;
+}
+
+int load_tilemap(void const *data, size_t size) {
+	struct sets const *const sets = data;
+	data = sets + 1;
+	size -= sizeof (struct sets);
+	char const *str = data;
+	int y = 0;
+	SDL_SetRenderTarget(renderer, tilemap_tileset);
+	SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
+	SDL_RenderClear(renderer);
+
+	for (uint32_t count = sets->tilesets_count; count > 0; count--) {
+		int height = *(uint32_t *) str;
+		str += sizeof (uint32_t);
+		size_t len = strnlen(str, size);
+		if (len == size) {
+			return 1;
+		}
+		void *tex = res_get_texture(str).data;
+		
+		//printf("%u %u\n", y, height);
+		SDL_RenderCopy(renderer, tex, &(SDL_Rect) {0, 0, 128, height}, &(SDL_Rect) {0, y, 128, height});
+		
+		//printf("%s %u\n", str, tex);
+		y += height;
+		str += len + 1;
+		size -= len + 4 + 1;
+	}
+	
+	SDL_SetRenderTarget(renderer, tilemap_wang_tileset);
+	SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
+	SDL_RenderClear(renderer);
+
+	if (sets->tilesets_count) {
+		int height = *(uint32_t *) str;
+		str += sizeof (uint32_t);
+		size_t len = strnlen(str, size);
+		if (len == size) {
+			return 1;
+		}
+		void *tex = res_get_texture(str).data;
+		
+		//printf("%u %u\n", y, height);
+		SDL_RenderCopy(renderer, tex, &(SDL_Rect) {0, 0, 128, height}, &(SDL_Rect) {0, 0, 128, height});
+
+		//printf("%s %u\n", str, tex);
+		str += len + 1;
+		size -= len + 4 + 1;
+	}
+	
+	struct map const *const map = (void *) str;
+	if (map->width * map->height * map->layers != size - sizeof (struct map)) {
+		return 1;
+	}
+	
+	SDL_Texture *tilemap = SDL_CreateTexture(renderer, SDL_PIXELTYPE_UNKNOWN, SDL_TEXTUREACCESS_TARGET, map->width * 8, map->height * 8);
+	SDL_SetTextureBlendMode(tilemap, SDL_BLENDMODE_BLEND);
+	SDL_SetRenderTarget(renderer, tilemap);
+
+	for (int y = 0; y < map->height; y++) {
+		for (int x = 0; x < map->width; x++) {
+			unsigned tile = map->tiles[x + map->width * y];
+			int tileX = tile & 0x0f;
+			int tileY = tile >> 4;
+			SDL_RenderCopy(renderer, tilemap_tileset, &(SDL_Rect) {tileX * 8, tileY * 8, 8, 8}, &(SDL_Rect) {x * 8, y * 8, 8, 8});
+		}
+	}
+
+	for (int tile = 0xf0; tile < 0x100; tile++) {
+		for (int y = 0; y < map->height - 1; y++) {
+			for (int x = 0; x < map->width - 1; x++) {
+				int tl = map->tiles[x + map->width * y] == tile;
+				int tr = map->tiles[x + 1 + map->width * y] == tile;
+				int bl = map->tiles[x + map->width * (y + 1)] == tile;
+				int br = map->tiles[x + 1 + map->width * (y + 1)] == tile;
+				//int tileX = tl << 3 | tr << 2 | bl << 1 | br << 0;
+				int tileX = tl << 0 | tr << 1 | bl << 2 | br << 3;
+				int tileY = tile & 0x0f;
+				SDL_RenderCopy(renderer, tilemap_wang_tileset, &(SDL_Rect) {tileX * 8, tileY * 8, 8, 8}, &(SDL_Rect) {x * 8 + 4, y * 8 + 4, 8, 8});
+			}
+		}
+	}
+
+	SDL_DestroyTexture(tilemap_tilemap), tilemap_tilemap = tilemap;
+
+	SDL_SetRenderTarget(renderer, NULL);
+	
+	return 0;
+}
diff --git a/src/tilemap.h b/src/tilemap.h
new file mode 100644
index 0000000..54e051b
--- /dev/null
+++ b/src/tilemap.h
@@ -0,0 +1,5 @@
+extern void /*SDL_Texture*/ *tilemap_tileset, *tilemap_wang_tileset, *tilemap_tilemap;
+
+void init_tilemap(void);
+void free_tilemap(void);
+int load_tilemap(void const *data, size_t size);
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..fc20e94
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,66 @@
+#include <stdio.h> // FILE *
+#include <errno.h> // ESPIPE
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h> // true/false
+
+#include "util.h"
+
+void *util_loadFile(FILE *const restrict file, size_t *const restrict outsize) { // open and fully load a file
+	if (ftell(file) == -1L) {
+		if (errno != ESPIPE) {
+			// perror("ftell");
+			return NULL;
+		}
+		fputs("opening pipes isnt supported yet\n", stderr);
+		return NULL;
+	} else {
+		if (fseek(file, 0, SEEK_END)) {
+			// perror("fseek");
+		}
+		long ssize = ftell(file);
+		size_t size = (size_t) ssize;
+		if (ssize == -1L) {
+			// perror("ftell");
+			return NULL;
+		}
+		rewind(file);
+		void *buffer = malloc(size);
+		if (buffer == NULL) {
+			// perror("malloc");
+			return NULL;
+		}
+		if (fread(buffer, 1, size, file) != size) {
+			if (ferror(file)) {
+				fputs("ferror set\n", stderr); // the internet was VERY helpful
+			}
+			// perror("fread");
+			return NULL;
+		}
+		if (outsize != NULL) {
+			*outsize = size;
+		}
+		return buffer;
+	}
+}
+
+#if defined(__unix__) || defined(__APPLE__)
+	#define DIR_SEPARATOR '/' // UNIX or OSX
+#elif defined(_WIN32)
+	#define DIR_SEPARATOR '\\' // DOS
+#endif
+
+char *util_executableRelativePath(char *path, char *execPath, size_t dirLength) { // allocated on the heap
+	dirLength = 0;
+	if (dirLength == 0) {
+		for (dirLength = strlen(execPath); dirLength > 0 && execPath[dirLength - 1] != DIR_SEPARATOR; dirLength--)
+			;
+	}
+	
+	size_t fileLength = strlen(path);
+	char *filePath = malloc(dirLength + fileLength + 1);
+	filePath[dirLength + fileLength] = 0;
+	memcpy(filePath, execPath, dirLength);
+	memcpy(filePath + dirLength, path, fileLength);
+	return filePath;
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..1fe3a9d
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,2 @@
+void *util_loadFile(FILE *const restrict file, size_t *const restrict outsize);
+char *util_executableRelativePath(char *path, char *execPath, size_t dirLength); // allocated on the heap
diff --git a/src/zip.c b/src/zip.c
new file mode 100644
index 0000000..aca980d
--- /dev/null
+++ b/src/zip.c
@@ -0,0 +1,168 @@
+#include <stdlib.h> // malloc
+#include <string.h> // memcmp
+
+#include "zip.h"
+
+static struct zip_footer *zip_find_footer(char *const file, size_t const size, int *const error);
+static int index_zip(struct zip_index *const restrict headers, char *const restrict file, struct zip_footer const *const restrict footer);
+
+enum {
+	ZIP_OK,
+	ZIP_BIG,
+	ZIP_SMALL,
+	ZIP_SPLIT,
+	ZIP_NO_FOOTER,
+	ZIP_BAD_SIGNATURE,
+	ZIP_UNSUPPORTED,
+	ZIP_SIZE_MISMATCH,
+	ZIP_VALUE_MISMATCH,
+	ZIP_64,
+};
+
+char const *const zip_error(int const code) {
+	return (char const *const []) {
+		"ok",
+		"probably not a zip file (file too big)",
+		"not a zip file (file too smol)",
+		"split archives arent supported",
+		"found no zip footer",
+		"bad signature",
+		"unsupported configuration",
+		"size mismatch",
+		"central and local headers dont match",
+		"zip 64 is not supported",
+	} [code];
+}
+
+struct zip_file *zip_index(char *const restrict file, size_t const size, int *const restrict error) {
+	int err;
+	// find and verify the footer
+	struct zip_footer *footer = zip_find_footer(file, size, &err);
+	if (footer == NULL) {
+		error && (*error = err);
+		return NULL;
+	}
+	
+	// The Rest &trade;
+	struct zip_file *headers = malloc(sizeof (*headers) + footer->headers_no * sizeof (*headers->files));
+	err = index_zip(headers->files, file, footer);
+	if (err) {
+		error && (*error = err);
+		free(headers);
+		return NULL;
+	}
+	headers->file_count = footer->headers_no;
+	return headers;
+}
+
+static struct zip_footer *zip_find_footer(char *const file, size_t const size, int *const error) {
+	// check if zip file is too big, and if size_t can store values bigger than a max* size zip file
+	if ((size_t) 1 << 32 && size >= (size_t) 1 << 32) {
+		*error = ZIP_BIG;
+		return NULL;
+	}
+
+	// the other way around, smallest zip file is 22 bytes
+	if (size < sizeof (struct zip_footer)) {
+		*error = ZIP_SMALL;
+		return NULL;
+	}
+
+	// scan for the footer
+	for (size_t i = 0, bounds = size - sizeof (struct zip_footer) + 1; i < 65536 /* hacky limit */ && /* obligatory check */ bounds; i++, bounds--) {
+		struct zip_footer *footer = (struct zip_footer *) (file + (size - sizeof (struct zip_footer) - i));
+		if (
+			footer->magic != ZIP_FOOTER_MAGIC               || // incorrect signature
+			footer->comment_length != i                     || // incorrect comment length
+			footer->headers_no_total < footer->headers_no   || // logical error
+			footer->headers_addr > size                     || // metadata allegedly located after EOF
+			footer->headers_addr + footer->headers_size > size // metadata ends after EOF
+		) {
+			continue; // not the footer / unusable, try again
+		}
+		if (footer->disk_no != 0 || footer->central_disk_no != 0) { // split archive
+			*error = ZIP_SPLIT;
+			continue;
+		}
+		*error = ZIP_OK;
+		return footer; // 100% valid footer, safe to return
+	}
+	if (*error != ZIP_SPLIT) {
+		*error = ZIP_NO_FOOTER;
+	}
+	return NULL;
+}
+
+static int index_zip(struct zip_index *const restrict headers, char *const restrict file, struct zip_footer const *const footer) {
+	// needs more error checking
+	char *current_header = file + footer->headers_addr;
+	for (uint16_t i = 0; i < footer->headers_no; i++) {
+		struct zip_central *const central = (void *) current_header;
+		struct zip_header *const local = (void *) (file + central->file_addr);
+		
+		// magic number check
+		if (central->magic != ZIP_CENTRAL_MAGIC) {
+			return ZIP_BAD_SIGNATURE;
+		}
+		
+		// zip64 detection
+		if (
+			central->compressed_size == 0xffffffff &&
+			central->original_size == 0xffffffff &&
+			central->file_disk_no == 0xffff &&
+			central->file_addr == 0xffffffff
+		) {
+			return ZIP_64;
+		}
+		
+		// magic check 2
+		if (local->magic != ZIP_HEADER_MAGIC) {
+			return ZIP_BAD_SIGNATURE;
+		}
+
+		// more zip64 detection
+		if (
+			local->compressed_size == 0xffffffff &&
+			local->original_size == 0xffffffff
+		) {
+			return ZIP_64;
+		}
+
+		// various checks
+		if (
+			local->ver_needed != central->ver_needed                        ||
+			local->flags != central->flags                                  ||
+			local->method != central->method                                ||
+			memcmp(&local->mtime, &central->mtime, sizeof (struct dos_date))||
+			local->checksum != central->checksum                            ||
+			local->compressed_size != central->compressed_size              ||
+			local->original_size != central->original_size                  ||
+			local->filename_length != central->filename_length              ||
+			memcmp(local->filename, central->filename, local->filename_length)
+		) {
+			return ZIP_VALUE_MISMATCH;
+		}
+		
+		// feature check
+		if (central->flags & 0x0079) { // 0b0000'0000'0111'1001, that is encryption, data trailer, "enhanced deflating", or patches
+			return ZIP_UNSUPPORTED;
+		}
+
+		headers[i].central = central;
+		headers[i].header = local;
+		
+		headers[i].data = headers[i].header->filename + local->filename_length + local->extra_length;
+		headers[i].filename = local->filename;
+		headers[i].filename_length = local->filename_length;
+		headers[i].crc32 = local->checksum;
+		headers[i].compressed_size = local->compressed_size;
+		headers[i].original_size = local->original_size;
+		headers[i].method = local->method;
+		headers[i].flags = local->flags;
+		current_header += sizeof (struct zip_central) + central->filename_length + central->extra_length + central->comment_length;
+	}
+	if (current_header - footer->headers_size != file + footer->headers_addr) {
+		return ZIP_SIZE_MISMATCH;
+	}
+	return 0;
+}
diff --git a/src/zip.h b/src/zip.h
new file mode 100644
index 0000000..d8b3e02
--- /dev/null
+++ b/src/zip.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define ZIP_FOOTER_MAGIC  0x06054b50 // "PK\5\6"
+#define ZIP_CENTRAL_MAGIC 0x02014b50 // "PK\1\2"
+#define ZIP_HEADER_MAGIC  0x04034b50 // "PK\3\4"
+
+struct zip_file *zip_index(char *const restrict file, size_t const size, int *const restrict /* _Nullable */ error);
+char const *const zip_error(int const code); // error messages do NOT have any newlines
+
+struct zip_index {
+	struct zip_central *central;
+	struct zip_header *header;
+	void *data;
+	uint8_t *filename;
+	size_t filename_length;
+	uint16_t flags;
+	uint16_t method;
+	uint32_t crc32;
+	uint32_t compressed_size;
+	uint32_t original_size;
+};
+
+struct zip_file {
+	size_t file_count;
+	struct zip_index files[];
+};
+
+struct dos_date {
+	uint16_t second: 5, minute: 6, hour: 5;
+	uint16_t day:    5, month:  4, year: 7;
+};
+
+struct __attribute__((__packed__)) zip_footer {
+	uint32_t magic;
+	uint16_t disk_no;
+	uint16_t central_disk_no;
+	uint16_t headers_no;
+	uint16_t headers_no_total;
+	uint32_t headers_size;
+	uint32_t headers_addr;
+	uint16_t comment_length;
+	uint8_t comment[];
+};
+
+struct __attribute__((__packed__)) zip_central {
+	uint32_t magic;
+	uint16_t ver_used;
+	uint16_t ver_needed;
+	uint16_t flags;
+	uint16_t method;
+	struct dos_date mtime;
+	uint32_t checksum;
+	uint32_t compressed_size;
+	uint32_t original_size;
+	uint16_t filename_length;
+	uint16_t extra_length;
+	uint16_t comment_length;
+	uint16_t file_disk_no;
+	uint16_t internal_attr;
+	uint32_t external_attr;
+	uint32_t file_addr;
+	uint8_t filename[];
+};
+
+struct __attribute__((__packed__)) zip_header {
+	uint32_t magic;
+	uint16_t ver_needed;
+	uint16_t flags;
+	uint16_t method;
+	struct dos_date mtime;
+	uint32_t checksum;
+	uint32_t compressed_size;
+	uint32_t original_size;
+	uint16_t filename_length;
+	uint16_t extra_length;
+	uint8_t filename[];
+};