summaryrefslogtreecommitdiff
path: root/src/image_processor.cc
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2026-01-02 22:42:31 +0100
committerJoel Klinghed <the_jk@spawned.biz>2026-01-02 22:42:31 +0100
commit6ed8f5151719fbc14ec0ac6d28a346d1f74cf2ca (patch)
treeebe7588e89e1aa2ae5376acf85f3a3a7b2ec7e10 /src/image_processor.cc
Initial commitHEADmain
Diffstat (limited to 'src/image_processor.cc')
-rw-r--r--src/image_processor.cc947
1 files changed, 947 insertions, 0 deletions
diff --git a/src/image_processor.cc b/src/image_processor.cc
new file mode 100644
index 0000000..55e3cac
--- /dev/null
+++ b/src/image_processor.cc
@@ -0,0 +1,947 @@
+#include "image_processor.hh"
+
+#include "colour.hh"
+#include "config.h"
+#include "image.hh"
+#include "image_loader.hh"
+#include "io.hh"
+#include "size.hh"
+#include "spawner.hh"
+
+#include <algorithm>
+#include <bit>
+#include <cassert>
+#include <cctype>
+#include <charconv>
+#include <cstdint>
+#include <cstring>
+#include <expected>
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <system_error>
+#include <utility>
+
+#if HAVE_JPEG || HAVE_PNG
+# include <cerrno>
+# include <csetjmp>
+# include <cstddef>
+# include <cstdio>
+#endif
+
+#if HAVE_JPEG
+# include <jpeglib.h>
+#endif
+
+#if HAVE_PNG
+# include <png.h>
+#endif
+
+#if HAVE_RSVG
+# include <cairo.h>
+# include <cmath>
+# include <gio/gio.h>
+# include <glib-object.h>
+# include <librsvg/rsvg.h>
+#endif
+
+#if HAVE_XPM
+# include <X11/xpm.h>
+extern "C" {
+# include "xpm/include/dix.h"
+}
+#endif
+
+namespace {
+
+struct Request {
+ bool head;
+ Image::Format format;
+ uint32_t max_width;
+ uint32_t max_height;
+ uint32_t background;
+ size_t path_len;
+};
+
+struct Response {
+ uint32_t width;
+ uint32_t height;
+ uint8_t error;
+ size_t scanline;
+};
+
+struct Result {
+ Response response;
+ Image::Format format;
+ std::unique_ptr<uint8_t[]> pixels;
+};
+
+std::expected<void, ImageLoadError> write_request(
+ io::Writer& writer, std::filesystem::path const& path, bool head,
+ Image::Format format, uint32_t max_width, uint32_t max_height,
+ Colour background) {
+ auto path_str = path.native();
+ Request request{.head = head, .format = format, .max_width = max_width,
+ .max_height = max_height, .background = background.argb,
+ .path_len = path_str.size()};
+ auto ret = writer.repeat_write(&request, sizeof(request));
+ if (!ret.has_value() || ret.value() != sizeof(request)) {
+ return std::unexpected(ImageLoadError::kProcessError);
+ }
+ ret = writer.repeat_write(path_str.data(), path_str.size());
+ if (!ret.has_value() || ret.value() != path_str.size()) {
+ return std::unexpected(ImageLoadError::kProcessError);
+ }
+ return {};
+}
+
+std::expected<Response, ImageLoadError> read_response(io::Reader& reader) {
+ Response response;
+ auto ret = reader.repeat_read(&response, sizeof(response));
+ if (!ret.has_value() || ret.value() != sizeof(response)) {
+ return std::unexpected(ImageLoadError::kProcessError);
+ }
+ if (response.width == 0) {
+ return std::unexpected(static_cast<ImageLoadError>(response.error));
+ }
+ return response;
+}
+
+Size rescale(Size const& size, uint32_t max_width, uint32_t max_height) {
+ if (max_width > 0 && size.width > max_height) {
+ if (max_height > 0 && size.height > max_height) {
+ auto sx = static_cast<float>(size.width) / static_cast<float>(max_width);
+ auto sy = static_cast<float>(size.height) / static_cast<float>(max_height);
+ if (sx >= sy) {
+ return Size{max_width, (size.height * max_width) / size.width};
+ }
+ return Size{(size.width * max_height) / size.height, max_height};
+ }
+ return Size{max_width, (size.height * max_width) / size.width};
+ }
+ if (max_height > 0 && size.height > max_height) {
+ return Size{(size.width * max_height) / size.height, max_height};
+ }
+ return size;
+}
+
+void swap_four_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline) {
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* const pixel = reinterpret_cast<uint32_t*>(row);
+ for (uint32_t x = 0; x < width; ++x) {
+ pixel[x] = std::byteswap(pixel[x]);
+ }
+ row += scanline;
+ }
+}
+
+void swap_two_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline, size_t offset) {
+ assert(offset < 4);
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* pixel = row + offset;
+ for (uint32_t x = 0; x < width; ++x, pixel += 4) {
+ std::swap(pixel[0], pixel[2]);
+ }
+ row += scanline;
+ }
+}
+
+void shift_left_bytes(std::span<uint8_t> pixels, uint32_t width,
+ uint32_t height, size_t scanline) {
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* const pixel = reinterpret_cast<uint32_t*>(row);
+ for (uint32_t x = 0; x < width; ++x) {
+ pixel[x] = std::rotl(pixel[x], 8);
+ }
+ row += scanline;
+ }
+}
+
+void shift_right_bytes(std::span<uint8_t> pixels, uint32_t width,
+ uint32_t height, size_t scanline) {
+ assert(height * scanline <= pixels.size());
+ auto* row = pixels.data();
+ while (height--) {
+ auto* const pixel = reinterpret_cast<uint32_t*>(row);
+ for (uint32_t x = 0; x < width; ++x) {
+ pixel[x] = std::rotr(pixel[x], 8);
+ }
+ row += scanline;
+ }
+}
+
+void set_alpha_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline, size_t offset) {
+ assert(height * scanline <= pixels.size());
+ assert(offset < 4);
+ auto* row = pixels.data();
+ while (height--) {
+ auto* pixel = row + offset;
+ for (uint32_t x = 0; x < width; ++x, pixel += 4) {
+ *pixel = 0xff;
+ }
+ row += scanline;
+ }
+}
+
+void insert_alpha_bytes(std::span<uint8_t> pixels, uint32_t width,
+ uint32_t height, size_t scanline, size_t offset) {
+ assert(static_cast<size_t>(width) * 4 <= scanline);
+ assert(height * scanline <= pixels.size());
+
+ if (offset == 0) {
+ auto* row = pixels.data();
+ while (height--) {
+ auto* read_pixel = row + static_cast<size_t>((width - 1) * 3);
+ auto* write_pixel = row + static_cast<size_t>((width - 1) * 4);
+ for (uint32_t x = 0; x < width; ++x, read_pixel -= 3, write_pixel -= 4) {
+ write_pixel[0] = 0xff;
+ std::copy_n(read_pixel, 3, write_pixel + 1);
+ }
+ row += scanline;
+ }
+ } else {
+ assert(offset == 3);
+
+ auto* row = pixels.data();
+ while (height--) {
+ auto* read_pixel = row + static_cast<size_t>((width - 1) * 3);
+ auto* write_pixel = row + static_cast<size_t>((width - 1) * 4);
+ for (uint32_t x = 0; x < width; ++x, read_pixel -= 3, write_pixel -= 4) {
+ std::copy_n(read_pixel, 3, write_pixel);
+ write_pixel[3] = 0xff;
+ }
+ row += scanline;
+ }
+ }
+}
+
+void rearrange_bytes(std::span<uint8_t> pixels, uint32_t width, uint32_t height,
+ size_t scanline, Image::Format source,
+ Image::Format target) {
+ switch (source) {
+ case Image::Format::RGBA_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ return;
+ case Image::Format::ARGB_8888:
+ shift_right_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::BGRA_8888:
+ swap_two_bytes(pixels, width, height, scanline, 0);
+ return;
+ case Image::Format::ABGR_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ }
+ break;
+ case Image::Format::ARGB_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ shift_left_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ARGB_8888:
+ return;
+ case Image::Format::BGRA_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ABGR_8888:
+ swap_two_bytes(pixels, width, height, scanline, 1);
+ return;
+ }
+ break;
+ case Image::Format::BGRA_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ swap_two_bytes(pixels, width, height, scanline, 0);
+ return;
+ case Image::Format::ARGB_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::BGRA_8888:
+ return;
+ case Image::Format::ABGR_8888:
+ shift_left_bytes(pixels, width, height, scanline);
+ return;
+ }
+ break;
+ case Image::Format::ABGR_8888:
+ switch (target) {
+ case Image::Format::RGBA_8888:
+ swap_four_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ARGB_8888:
+ swap_two_bytes(pixels, width, height, scanline, 1);
+ return;
+ case Image::Format::BGRA_8888:
+ shift_right_bytes(pixels, width, height, scanline);
+ return;
+ case Image::Format::ABGR_8888:
+ return;
+ }
+ break;
+ }
+}
+
+#if HAVE_JPEG
+struct jpeg_extended_error_mgr {
+ struct jpeg_error_mgr err;
+ jmp_buf jmpbuf;
+};
+
+void jpeg_error_exit(j_common_ptr info) {
+ auto* err = reinterpret_cast<jpeg_extended_error_mgr*>(info->err);
+ jpeg_destroy(info);
+ longjmp(err->jmpbuf, 1);
+}
+
+void jpeg_error_output_nothing(j_common_ptr /* info */) {
+ // be silent, do nothing
+}
+
+# if BITS_IN_JSAMPLE != 8
+# error Unsupported libjpeg setup
+# endif
+
+std::expected<Result, ImageLoadError> load_jpeg(
+ std::filesystem::path const& path, Request const& request) {
+ jpeg_extended_error_mgr err;
+ FILE* fh = nullptr;
+ if (setjmp(err.jmpbuf)) {
+ // This is better than exit() which is the default behavior,
+ // but almost guaranteed to leak some memory here.
+ if (fh)
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ fh = fopen(path.c_str(), "rb");
+ if (fh == nullptr) {
+ if (errno == ENOENT) {
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ }
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ jpeg_decompress_struct info;
+ info.err = jpeg_std_error(&err.err);
+ info.err->error_exit = jpeg_error_exit;
+ info.err->output_message = jpeg_error_output_nothing;
+
+ jpeg_create_decompress(&info);
+
+ jpeg_stdio_src(&info, fh);
+ if (jpeg_read_header(&info, TRUE) != JPEG_HEADER_OK) {
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (request.head) {
+ Response resp{.width = info.image_width, .height = info.image_height, .error = 0, .scanline = 0};
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+ return Result{.response=resp, .format=Image::Format::RGBA_8888, .pixels=nullptr};
+ }
+
+ enum class Conversion : uint8_t {
+ kNone,
+ kSetAlphaFirst,
+ kSetAlphaLast,
+ kAddAlphaFirst,
+ kAddAlphaLast,
+ } conversion = Conversion::kNone;
+ Image::Format output_format = request.format;
+
+# if JCS_EXTENSIONS
+# if JCS_ALPHA_EXTENSIONS
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ info.out_color_space = JCS_EXT_RGBA;
+ break;
+ case Image::Format::BGRA_8888:
+ info.out_color_space = JCS_EXT_BGRA;
+ break;
+ case Image::Format::ABGR_8888:
+ info.out_color_space = JCS_EXT_ABGR;
+ break;
+ case Image::Format::ARGB_8888:
+ info.out_color_space = JCS_EXT_ARGB;
+ break;
+ }
+# else
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ info.out_color_space = JCS_EXT_RGBX;
+ conversion = Conversion::kSetAlphaLast;
+ break;
+ case Image::Format::BGRA_8888:
+ info.out_color_space = JCS_EXT_BGRX;
+ conversion = Conversion::kSetAlphaLast;
+ break;
+ case Image::Format::ABGR_8888:
+ info.out_color_space = JCS_EXT_XBGR;
+ conversion = Conversion::kSetAlphaFirst;
+ break;
+ case Image::Format::ARGB_8888:
+ info.out_color_space = JCS_EXT_XRGB;
+ conversion = Conversion::kSetAlphaFirst;
+ break;
+ }
+# endif
+# else
+# if RGB_RED != 0 || RGB_GREEN != 1 || RGB_BLUE != 2 || RGB_PIXELSIZE != 3
+# error Unsupported libjpeg setup
+# endif
+ info.out_color_space = JCS_RGB;
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ case Image::Format::BGRA_8888:
+ format = Image::Format::RGBA_8888;
+ conversion = Conversion::kAddAlphaLast;
+ break;
+ case Image::Format::ABGR_8888:
+ case Image::Format::ARGB_8888:
+ format = Image::Format::ARGB_8888;
+ conversion = Conversion::kAddAlphaFirst;
+ break;
+ }
+# endif
+
+ if (request.max_width || request.max_height) {
+ auto new_size = rescale(Size{info.image_width, info.image_height},
+ request.max_width, request.max_height);
+ unsigned int denom = 2;
+ while (info.image_width / denom >= new_size.width &&
+ info.image_height / denom >= new_size.height) {
+ info.scale_denom = denom;
+ denom *= 2;
+ }
+ }
+
+ jpeg_start_decompress(&info);
+
+ size_t scanline = static_cast<size_t>(info.output_width) * 4;
+ auto pixels = std::make_unique<uint8_t[]>(scanline * info.output_height);
+
+ auto buffer = std::make_unique<JSAMPROW[]>(info.rec_outbuf_height);
+
+ while (info.output_scanline < info.output_height) {
+ int buf_height = 1;
+ buffer[0] = reinterpret_cast<JSAMPROW>(pixels.get() +
+ (info.output_scanline * scanline));
+ while (buf_height < info.rec_outbuf_height &&
+ info.output_scanline + buf_height < info.output_height) {
+ buffer[buf_height] = reinterpret_cast<JSAMPROW>(
+ pixels.get() + ((info.output_scanline + buf_height) * scanline));
+ ++buf_height;
+ }
+ jpeg_read_scanlines(&info, buffer.get(), buf_height);
+ }
+
+ switch (conversion) {
+ case Conversion::kNone:
+ break;
+ case Conversion::kAddAlphaFirst:
+ insert_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 0);
+ break;
+ case Conversion::kAddAlphaLast:
+ insert_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 3);
+ break;
+ case Conversion::kSetAlphaFirst:
+ set_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 0);
+ break;
+ case Conversion::kSetAlphaLast:
+ set_alpha_bytes(std::span{pixels.get(), scanline * info.output_height},
+ info.output_width, info.output_height, scanline, 3);
+ break;
+ }
+
+ Response resp{.width = info.output_width, .height = info.output_height, .error = 0, .scanline = scanline};
+ jpeg_destroy_decompress(&info);
+ fclose(fh);
+ return Result{.response = resp, .format = output_format, .pixels = std::move(pixels)};
+}
+#endif
+
+#if HAVE_PNG
+// NOLINTBEGIN(misc-include-cleaner)
+void png_error(png_structp png_ptr, png_const_charp /* message */) {
+ // Don't write anything
+ longjmp(png_jmpbuf(png_ptr), 1);
+}
+
+void png_warning(png_structp /* png_ptr */, png_const_charp /* message */) {
+ // Do nothing
+}
+
+std::expected<Result, ImageLoadError> load_png(
+ std::filesystem::path const& path, Request const& request) {
+ FILE* fh = fopen(path.c_str(), "rb");
+ if (fh == nullptr) {
+ if (errno == ENOENT) {
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ }
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ uint8_t header[8];
+ if (fread(header, 1, sizeof(header), fh) != sizeof(header)) {
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (png_sig_cmp(header, 0, sizeof(header))) {
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr,
+ png_error, png_warning);
+
+ if (!png_ptr) {
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ png_infop info_ptr = png_create_info_struct(png_ptr);
+ if (!info_ptr) {
+ png_destroy_read_struct(&png_ptr, nullptr, nullptr);
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (setjmp(png_jmpbuf(png_ptr))) {
+ png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
+ fclose(fh);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ png_init_io(png_ptr, fh);
+ png_set_sig_bytes(png_ptr, sizeof(header));
+
+ png_set_alpha_mode(png_ptr, PNG_ALPHA_PNG, PNG_DEFAULT_sRGB);
+
+ png_read_info(png_ptr, info_ptr);
+
+ png_uint_32 width;
+ png_uint_32 height;
+ int bit_depth;
+ int color_type;
+
+ png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type,
+ nullptr, nullptr, nullptr);
+
+ if (request.head) {
+ Response resp{.width =width, .height = height, .error = 0, .scanline = 0};
+ png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
+ fclose(fh);
+ return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr};
+ }
+
+ if (request.background != 0) {
+ png_color_16 background_color;
+ Colour colour{request.background};
+ background_color.red = colour.red() << 8 | colour.red();
+ background_color.green = colour.green() << 8 | colour.green();
+ background_color.blue = colour.blue() << 8 | colour.blue();
+ png_set_background(png_ptr, &background_color, PNG_BACKGROUND_GAMMA_SCREEN,
+ 0, 1);
+ } else {
+ png_color_16p background_color;
+ if (png_get_bKGD(png_ptr, info_ptr, &background_color))
+ png_set_background(png_ptr, background_color, PNG_BACKGROUND_GAMMA_FILE,
+ 1, 1);
+ }
+
+ if (color_type == PNG_COLOR_TYPE_PALETTE)
+ png_set_palette_to_rgb(png_ptr);
+
+ if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
+ png_set_tRNS_to_alpha(png_ptr);
+
+ if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
+ png_set_expand_gray_1_2_4_to_8(png_ptr);
+
+ if (bit_depth == 16)
+ png_set_scale_16(png_ptr);
+
+ if (color_type == PNG_COLOR_TYPE_GRAY ||
+ color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
+ png_set_gray_to_rgb(png_ptr);
+
+ switch (request.format) {
+ case Image::Format::RGBA_8888:
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);
+ break;
+ case Image::Format::ARGB_8888:
+ png_set_swap_alpha(png_ptr);
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_BEFORE);
+ break;
+ case Image::Format::BGRA_8888:
+ png_set_bgr(png_ptr);
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_AFTER);
+ break;
+ case Image::Format::ABGR_8888:
+ png_set_bgr(png_ptr);
+ png_set_swap_alpha(png_ptr);
+ png_set_filler(png_ptr, 0xff, PNG_FILLER_BEFORE);
+ break;
+ }
+
+ png_read_update_info(png_ptr, info_ptr);
+
+ width = png_get_image_width(png_ptr, info_ptr);
+ height = png_get_image_height(png_ptr, info_ptr);
+ bit_depth = png_get_bit_depth(png_ptr, info_ptr);
+ color_type = png_get_color_type(png_ptr, info_ptr);
+ size_t scanline = png_get_rowbytes(png_ptr, info_ptr);
+
+ /* Guard against integer overflow */
+ if (height > PNG_SIZE_MAX / scanline)
+ png_error(png_ptr, "image_data buffer would be too large");
+
+ auto pixels = std::make_unique<uint8_t[]>(scanline * height);
+ auto rows = std::make_unique<png_bytep[]>(height);
+
+ for (png_uint_32 y = 0; y < height; ++y)
+ rows[y] = pixels.get() + (y * scanline);
+
+ png_read_image(png_ptr, rows.get());
+
+ png_read_end(png_ptr, nullptr);
+
+ png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
+
+ fclose(fh);
+
+ Response resp{.width = width, .height = height, .error = 0, .scanline = scanline};
+ return Result{.response = resp, .format = request.format, .pixels = std::move(pixels)};
+}
+// NOLINTEND(misc-include-cleaner)
+#endif
+
+#if HAVE_RSVG
+std::expected<Result, ImageLoadError> load_svg(
+ std::filesystem::path const& path, Request const& request) {
+ GFile* file = g_file_new_for_path(path.c_str());
+ if (!file) {
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ }
+ RsvgHandle* handle =
+ rsvg_handle_new_from_gfile_sync(file, RSVG_HANDLE_FLAGS_NONE, nullptr, nullptr);
+
+ if (!handle) {
+ g_object_unref(file);
+ return std::unexpected(ImageLoadError::kError);
+ }
+ g_object_unref(file);
+
+ // TODO: Get this as part of the request?
+ rsvg_handle_set_dpi(handle, 96.0);
+
+ Size size;
+ {
+ gdouble width_double; // NOLINT(misc-include-cleaner)
+ gdouble height_double; // NOLINT(misc-include-cleaner)
+ if (!rsvg_handle_get_intrinsic_size_in_pixels(handle, &width_double,
+ &height_double)) {
+ g_object_unref(handle);
+ return std::unexpected(ImageLoadError::kError);
+ }
+ size.width = static_cast<uint32_t>(ceil(width_double));
+ size.height = static_cast<uint32_t>(ceil(height_double));
+ }
+
+ if (request.head) {
+ Response resp{.width = size.width, .height = size.height, .error = 0, .scanline = 0};
+ g_object_unref(handle);
+ return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr};
+ }
+
+ if (request.max_width || request.max_height) {
+ size = rescale(size, request.max_width, request.max_height);
+ }
+
+ size_t scanline = static_cast<size_t>(size.width) * 4;
+ auto pixels = std::make_unique<uint8_t[]>(scanline * size.height);
+
+ cairo_surface_t* surface = cairo_image_surface_create_for_data(
+ reinterpret_cast<unsigned char*>(pixels.get()), CAIRO_FORMAT_ARGB32,
+ static_cast<int>(size.width), static_cast<int>(size.height),
+ static_cast<int>(scanline));
+
+ if (!surface) {
+ g_object_unref(handle);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ cairo_t* cr = cairo_create(surface);
+
+ if (request.background) {
+ cairo_rectangle(cr, 0, 0, size.width, size.height);
+ Colour colour{request.background};
+ cairo_set_source_rgba(cr, colour.red() / 255.0, colour.green() / 255.0,
+ colour.blue() / 255.0, colour.alpha() / 255.0);
+ cairo_fill(cr);
+ }
+
+ RsvgRectangle viewport = {
+ .x = 0.0,
+ .y = 0.0,
+ .width = static_cast<double>(size.width),
+ .height = static_cast<double>(size.height),
+ };
+
+ if (!rsvg_handle_render_document(handle, cr, &viewport, nullptr)) {
+ cairo_destroy(cr);
+ cairo_surface_destroy(surface);
+ g_object_unref(handle);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ cairo_destroy(cr);
+ cairo_surface_destroy(surface);
+ g_object_unref(handle);
+
+ // Cairo uses premultiplied pixels, we don't
+ {
+ uint8_t* row = pixels.get();
+ for (uint32_t y = 0; y < size.height; ++y) {
+ uint8_t* pixel = row;
+ for (uint32_t x = 0; x < size.width; ++x, pixel += 4) {
+ if constexpr (std::endian::native == std::endian::big) {
+ if (pixel[0] != 0xff && pixel[0] > 0) {
+ pixel[1] = (static_cast<uint16_t>(pixel[1]) * 255) / pixel[0];
+ pixel[2] = (static_cast<uint16_t>(pixel[2]) * 255) / pixel[0];
+ pixel[3] = (static_cast<uint16_t>(pixel[3]) * 255) / pixel[0];
+ }
+ } else {
+ if (pixel[3] != 0xff && pixel[3] > 0) {
+ pixel[0] = (static_cast<uint16_t>(pixel[0]) * 255) / pixel[3];
+ pixel[1] = (static_cast<uint16_t>(pixel[1]) * 255) / pixel[3];
+ pixel[2] = (static_cast<uint16_t>(pixel[2]) * 255) / pixel[3];
+ }
+ }
+ }
+ row += scanline;
+ }
+ }
+
+ return Result{
+ .response = Response{.width = size.width, .height = size.height, .error = 0, .scanline = scanline},
+ .format = std::endian::native == std::endian::big ? Image::Format::ARGB_8888
+ : Image::Format::BGRA_8888,
+ .pixels = std::move(pixels)
+ };
+}
+#endif
+
+#if HAVE_XPM
+
+std::optional<uint32_t> xpm_parse_color(XpmColor const& color) {
+ if (color.c_color) {
+ auto const len = strlen(color.c_color);
+ if (color.c_color[0] == '#' && len == 7) {
+ uint32_t rgb;
+ if (std::from_chars(color.c_color + 1, color.c_color + 7, rgb, 16).ec == std::errc()) {
+ return 0xff000000 | rgb;
+ }
+ }
+ if (len == 4 && (strcmp(color.c_color, "None") == 0 ||
+ strcmp(color.c_color, "none") == 0))
+ return 0;
+ unsigned short red;
+ unsigned short green;
+ unsigned short blue;
+ if (dixLookupBuiltinColor(0, color.c_color, len,
+ &red, &green, &blue)) {
+ return 0xff000000 | (static_cast<uint32_t>(red & 0xff) << 16) |
+ (green & 0xff00) | (blue & 0xff);
+ }
+ }
+ if (color.m_color) {
+ if (strcmp(color.m_color, "white") == 0) {
+ return 0xffffffff;
+ }
+ if (strcmp(color.m_color, "black") == 0) {
+ return 0xff000000;
+ }
+ }
+ return std::nullopt;
+}
+
+std::expected<Result, ImageLoadError> load_xpm(
+ std::filesystem::path const& path, Request const& request) {
+ XpmImage image;
+ XpmInfo info;
+ auto ret = XpmReadFileToXpmImage(path.c_str(), &image, &info);
+ if (ret != XpmSuccess) {
+ if (ret == XpmOpenFailed)
+ return std::unexpected(ImageLoadError::kNoSuchFile);
+ return std::unexpected(ImageLoadError::kError);
+ }
+
+ if (request.head) {
+ Response resp{.width = image.width, .height = image.height, .error = 0, .scanline = 0};
+ XpmFreeXpmImage(&image);
+ XpmFreeXpmInfo(&info);
+ return Result{.response = resp, .format = Image::Format::RGBA_8888, .pixels = nullptr};
+ }
+
+ size_t scanline = static_cast<size_t>(image.width) * 4;
+ auto pixels = std::make_unique<uint8_t[]>(scanline * image.height);
+
+ auto colors = std::make_unique<uint32_t[]>(image.ncolors);
+ for (unsigned int i = 0; i < image.ncolors; ++i) {
+ auto ret = xpm_parse_color(image.colorTable[i]);
+ if (ret.has_value()) {
+ colors[i] = ret.value();
+ } else {
+ return std::unexpected(ImageLoadError::kUnsupportedFormat);
+ }
+ }
+
+ auto* out_row = pixels.get();
+ auto* in = image.data;
+ for (unsigned int y = 0; y < image.height; ++y) {
+ auto* out_pixel = reinterpret_cast<uint32_t*>(out_row);
+ for (unsigned int x = 0; x < image.width; ++x) {
+ out_pixel[x] = colors[*in++];
+ }
+ out_row += scanline;
+ }
+
+ XpmFreeXpmImage(&image);
+ XpmFreeXpmInfo(&info);
+
+ return Result{
+ .response = Response{.width = image.width, .height = image.height, .error = 0, .scanline = scanline},
+ .format = std::endian::native == std::endian::big ? Image::Format::ARGB_8888 : Image::Format::BGRA_8888,
+ .pixels = std::move(pixels),
+ };
+}
+#endif
+
+} // namespace
+
+namespace image_processor {
+
+std::expected<Size, ImageLoadError> peek(Process& process,
+ std::filesystem::path const& path) {
+ auto ret = write_request(process.writer(), path, true,
+ Image::Format::ARGB_8888, 0, 0, {});
+ if (!ret) {
+ return std::unexpected(ret.error());
+ }
+ auto ret2 = read_response(process.reader());
+ if (!ret2)
+ return std::unexpected(ret2.error());
+ return Size{ret2->width, ret2->height};
+}
+
+std::expected<std::unique_ptr<Image>, ImageLoadError> load(
+ Process& process, std::filesystem::path const& path, Image::Format format,
+ uint32_t max_width, uint32_t max_height, std::optional<Colour> background) {
+ auto ret = write_request(process.writer(), path, false, format, max_width,
+ max_height, background.value_or({}));
+ if (!ret) {
+ return std::unexpected(ret.error());
+ }
+ auto ret2 = read_response(process.reader());
+ if (!ret2)
+ return std::unexpected(ret2.error());
+ size_t size = static_cast<size_t>(ret2->height) * ret2->scanline;
+ auto pixels = std::make_unique<uint8_t[]>(size);
+ auto ret3 = process.reader().repeat_read(pixels.get(), size);
+ if (!ret3.has_value() || ret3.value() != size) {
+ return std::unexpected(ret.error());
+ }
+ return std::make_unique<Image>(format, Size{ret2->width, ret2->height},
+ ret2->scanline, std::move(pixels));
+}
+
+int run(std::unique_ptr<io::Reader> reader,
+ std::unique_ptr<io::Writer> writer) {
+ while (true) {
+ Request request;
+ auto ret = reader->repeat_read(&request, sizeof(request));
+ if (!ret.has_value() || ret.value() != sizeof(request))
+ break;
+ std::string path_str(request.path_len, ' ');
+ ret = reader->repeat_read(path_str.data(), path_str.size());
+ if (!ret.has_value() || ret.value() != path_str.size())
+ break;
+ std::filesystem::path path(std::move(path_str));
+
+ auto extension = path.extension().native();
+ std::ranges::transform(extension, extension.begin(),
+ [](unsigned char c){ return std::tolower(c); });
+ std::expected<Result, ImageLoadError> result{
+ std::unexpected(ImageLoadError::kUnsupportedFormat)};
+#if HAVE_JPEG
+ if (extension == ".jpeg" || extension == ".jpg" || extension == ".jpe" ||
+ extension == ".jfif" || extension == ".jfi" || extension == ".jif") {
+ result = load_jpeg(path, request);
+ }
+#endif
+#if HAVE_PNG
+ if (path.extension() == ".png") {
+ result = load_png(path, request);
+ }
+#endif
+#if HAVE_RSVG
+ if (path.extension() == ".svg" || path.extension() == ".svgz") {
+ result = load_svg(path, request);
+ }
+#endif
+#if HAVE_XPM
+ if (path.extension() == ".xpm") {
+ result = load_xpm(path, request);
+ }
+#endif
+ if (!request.head && result.has_value() &&
+ result->format != request.format) {
+ auto size = static_cast<size_t>(result->response.height) *
+ result->response.scanline;
+ rearrange_bytes(std::span(result->pixels.get(), size),
+ result->response.width, result->response.height,
+ result->response.scanline, result->format,
+ request.format);
+ }
+ if (result.has_value()) {
+ auto ret2 =
+ writer->repeat_write(&result->response, sizeof(result->response));
+ if (!ret2.has_value() || ret2.value() != sizeof(result->response))
+ break;
+ if (!request.head) {
+ auto size = static_cast<size_t>(result->response.height) *
+ result->response.scanline;
+ ret2 = writer->repeat_write(result->pixels.get(), size);
+ if (!ret2.has_value() || ret2.value() != size)
+ break;
+ }
+ } else {
+ Response response{.width = 0, .height = 0, .error = static_cast<uint8_t>(result.error()), .scanline = 0};
+ auto ret2 = writer->repeat_write(&response, sizeof(response));
+ if (!ret2.has_value() || ret2.value() != sizeof(response))
+ break;
+ }
+ }
+ return -1;
+}
+
+} // namespace image_processor