diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2026-01-02 22:42:31 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2026-01-02 22:42:31 +0100 |
| commit | 6ed8f5151719fbc14ec0ac6d28a346d1f74cf2ca (patch) | |
| tree | ebe7588e89e1aa2ae5376acf85f3a3a7b2ec7e10 /src/image_processor.cc | |
Diffstat (limited to 'src/image_processor.cc')
| -rw-r--r-- | src/image_processor.cc | 947 |
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 |
