diff options
Diffstat (limited to 'fiv-io.c')
-rw-r--r-- | fiv-io.c | 2632 |
1 files changed, 1275 insertions, 1357 deletions
@@ -1,7 +1,7 @@ // // fiv-io.c: image operations // -// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name> +// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name> // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. @@ -19,30 +19,32 @@ #include <errno.h> #include <math.h> +#include <setjmp.h> +#include <stdio.h> #include <cairo.h> #include <glib.h> +#include <jpeglib.h> #include <turbojpeg.h> #include <webp/decode.h> #include <webp/demux.h> #include <webp/encode.h> #include <webp/mux.h> - #ifdef HAVE_JPEG_QS -#include <setjmp.h> -#include <stdio.h> - -#include <jpeglib.h> #include <libjpegqs.h> #endif // HAVE_JPEG_QS -// Colour management must be handled before RGB conversions. -#ifdef HAVE_LCMS2 -#include <lcms2.h> -#endif // HAVE_LCMS2 +#define TIFF_TABLES_CONSTANTS_ONLY +#include "tiff-tables.h" +#include "tiffer.h" #ifdef HAVE_LIBRAW #include <libraw.h> +#if LIBRAW_VERSION >= LIBRAW_MAKE_VERSION(0, 21, 0) +#define LIBRAW_OPIONS_NO_MEMERR_CALLBACK 0 +#else +#define rawparams params +#endif #endif // HAVE_LIBRAW #ifdef HAVE_RESVG #include <resvg.h> @@ -59,6 +61,9 @@ #ifdef HAVE_LIBTIFF #include <tiff.h> #include <tiffio.h> +#ifndef TIFF_TMSIZE_T_MAX +#define TIFF_TMSIZE_T_MAX ((tmsize_t) (SIZE_MAX >> 1)) +#endif #endif // HAVE_LIBTIFF #ifdef HAVE_GDKPIXBUF #include <gdk-pixbuf/gdk-pixbuf.h> @@ -77,7 +82,7 @@ #define WUFFS_CONFIG__MODULE__PNG #define WUFFS_CONFIG__MODULE__TGA #define WUFFS_CONFIG__MODULE__ZLIB -#include "wuffs-mirror-release-c/release/c/wuffs-v0.3.c" +#include "submodules/wuffs-mirror-release-c/release/c/wuffs-v0.3.c" #include "fiv-io.h" @@ -118,23 +123,28 @@ const char *fiv_io_supported_media_types[] = { gchar ** fiv_io_all_supported_media_types(void) { + GHashTable *unique = + g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GPtrArray *types = g_ptr_array_new(); for (const char **p = fiv_io_supported_media_types; *p; p++) - g_ptr_array_add(types, g_strdup(*p)); + if (g_hash_table_insert(unique, g_strdup(*p), NULL)) + g_ptr_array_add(types, g_strdup(*p)); #ifdef HAVE_GDKPIXBUF GSList *formats = gdk_pixbuf_get_formats(); for (GSList *iter = formats; iter; iter = iter->next) { gchar **subtypes = gdk_pixbuf_format_get_mime_types(iter->data); for (gchar **p = subtypes; *p; p++) - g_ptr_array_add(types, *p); + if (g_hash_table_insert(unique, *p, NULL)) + g_ptr_array_add(types, g_strdup(*p)); g_free(subtypes); } g_slist_free(formats); #endif // HAVE_GDKPIXBUF + g_hash_table_unref(unique); g_ptr_array_add(types, NULL); - return (char **) g_ptr_array_free(types, FALSE); + return (gchar **) g_ptr_array_free(types, FALSE); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -168,305 +178,117 @@ add_warning(const FivIoOpenContext *ctx, const char *format, ...) va_end(ap); } -static bool -try_append_page(cairo_surface_t *surface, cairo_surface_t **result, - cairo_surface_t **result_tail) -{ - if (!surface) - return false; +// --- Images ------------------------------------------------------------------ - if (*result) { - cairo_surface_set_user_data(*result_tail, &fiv_io_key_page_next, - surface, (cairo_destroy_func_t) cairo_surface_destroy); - cairo_surface_set_user_data( - surface, &fiv_io_key_page_previous, *result_tail, NULL); - *result_tail = surface; - } else { - *result = *result_tail = surface; +FivIoImage * +fiv_io_image_new(cairo_format_t format, uint32_t width, uint32_t height) +{ + // CAIRO_STRIDE_ALIGNMENT is 4 bytes, we only use multiples. + size_t unit = 0; + switch (format) { + case CAIRO_FORMAT_RGB24: + case CAIRO_FORMAT_RGB30: + case CAIRO_FORMAT_ARGB32: + unit = 4; + break; +#if CAIRO_VERSION >= 11702 + case CAIRO_FORMAT_RGB96F: + unit = 12; + break; + case CAIRO_FORMAT_RGBA128F: + unit = 16; + break; +#endif + default: + return NULL; } - return true; -} -// --- Colour management ------------------------------------------------------- + uint8_t *data = g_try_malloc0(unit * width * height); + if (!data) + return NULL; -FivIoProfile -fiv_io_profile_new(const void *data, size_t len) -{ -#ifdef HAVE_LCMS2 - return cmsOpenProfileFromMem(data, len); -#else - (void) data; - (void) len; - return NULL; -#endif + FivIoImage *image = g_rc_box_new0(FivIoImage); + image->data = data; + image->format = format; + image->width = width; + image->stride = width * unit; + image->height = height; + return image; } -FivIoProfile -fiv_io_profile_new_sRGB(void) +FivIoImage * +fiv_io_image_ref(FivIoImage *self) { -#ifdef HAVE_LCMS2 - return cmsCreate_sRGBProfile(); -#else - return NULL; -#endif + return g_rc_box_acquire(self); } -FivIoProfile -fiv_io_profile_new_sRGB_gamma(double gamma) +static void +fiv_io_image_finalize(FivIoImage *image) { -#ifdef HAVE_LCMS2 - // TODO(p): Make sure to use the library in a thread-safe manner. - cmsContext context = NULL; + g_free(image->data); - static const cmsCIExyY D65 = {0.3127, 0.3290, 1.0}; - static const cmsCIExyYTRIPLE primaries = { - {0.6400, 0.3300, 1.0}, {0.3000, 0.6000, 1.0}, {0.1500, 0.0600, 1.0}}; - cmsToneCurve *curve = cmsBuildGamma(context, gamma); - if (!curve) - return NULL; + g_bytes_unref(image->exif); + g_bytes_unref(image->icc); + g_bytes_unref(image->xmp); + g_bytes_unref(image->thum); - cmsHPROFILE profile = cmsCreateRGBProfileTHR( - context, &D65, &primaries, (cmsToneCurve *[3]){curve, curve, curve}); - cmsFreeToneCurve(curve); - return profile; -#else - (void) gamma; - return NULL; -#endif -} + if (image->text) + g_hash_table_unref(image->text); -static FivIoProfile -fiv_io_profile_new_from_bytes(GBytes *bytes) -{ - gsize len = 0; - gconstpointer p = g_bytes_get_data(bytes, &len); - return fiv_io_profile_new(p, len); -} + if (image->render) + image->render->destroy(image->render); -static GBytes * -fiv_io_profile_to_bytes(FivIoProfile profile) -{ -#ifdef HAVE_LCMS2 - cmsUInt32Number len = 0; - (void) cmsSaveProfileToMem(profile, NULL, &len); - gchar *data = g_malloc0(len); - if (!cmsSaveProfileToMem(profile, data, &len)) { - g_free(data); - return NULL; - } - return g_bytes_new_take(data, len); -#else - (void) profile; - return NULL; -#endif + if (image->page_next) + fiv_io_image_unref(image->page_next); + if (image->frame_next) + fiv_io_image_unref(image->frame_next); } void -fiv_io_profile_free(FivIoProfile self) +fiv_io_image_unref(FivIoImage *self) { -#ifdef HAVE_LCMS2 - cmsCloseProfile(self); -#else - (void) self; -#endif + g_rc_box_release_full(self, (GDestroyNotify) fiv_io_image_finalize); } -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F. -#define FIV_IO_LCMS2_ARGB32 \ - (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8) -#define FIV_IO_LCMS2_4X16LE \ - (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE) - -// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with -// ARGB/BGRA/XRGB/BGRX. -static void -trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len) -{ - // This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms. - // It will typically produce horribly oversaturated results. - // Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop - // does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096 - while (len--) { - int c = p[0], m = p[1], y = p[2], k = p[3]; -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - p[0] = k * y / 255; - p[1] = k * m / 255; - p[2] = k * c / 255; - p[3] = 255; -#else - p[3] = k * y / 255; - p[2] = k * m / 255; - p[1] = k * c / 255; - p[0] = 255; -#endif - p += 4; - } -} - -static void -fiv_io_profile_cmyk( - cairo_surface_t *surface, FivIoProfile source, FivIoProfile target) +cairo_surface_t * +fiv_io_image_to_surface_noref(const FivIoImage *image) { - unsigned char *data = cairo_image_surface_get_data(surface); - int w = cairo_image_surface_get_width(surface); - int h = cairo_image_surface_get_height(surface); - -#ifndef HAVE_LCMS2 - (void) source; - (void) target; -#else - cmsHTRANSFORM transform = NULL; - if (source && target) { - transform = cmsCreateTransform(source, TYPE_CMYK_8_REV, target, - FIV_IO_LCMS2_ARGB32, INTENT_PERCEPTUAL, 0); - } - if (transform) { - cmsDoTransform(transform, data, data, w * h); - cmsDeleteTransform(transform); - return; - } -#endif - trivial_cmyk_to_host_byte_order_argb(data, w * h); + return cairo_image_surface_create_for_data( + image->data, image->format, image->width, image->height, image->stride); } -static void -fiv_io_profile_xrgb32_direct(unsigned char *data, int w, int h, - FivIoProfile source, FivIoProfile target) -{ -#ifndef HAVE_LCMS2 - (void) data; - (void) w; - (void) h; - (void) source; - (void) target; -#else - // TODO(p): We should make this optional. - cmsHPROFILE src_fallback = NULL; - if (target && !source) - source = src_fallback = cmsCreate_sRGBProfile(); - - cmsHTRANSFORM transform = NULL; - if (source && target) { - transform = cmsCreateTransform(source, FIV_IO_LCMS2_ARGB32, target, - FIV_IO_LCMS2_ARGB32, INTENT_PERCEPTUAL, 0); - } - if (transform) { - cmsDoTransform(transform, data, data, w * h); - cmsDeleteTransform(transform); - } - if (src_fallback) - cmsCloseProfile(src_fallback); -#endif -} - -static void -fiv_io_profile_xrgb32( - cairo_surface_t *surface, FivIoProfile source, FivIoProfile target) +cairo_surface_t * +fiv_io_image_to_surface(FivIoImage *image) { - unsigned char *data = cairo_image_surface_get_data(surface); - int w = cairo_image_surface_get_width(surface); - int h = cairo_image_surface_get_height(surface); - fiv_io_profile_xrgb32_direct(data, w, h, source, target); -} + // TODO(p): Remove this shortcut eventually. And the function. + if (!image) + return NULL; -static void -fiv_io_profile_4x16le_direct( - unsigned char *data, int w, int h, FivIoProfile source, FivIoProfile target) -{ -#ifndef HAVE_LCMS2 - (void) data; - (void) w; - (void) h; - (void) source; - (void) target; -#else - // TODO(p): We should make this optional. - cmsHPROFILE src_fallback = NULL; - if (target && !source) - source = src_fallback = cmsCreate_sRGBProfile(); - - cmsHTRANSFORM transform = NULL; - if (source && target) { - transform = cmsCreateTransform(source, FIV_IO_LCMS2_4X16LE, target, - FIV_IO_LCMS2_4X16LE, INTENT_PERCEPTUAL, 0); - } - if (transform) { - cmsDoTransform(transform, data, data, w * h); - cmsDeleteTransform(transform); - } - if (src_fallback) - cmsCloseProfile(src_fallback); -#endif + static cairo_user_data_key_t key_image; + cairo_surface_t *surface = cairo_image_surface_create_for_data( + image->data, image->format, image->width, image->height, image->stride); + cairo_surface_set_user_data(surface, &key_image, + image, (cairo_destroy_func_t) fiv_io_image_unref); + return surface; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -fiv_io_profile_xrgb32_page(cairo_surface_t *page, FivIoProfile target) -{ - GBytes *bytes = NULL; - FivIoProfile source = NULL; - if ((bytes = cairo_surface_get_user_data(page, &fiv_io_key_icc))) - source = fiv_io_profile_new_from_bytes(bytes); - - // TODO(p): All animations need to be composited in a linear colour space. - for (cairo_surface_t *frame = page; frame != NULL; - frame = cairo_surface_get_user_data(frame, &fiv_io_key_frame_next)) - fiv_io_profile_xrgb32(frame, source, target); - - if (source) - fiv_io_profile_free(source); -} - -// TODO(p): Offer better integration, upgrade the bit depth if appropriate. -static cairo_surface_t * -fiv_io_profile_finalize(cairo_surface_t *image, FivIoProfile target) -{ - if (!image || !target) - return image; - - for (cairo_surface_t *page = image; page != NULL; - page = cairo_surface_get_user_data(page, &fiv_io_key_page_next)) { - // TODO(p): 1. un/premultiply ARGB, 2. do colour management - // early enough, so that no avoidable increase of quantization error - // occurs beforehands, and also for correct alpha compositing. - // FIXME: This assumes that if the first frame is opaque, they all are. - if (cairo_image_surface_get_format(page) == CAIRO_FORMAT_RGB24) - fiv_io_profile_xrgb32_page(page, target); - } - return image; -} - -static void -fiv_io_premultiply_argb32(cairo_surface_t *surface) +static bool +try_append_page( + FivIoImage *image, FivIoImage **result, FivIoImage **result_tail) { - int w = cairo_image_surface_get_width(surface); - int h = cairo_image_surface_get_height(surface); - unsigned char *data = cairo_image_surface_get_data(surface); - int stride = cairo_image_surface_get_stride(surface); - if (cairo_image_surface_get_format(surface) != CAIRO_FORMAT_ARGB32) - return; + if (!image) + return false; - for (int y = 0; y < h; y++) { - uint32_t *dstp = (uint32_t *) (data + stride * y); - for (int x = 0; x < w; x++) { - uint32_t argb = dstp[x], a = argb >> 24; - dstp[x] = a << 24 | - PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 | - PREMULTIPLY8(a, 0xFF & (argb >> 8)) << 8 | - PREMULTIPLY8(a, 0xFF & argb); - } + if (*result) { + (*result_tail)->page_next = image; + image->page_previous = *result_tail; + *result_tail = image; + } else { + *result = *result_tail = image; } -} - -static void -fiv_io_premultiply_argb32_page(cairo_surface_t *page) -{ - for (cairo_surface_t *frame = page; frame != NULL; - frame = cairo_surface_get_user_data(frame, &fiv_io_key_frame_next)) - fiv_io_premultiply_argb32(frame); + return true; } // --- Wuffs ------------------------------------------------------------------- @@ -562,11 +384,12 @@ struct load_wuffs_frame_context { GBytes *meta_iccp; ///< Reference-counted ICC profile GBytes *meta_xmp; ///< Reference-counted XMP - FivIoProfile target; ///< Target device profile, if any - FivIoProfile source; ///< Source colour profile, if any + FivIoCmm *cmm; ///< CMM context, if any + FivIoProfile *target; ///< Target device profile, if any + FivIoProfile *source; ///< Source colour profile, if any - cairo_surface_t *result; ///< The resulting surface (referenced) - cairo_surface_t *result_tail; ///< The final animation frame + FivIoImage *result; ///< The resulting image (referenced) + FivIoImage *result_tail; ///< The final animation frame }; static bool @@ -593,66 +416,58 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error) decode_format = CAIRO_FORMAT_ARGB32; unsigned char *targetbuf = NULL; - cairo_surface_t *surface = - cairo_image_surface_create(decode_format, ctx->width, ctx->height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); + FivIoImage *image = + fiv_io_image_new(decode_format, ctx->width, ctx->height); + if (!image) { + set_error(error, "image allocation failure"); goto fail; } - // CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with - // ARGB/BGR/XRGB/BGRX. This function does not support a stride different - // from the width, maybe Wuffs internals do not either. - unsigned char *surface_data = cairo_image_surface_get_data(surface); - int surface_stride = cairo_image_surface_get_stride(surface); + // There is no padding with ARGB/BGR/XRGB/BGRX. + // This function does not support a stride different from the width, + // maybe Wuffs internals do not either. wuffs_base__pixel_buffer pb = {0}; if (ctx->expand_16_float || ctx->pack_16_10) { - uint32_t targetbuf_size = ctx->height * ctx->width * 8; + uint32_t targetbuf_size = image->height * image->width * 8; targetbuf = g_malloc(targetbuf_size); status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, wuffs_base__make_slice_u8(targetbuf, targetbuf_size)); } else { status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, - wuffs_base__make_slice_u8(surface_data, - surface_stride * cairo_image_surface_get_height(surface))); + wuffs_base__make_slice_u8( + image->data, image->stride * image->height)); } if (!wuffs_base__status__is_ok(&status)) { set_error(error, wuffs_base__status__message(&status)); goto fail; } - // Starting to modify pixel data directly. Probably an unnecessary call. - cairo_surface_flush(surface); - status = wuffs_base__image_decoder__decode_frame(ctx->dec, &pb, ctx->src, WUFFS_BASE__PIXEL_BLEND__SRC, ctx->workbuf, NULL); if (!wuffs_base__status__is_ok(&status)) { set_error(error, wuffs_base__status__message(&status)); - // The PNG decoder, at minimum, will flush any pixel data, so use them. - if (status.repr != wuffs_base__suspension__short_read) - goto fail; + // The PNG decoder, at minimum, will flush any pixel data upon + // finding out that the input is truncated, so accept whatever we get. } if (ctx->target) { if (ctx->expand_16_float || ctx->pack_16_10) { - fiv_io_profile_4x16le_direct( + fiv_io_cmm_4x16le_direct(ctx->cmm, targetbuf, ctx->width, ctx->height, ctx->source, ctx->target); // The first one premultiplies below, the second doesn't need to. } else { - fiv_io_profile_xrgb32_direct(surface_data, ctx->width, ctx->height, - ctx->source, ctx->target); - fiv_io_premultiply_argb32(surface); + fiv_io_cmm_argb32_premultiply( + ctx->cmm, image, ctx->source, ctx->target); } } if (ctx->expand_16_float) { g_debug("Wuffs to Cairo RGBA128F"); uint16_t *in = (uint16_t *) targetbuf; - float *out = (float *) surface_data; - for (uint32_t y = 0; y < ctx->height; y++) { - for (uint32_t x = 0; x < ctx->width; x++) { + float *out = (float *) image->data; + for (uint32_t y = 0; y < image->height; y++) { + for (uint32_t x = 0; x < image->width; x++) { float b = *in++ / 65535., g = *in++ / 65535., r = *in++ / 65535., a = *in++ / 65535.; *out++ = r * a; @@ -664,9 +479,9 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error) } else if (ctx->pack_16_10) { g_debug("Wuffs to Cairo RGB30"); uint16_t *in = (uint16_t *) targetbuf; - uint32_t *out = (uint32_t *) surface_data; - for (uint32_t y = 0; y < ctx->height; y++) { - for (uint32_t x = 0; x < ctx->width; x++) { + uint32_t *out = (uint32_t *) image->data; + for (uint32_t y = 0; y < image->height; y++) { + for (uint32_t x = 0; x < image->width; x++) { uint32_t b = *in++, g = *in++, r = *in++, X = *in++; *out++ = (X >> 14) << 30 | (r >> 6) << 20 | (g >> 6) << 10 | (b >> 6); @@ -674,19 +489,17 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error) } } - // Pixel data has been written, need to let Cairo know. - cairo_surface_mark_dirty(surface); - // Single-frame images get a fast path, animations are are handled slowly: if (wuffs_base__frame_config__index(&fc) > 0) { - // Copy the previous frame to a new surface. - cairo_surface_t *canvas = cairo_image_surface_create( - ctx->cairo_format, ctx->width, ctx->height); - int stride = cairo_image_surface_get_stride(canvas); - int height = cairo_image_surface_get_height(canvas); - memcpy(cairo_image_surface_get_data(canvas), - cairo_image_surface_get_data(ctx->result_tail), stride * height); - cairo_surface_mark_dirty(canvas); + // Copy the previous frame to a new image. + FivIoImage *prev = ctx->result_tail, *canvas = fiv_io_image_new( + prev->format, prev->width, prev->height); + if (!canvas) { + set_error(error, "image allocation failure"); + goto fail; + } + + memcpy(canvas->data, prev->data, prev->stride * prev->height); // Apply that frame's disposal method. // XXX: We do not expect opaque pictures to receive holes this way. @@ -703,7 +516,9 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error) b = (uint8_t) (bg) / 255. / a; } - cairo_t *cr = cairo_create(canvas); + cairo_surface_t *surface = fiv_io_image_to_surface_noref(canvas); + cairo_t *cr = cairo_create(surface); + cairo_surface_destroy(surface); switch (wuffs_base__frame_config__disposal(&ctx->last_fc)) { case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_BACKGROUND: cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y, @@ -733,46 +548,41 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error) ? CAIRO_OPERATOR_SOURCE : CAIRO_OPERATOR_OVER); + surface = fiv_io_image_to_surface_noref(image); cairo_set_source_surface(cr, surface, 0, 0); + cairo_surface_destroy(surface); cairo_paint(cr); cairo_destroy(cr); - cairo_surface_destroy(surface); - surface = canvas; + + fiv_io_image_unref(image); + image = canvas; } if (ctx->meta_exif) - cairo_surface_set_user_data(surface, &fiv_io_key_exif, - g_bytes_ref(ctx->meta_exif), (cairo_destroy_func_t) g_bytes_unref); + image->exif = g_bytes_ref(ctx->meta_exif); if (ctx->meta_iccp) - cairo_surface_set_user_data(surface, &fiv_io_key_icc, - g_bytes_ref(ctx->meta_iccp), (cairo_destroy_func_t) g_bytes_unref); + image->icc = g_bytes_ref(ctx->meta_iccp); if (ctx->meta_xmp) - cairo_surface_set_user_data(surface, &fiv_io_key_xmp, - g_bytes_ref(ctx->meta_xmp), (cairo_destroy_func_t) g_bytes_unref); + image->xmp = g_bytes_ref(ctx->meta_xmp); - cairo_surface_set_user_data(surface, &fiv_io_key_loops, - (void *) (uintptr_t) wuffs_base__image_decoder__num_animation_loops( - ctx->dec), NULL); - cairo_surface_set_user_data(surface, &fiv_io_key_frame_duration, - (void *) (intptr_t) (wuffs_base__frame_config__duration(&fc) / - WUFFS_BASE__FLICKS_PER_MILLISECOND), NULL); + image->loops = wuffs_base__image_decoder__num_animation_loops(ctx->dec); + image->frame_duration = wuffs_base__frame_config__duration(&fc) / + WUFFS_BASE__FLICKS_PER_MILLISECOND; - cairo_surface_set_user_data( - surface, &fiv_io_key_frame_previous, ctx->result_tail, NULL); + image->frame_previous = ctx->result_tail; if (ctx->result_tail) - cairo_surface_set_user_data(ctx->result_tail, &fiv_io_key_frame_next, - surface, (cairo_destroy_func_t) cairo_surface_destroy); + ctx->result_tail->frame_next = image; else - ctx->result = surface; + ctx->result = image; - ctx->result_tail = surface; + ctx->result_tail = image; ctx->last_fc = fc; g_free(targetbuf); return wuffs_base__status__is_ok(&status); fail: - cairo_surface_destroy(surface); - g_clear_pointer(&ctx->result, cairo_surface_destroy); + g_clear_pointer(&image, fiv_io_image_unref); + g_clear_pointer(&ctx->result, fiv_io_image_unref); ctx->result_tail = NULL; g_free(targetbuf); return false; @@ -781,12 +591,13 @@ fail: // https://github.com/google/wuffs/blob/main/example/gifplayer/gifplayer.c // is pure C, and a good reference. I can't use the auxiliary libraries, // since they depend on C++, which is undesirable. -static cairo_surface_t * +static FivIoImage * open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src, const FivIoOpenContext *ioctx, GError **error) { struct load_wuffs_frame_context ctx = { - .dec = dec, .src = &src, .target = ioctx->screen_profile}; + .dec = dec, .src = &src, + .cmm = ioctx->cmm, .target = ioctx->screen_profile}; // TODO(p): PNG text chunks, like we do with PNG thumbnails. // TODO(p): See if something could and should be done about @@ -870,9 +681,11 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src, // TODO(p): Improve our simplistic PNG handling of: gAMA, cHRM, sRGB. if (ctx.target) { if (ctx.meta_iccp) - ctx.source = fiv_io_profile_new_from_bytes(ctx.meta_iccp); + ctx.source = fiv_io_cmm_get_profile_from_bytes( + ctx.cmm, ctx.meta_iccp); else if (isfinite(gamma) && gamma > 0) - ctx.source = fiv_io_profile_new_sRGB_gamma(gamma); + ctx.source = fiv_io_cmm_get_profile_sRGB_gamma( + ctx.cmm, gamma); } // Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine. @@ -943,8 +756,7 @@ open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src, // Wrap the chain around, since our caller receives only one pointer. if (ctx.result) - cairo_surface_set_user_data( - ctx.result, &fiv_io_key_frame_previous, ctx.result_tail, NULL); + ctx.result->frame_previous = ctx.result_tail; fail: free(ctx.workbuf.ptr); @@ -955,7 +767,7 @@ fail: return ctx.result; } -static cairo_surface_t * +static FivIoImage * open_wuffs_using(wuffs_base__image_decoder *(*allocate)(), const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) { @@ -965,11 +777,11 @@ open_wuffs_using(wuffs_base__image_decoder *(*allocate)(), return NULL; } - cairo_surface_t *surface = + FivIoImage *image = open_wuffs(dec, wuffs_base__ptr_u8__reader((uint8_t *) data, len, TRUE), ctx, error); free(dec); - return surface; + return image; } // --- Wuffs for PNG thumbnails ------------------------------------------------ @@ -1003,7 +815,7 @@ pull_metadata_kvp(wuffs_png__decoder *dec, wuffs_base__io_buffer *src, } // An uncomplicated variant of fiv_io_open(), might be up for refactoring. -cairo_surface_t * +FivIoImage * fiv_io_open_png_thumbnail(const char *path, GError **error) { wuffs_png__decoder dec = {}; @@ -1026,7 +838,7 @@ fiv_io_open_png_thumbnail(const char *path, GError **error) wuffs_base__image_config cfg = {}; wuffs_base__slice_u8 workbuf = {}; - cairo_surface_t *surface = NULL; + FivIoImage *image = NULL; bool success = false; GHashTable *texts = @@ -1068,23 +880,19 @@ fiv_io_open_png_thumbnail(const char *path, GError **error) } } - surface = cairo_image_surface_create( + image = fiv_io_image_new( wuffs_base__image_config__first_frame_is_opaque(&cfg) ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, width, height); - - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); + if (!image) { + set_error(error, "image allocation failure"); goto fail; } wuffs_base__pixel_buffer pb = {}; status = wuffs_base__pixel_buffer__set_from_slice(&pb, &cfg.pixcfg, - wuffs_base__make_slice_u8(cairo_image_surface_get_data(surface), - cairo_image_surface_get_stride(surface) * - cairo_image_surface_get_height(surface))); + wuffs_base__make_slice_u8(image->data, image->stride * image->height)); if (!wuffs_base__status__is_ok(&status)) { set_error(error, wuffs_base__status__message(&status)); goto fail; @@ -1117,49 +925,220 @@ fiv_io_open_png_thumbnail(const char *path, GError **error) g_assert(key == NULL); - cairo_surface_mark_dirty(surface); - cairo_surface_set_user_data(surface, &fiv_io_key_text, - g_hash_table_ref(texts), (cairo_destroy_func_t) g_hash_table_unref); + image->text = g_hash_table_ref(texts); success = true; fail: if (!success) - g_clear_pointer(&surface, cairo_surface_destroy); + g_clear_pointer(&image, fiv_io_image_unref); free(workbuf.ptr); g_free(data); g_hash_table_unref(texts); - return surface; + return image; +} + +// --- Multi-Picture Format ---------------------------------------------------- + +static uint32_t +parse_mpf_mpentry(const uint8_t *p, const struct tiffer *T) +{ + uint32_t attrs = T->un->u32(p); + uint32_t offset = T->un->u32(p + 8); + + enum { + TypeBaselineMPPrimaryImage = 0x030000, + TypeLargeThumbnailVGA = 0x010001, + TypeLargeThumbnailFullHD = 0x010002, + TypeMultiFrameImagePanorama = 0x020001, + TypeMultiFrameImageDisparity = 0x020002, + TypeMultiFrameImageMultiAngle = 0x020003, + TypeUndefined = 0x000000, + }; + switch (attrs & 0xFFFFFF) { + case TypeLargeThumbnailVGA: + case TypeLargeThumbnailFullHD: + // Wasted cycles. + case TypeUndefined: + // Apple uses this for HDR and depth maps (same and lower resolution). + // TODO(p): It would be nice to be able to view them. + return 0; + } + + // Don't report non-JPEGs, even though they're unlikely. + if (((attrs >> 24) & 0x7) != 0) + return 0; + + return offset; +} + +static uint32_t * +parse_mpf_index_entries(const struct tiffer *T, struct tiffer_entry *entry) +{ + uint32_t count = entry->remaining_count / 16; + uint32_t *offsets = g_malloc0_n(sizeof *offsets, count + 1), *out = offsets; + for (uint32_t i = 0; i < count; i++) { + // 5.2.3.3.3. Individual Image Data Offset + uint32_t offset = parse_mpf_mpentry(entry->p + i * 16, T); + if (offset) + *out++ = offset; + } + return offsets; +} + +static uint32_t * +parse_mpf_index_ifd(struct tiffer *T) +{ + struct tiffer_entry entry = {}; + while (tiffer_next_entry(T, &entry)) { + // 5.2.3.3. MP Entry + if (entry.tag == MPF_MPEntry && entry.type == TIFFER_UNDEFINED && + !(entry.remaining_count % 16)) { + return parse_mpf_index_entries(T, &entry); + } + } + return NULL; +} + +static bool +parse_mpf( + GPtrArray *individuals, const uint8_t *mpf, size_t len, size_t total_len) +{ + struct tiffer T; + if (!tiffer_init(&T, mpf, len) || !tiffer_next_ifd(&T)) + return false; + + // First image: IFD0 is Index IFD, any IFD1 is Attribute IFD. + // Other images: IFD0 is Attribute IFD, there is no Index IFD. + uint32_t *offsets = parse_mpf_index_ifd(&T); + if (offsets) { + for (const uint32_t *o = offsets; *o; o++) + if (*o <= total_len) + g_ptr_array_add(individuals, (gpointer) mpf + *o); + free(offsets); + } + return true; } // --- JPEG -------------------------------------------------------------------- -static GBytes * -parse_jpeg_metadata(cairo_surface_t *surface, const char *data, gsize len) +struct exif_profile { + double whitepoint[2]; ///< TIFF_WhitePoint + double primaries[6]; ///< TIFF_PrimaryChromaticities + enum Exif_ColorSpace colorspace; ///< Exif_ColorSpace + double gamma; ///< Exif_Gamma + + bool have_whitepoint; + bool have_primaries; + bool have_colorspace; + bool have_gamma; +}; + +static bool +parse_exif_profile_reals( + const struct tiffer *T, struct tiffer_entry *entry, double *out) +{ + while (tiffer_real(T, entry, out++)) + if (!tiffer_next_value(entry)) + return false; + return true; +} + +static void +parse_exif_profile_subifd( + struct exif_profile *params, const struct tiffer *T, uint32_t offset) +{ + struct tiffer subT = {}; + if (!tiffer_subifd(T, offset, &subT)) + return; + + struct tiffer_entry entry = {}; + while (tiffer_next_entry(&subT, &entry)) { + int64_t value = 0; + if (G_UNLIKELY(entry.tag == Exif_ColorSpace) && + entry.type == TIFFER_SHORT && entry.remaining_count == 1 && + tiffer_integer(&subT, &entry, &value)) { + params->have_colorspace = true; + params->colorspace = value; + } else if (G_UNLIKELY(entry.tag == Exif_Gamma) && + entry.type == TIFFER_RATIONAL && entry.remaining_count == 1 && + tiffer_real(&subT, &entry, ¶ms->gamma)) { + params->have_gamma = true; + } + } +} + +static FivIoProfile * +parse_exif_profile(FivIoCmm *cmm, const void *data, size_t len) +{ + struct tiffer T = {}; + if (!tiffer_init(&T, (const uint8_t *) data, len) || !tiffer_next_ifd(&T)) + return NULL; + + struct exif_profile params = {}; + struct tiffer_entry entry = {}; + while (tiffer_next_entry(&T, &entry)) { + int64_t offset = 0; + if (G_UNLIKELY(entry.tag == TIFF_ExifIFDPointer) && + entry.type == TIFFER_LONG && entry.remaining_count == 1 && + tiffer_integer(&T, &entry, &offset) && + offset >= 0 && offset <= UINT32_MAX) { + parse_exif_profile_subifd(¶ms, &T, offset); + } else if (G_UNLIKELY(entry.tag == TIFF_WhitePoint) && + entry.type == TIFFER_RATIONAL && + entry.remaining_count == G_N_ELEMENTS(params.whitepoint)) { + params.have_whitepoint = + parse_exif_profile_reals(&T, &entry, params.whitepoint); + } else if (G_UNLIKELY(entry.tag == TIFF_PrimaryChromaticities) && + entry.type == TIFFER_RATIONAL && + entry.remaining_count == G_N_ELEMENTS(params.primaries)) { + params.have_primaries = + parse_exif_profile_reals(&T, &entry, params.primaries); + } + } + if (!params.have_colorspace) + return NULL; + + // If sRGB is claimed, assume all parameters are standard. + if (params.colorspace == Exif_ColorSpace_sRGB) + return fiv_io_cmm_get_profile_sRGB(cmm); + + // AdobeRGB Nikon JPEGs provide all of these. + if (params.colorspace != Exif_ColorSpace_Uncalibrated || + !params.have_gamma || + !params.have_whitepoint || + !params.have_primaries) + return NULL; + + return fiv_io_cmm_get_profile_parametric(cmm, + params.gamma, params.whitepoint, params.primaries); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct jpeg_metadata { + GByteArray *exif; ///< Exif buffer or NULL + GByteArray *icc; ///< ICC profile buffer or NULL + GPtrArray *mpf; ///< Multi-Picture Format or NULL + int width; ///< Image width + int height; ///< Image height +}; + +static void +parse_jpeg_metadata(const char *data, size_t len, struct jpeg_metadata *meta) { // Because the JPEG file format is simple, just do it manually. // See: https://www.w3.org/Graphics/JPEG/itu-t81.pdf enum { - APP0 = 0xE0, - APP1, - APP2, - RST0 = 0xD0, - RST1, - RST2, - RST3, - RST4, - RST5, - RST6, - RST7, - SOI = 0xD8, - EOI = 0xD9, - SOS = 0xDA, TEM = 0x01, + SOF0 = 0xC0, SOF1, SOF2, SOF3, DHT, SOF5, SOF6, SOF7, + JPG, SOF9, SOF10, SOF11, DAC, SOF13, SOF14, SOF15, + RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7, + SOI, EOI, SOS, DQT, DNL, DRI, DHP, EXP, + APP0, APP1, APP2, APP3, APP4, APP5, APP6, APP7, }; - GByteArray *exif = g_byte_array_new(), *icc = g_byte_array_new(); int icc_sequence = 0, icc_done = FALSE; - const guint8 *p = (const guint8 *) data, *end = p + len; while (p + 3 < end && *p++ == 0xFF && *p != SOS && *p != EOI) { // The previous byte is a fill byte, restart. @@ -1188,155 +1167,127 @@ parse_jpeg_metadata(cairo_surface_t *surface, const char *data, gsize len) if (G_UNLIKELY((p += length) > end)) break; + switch (marker) { + case SOF0: + case SOF1: + case SOF2: + case SOF3: + case SOF5: + case SOF6: + case SOF7: + case SOF9: + case SOF10: + case SOF11: + case SOF13: + case SOF14: + case SOF15: + if (length >= 5) { + meta->width = (payload[3] << 8) + payload[4]; + meta->height = (payload[1] << 8) + payload[2]; + } + } + // https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf 4.7.2 // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 // Not checking the padding byte is intentional. - if (marker == APP1 && p - payload >= 6 && - !memcmp(payload, "Exif\0", 5) && !exif->len) { + // XXX: Thumbnails may in practice overflow into follow-up segments. + if (meta->exif && marker == APP1 && p - payload >= 6 && + !memcmp(payload, "Exif\0", 5) && !meta->exif->len) { payload += 6; - g_byte_array_append(exif, payload, p - payload); + g_byte_array_append(meta->exif, payload, p - payload); } // https://www.color.org/specification/ICC1v43_2010-12.pdf B.4 - if (marker == APP2 && p - payload >= 14 && + if (meta->icc && marker == APP2 && p - payload >= 14 && !memcmp(payload, "ICC_PROFILE\0", 12) && !icc_done && payload[12] == ++icc_sequence && payload[13] >= payload[12]) { payload += 14; - g_byte_array_append(icc, payload, p - payload); + g_byte_array_append(meta->icc, payload, p - payload); icc_done = payload[-1] == icc_sequence; } + // CIPA DC-007-2021 (Multi-Picture Format) 5.2 + // https://www.cipa.jp/e/std/std-sec.html + if (meta->mpf && marker == APP2 && p - payload >= 8 && + !memcmp(payload, "MPF\0", 4) && !meta->mpf->len) { + payload += 4; + parse_mpf(meta->mpf, payload, p - payload, end - payload); + } + // TODO(p): Extract the main XMP segment. } - if (exif->len) - cairo_surface_set_user_data(surface, &fiv_io_key_exif, - g_byte_array_free_to_bytes(exif), - (cairo_destroy_func_t) g_bytes_unref); + if (meta->icc && !icc_done) + g_byte_array_set_size(meta->icc, 0); +} + +static FivIoImage *open_libjpeg_turbo( + const char *data, gsize len, const FivIoOpenContext *ctx, GError **error); + +static void +load_jpeg_finalize(FivIoImage *image, bool cmyk, + const FivIoOpenContext *ctx, const char *data, size_t len) +{ + struct jpeg_metadata meta = { + .exif = g_byte_array_new(), + .icc = g_byte_array_new(), + .mpf = g_ptr_array_new(), + }; + + parse_jpeg_metadata(data, len, &meta); + + if (!ctx->first_frame_only) { + // XXX: This is ugly, as it relies on just the first individual image + // having any follow-up entries (as it should be). + FivIoImage *image_tail = image; + for (guint i = 0; i < meta.mpf->len; i++) { + const char *jpeg = meta.mpf->pdata[i]; + GError *error = NULL; + if (!try_append_page( + open_libjpeg_turbo(jpeg, len - (jpeg - data), ctx, &error), + &image, &image_tail)) { + add_warning(ctx, "MPF image %d: %s", i + 2, error->message); + g_error_free(error); + } + } + } + g_ptr_array_free(meta.mpf, TRUE); + + if (meta.exif->len) + image->exif = g_byte_array_free_to_bytes(meta.exif); else - g_byte_array_free(exif, TRUE); + g_byte_array_free(meta.exif, TRUE); GBytes *icc_profile = NULL; - if (icc_done) - cairo_surface_set_user_data(surface, &fiv_io_key_icc, - (icc_profile = g_byte_array_free_to_bytes(icc)), - (cairo_destroy_func_t) g_bytes_unref); + if (meta.icc->len) + image->icc = icc_profile = g_byte_array_free_to_bytes(meta.icc); else - g_byte_array_free(icc, TRUE); - return icc_profile; -} + g_byte_array_free(meta.icc, TRUE); -static void -load_jpeg_finalize(cairo_surface_t *surface, bool cmyk, - FivIoProfile destination, const char *data, size_t len) -{ - GBytes *icc_profile = parse_jpeg_metadata(surface, data, len); - FivIoProfile source = NULL; - if (icc_profile) - source = fiv_io_profile_new( + FivIoProfile *source = NULL; + if (icc_profile && ctx->cmm) + source = fiv_io_cmm_get_profile(ctx->cmm, g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile)); + else if (image->exif && ctx->cmm) + source = parse_exif_profile(ctx->cmm, + g_bytes_get_data(image->exif, NULL), g_bytes_get_size(image->exif)); if (cmyk) - fiv_io_profile_cmyk(surface, source, destination); + fiv_io_cmm_cmyk(ctx->cmm, image, source, ctx->screen_profile); else - fiv_io_profile_xrgb32(surface, source, destination); + fiv_io_cmm_any(ctx->cmm, image, source, ctx->screen_profile); if (source) fiv_io_profile_free(source); - - // Pixel data has been written, need to let Cairo know. - cairo_surface_mark_dirty(surface); -} - -static cairo_surface_t * -open_libjpeg_turbo( - const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) -{ - // Note that there doesn't seem to be much of a point in using this - // simplified API anymore, because JPEG-QS needs the original libjpeg API. - // It's just more or less duplicated code which won't compile with - // the slow version of the library. - tjhandle dec = tjInitDecompress(); - if (!dec) { - set_error(error, tjGetErrorStr2(dec)); - return NULL; - } - - int width = 0, height = 0, subsampling = TJSAMP_444, colorspace = TJCS_RGB; - if (tjDecompressHeader3(dec, (const unsigned char *) data, len, - &width, &height, &subsampling, &colorspace)) { - set_error(error, tjGetErrorStr2(dec)); - tjDestroy(dec); - return NULL; - } - - bool use_cmyk = colorspace == TJCS_CMYK || colorspace == TJCS_YCCK; - int pixel_format = use_cmyk - ? TJPF_CMYK - : (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRX : TJPF_XRGB); - - // The limit of Cairo/pixman is 32767. but JPEG can go as high as 65535. - // Prevent Cairo from throwing an error, and make use of libjpeg's scaling. - // gdk-pixbuf circumvents this check, producing unrenderable surfaces. - const int max = 32767; - - int nfs = 0; - tjscalingfactor *fs = tjGetScalingFactors(&nfs), f = {0, 1}; - if (fs && (width > max || height > max)) { - for (int i = 0; i < nfs; i++) { - if (TJSCALED(width, fs[i]) <= max && - TJSCALED(height, fs[i]) <= max && - fs[i].num * f.denom > f.num * fs[i].denom) - f = fs[i]; - } - - add_warning(ctx, - "the image is too large, and had to be scaled by %d/%d", - f.num, f.denom); - width = TJSCALED(width, f); - height = TJSCALED(height, f); - } - - cairo_surface_t *surface = - cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); - tjDestroy(dec); - return NULL; - } - - // Starting to modify pixel data directly. Probably an unnecessary call. - cairo_surface_flush(surface); - - int stride = cairo_image_surface_get_stride(surface); - if (tjDecompress2(dec, (const unsigned char *) data, len, - cairo_image_surface_get_data(surface), width, stride, height, - pixel_format, TJFLAG_ACCURATEDCT)) { - if (tjGetErrorCode(dec) == TJERR_WARNING) { - add_warning(ctx, "%s", tjGetErrorStr2(dec)); - } else { - set_error(error, tjGetErrorStr2(dec)); - cairo_surface_destroy(surface); - tjDestroy(dec); - return NULL; - } - } - - load_jpeg_finalize(surface, use_cmyk, ctx->screen_profile, data, len); - tjDestroy(dec); - return surface; } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -#ifdef HAVE_JPEG_QS - struct libjpeg_error_mgr { struct jpeg_error_mgr pub; jmp_buf buf; GError **error; + const FivIoOpenContext *ctx; }; static void @@ -1349,17 +1300,27 @@ libjpeg_error_exit(j_common_ptr cinfo) longjmp(err->buf, 1); } -static cairo_surface_t * -open_libjpeg_enhanced( - const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) +static void +libjpeg_output_message(j_common_ptr cinfo) +{ + struct libjpeg_error_mgr *err = (struct libjpeg_error_mgr *) cinfo->err; + char buf[JMSG_LENGTH_MAX] = ""; + (*cinfo->err->format_message)(cinfo, buf); + add_warning(err->ctx, "%s", buf); +} + +static FivIoImage * +load_libjpeg_turbo(const char *data, gsize len, const FivIoOpenContext *ctx, + void (*loop)(struct jpeg_decompress_struct *, JSAMPARRAY), GError **error) { - cairo_surface_t *volatile surface = NULL; + FivIoImage *volatile image = NULL; - struct libjpeg_error_mgr jerr = {.error = error}; + struct libjpeg_error_mgr jerr = {.error = error, .ctx = ctx}; struct jpeg_decompress_struct cinfo = {.err = jpeg_std_error(&jerr.pub)}; jerr.pub.error_exit = libjpeg_error_exit; + jerr.pub.output_message = libjpeg_output_message; if (setjmp(jerr.buf)) { - g_clear_pointer(&surface, cairo_surface_destroy); + g_clear_pointer(&image, fiv_io_image_unref); jpeg_destroy_decompress(&cinfo); return NULL; } @@ -1367,6 +1328,8 @@ open_libjpeg_enhanced( jpeg_create_decompress(&cinfo); jpeg_mem_src(&cinfo, (const unsigned char *) data, len); (void) jpeg_read_header(&cinfo, true); + // TODO(p): With newer libjpeg-turbo, if cinfo.data_precision is 12 or 16, + // try to load it with higher precision. bool use_cmyk = cinfo.jpeg_color_space == JCS_CMYK || cinfo.jpeg_color_space == JCS_YCCK; @@ -1381,45 +1344,98 @@ open_libjpeg_enhanced( int width = cinfo.output_width; int height = cinfo.output_height; - surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); + // The limit of Cairo/pixman is 32767. but JPEG can go as high as 65535. + // Prevent Cairo from throwing an error, and make use of libjpeg's scaling. + // gdk-pixbuf circumvents this check, producing unrenderable surfaces. + const int max = 32767; + + int nfs = 0; + tjscalingfactor *fs = tjGetScalingFactors(&nfs), f = {0, 1}; + if (fs && (width > max || height > max)) { + for (int i = 0; i < nfs; i++) { + if (TJSCALED(width, fs[i]) <= max && + TJSCALED(height, fs[i]) <= max && + fs[i].num * f.denom > f.num * fs[i].denom) + f = fs[i]; + } + + add_warning(ctx, + "the image is too large, and had to be scaled by %d/%d", + f.num, f.denom); + width = TJSCALED(width, f); + height = TJSCALED(height, f); + cinfo.scale_num = f.num; + cinfo.scale_denom = f.denom; + } + + image = fiv_io_image_new(CAIRO_FORMAT_RGB24, width, height); + if (!image) { + set_error(error, "image allocation failure"); longjmp(jerr.buf, 1); } - unsigned char *surface_data = cairo_image_surface_get_data(surface); - int surface_stride = cairo_image_surface_get_stride(surface); JSAMPARRAY lines = (*cinfo.mem->alloc_small)( (j_common_ptr) &cinfo, JPOOL_IMAGE, sizeof *lines * height); for (int i = 0; i < height; i++) - lines[i] = surface_data + i * surface_stride; + lines[i] = image->data + i * image->stride; + + // Slightly unfortunate generalization. + loop(&cinfo, lines); + + load_jpeg_finalize(image, use_cmyk, ctx, data, len); + jpeg_destroy_decompress(&cinfo); + return image; +} + +static void +load_libjpeg_simple( + struct jpeg_decompress_struct *cinfo, JSAMPARRAY lines) +{ + (void) jpeg_start_decompress(cinfo); + while (cinfo->output_scanline < cinfo->output_height) + (void) jpeg_read_scanlines(cinfo, lines + cinfo->output_scanline, + cinfo->output_height - cinfo->output_scanline); + (void) jpeg_finish_decompress(cinfo); +} +#ifdef HAVE_JPEG_QS + +static void +load_libjpeg_enhanced( + struct jpeg_decompress_struct *cinfo, JSAMPARRAY lines) +{ // Go for the maximum quality setting. jpegqs_control_t opts = { - .flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV | JPEGQS_UPSAMPLE_UV, + .flags = JPEGQS_DIAGONALS | JPEGQS_JOINT_YUV, .threads = g_get_num_processors(), .niter = 3, }; - (void) jpegqs_start_decompress(&cinfo, &opts); - while (cinfo.output_scanline < cinfo.output_height) - (void) jpeg_read_scanlines(&cinfo, lines + cinfo.output_scanline, - cinfo.output_height - cinfo.output_scanline); - if (cinfo.out_color_space == JCS_CMYK) - trivial_cmyk_to_host_byte_order_argb( - surface_data, cinfo.output_width * cinfo.output_height); - (void) jpegqs_finish_decompress(&cinfo); + // Waiting for https://github.com/ilyakurdyukov/jpeg-quantsmooth/issues/28 +#if LIBJPEG_TURBO_VERSION_NUMBER < 2001090 + opts.flags |= JPEGQS_UPSAMPLE_UV; +#endif - load_jpeg_finalize(surface, use_cmyk, ctx->screen_profile, data, len); - jpeg_destroy_decompress(&cinfo); - return surface; + (void) jpegqs_start_decompress(cinfo, &opts); + while (cinfo->output_scanline < cinfo->output_height) + (void) jpeg_read_scanlines(cinfo, lines + cinfo->output_scanline, + cinfo->output_height - cinfo->output_scanline); + (void) jpegqs_finish_decompress(cinfo); } #else -#define open_libjpeg_enhanced open_libjpeg_turbo +#define load_libjpeg_enhanced load_libjpeg_simple #endif +static FivIoImage * +open_libjpeg_turbo( + const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) +{ + return load_libjpeg_turbo(data, len, ctx, + ctx->enhance ? load_libjpeg_enhanced : load_libjpeg_simple, + error); +} + // --- WebP -------------------------------------------------------------------- static const char * @@ -1447,17 +1463,15 @@ load_libwebp_error(VP8StatusCode err) } } -static cairo_surface_t * +static FivIoImage * load_libwebp_nonanimated(WebPDecoderConfig *config, const WebPData *wd, const FivIoOpenContext *ctx, GError **error) { - cairo_surface_t *surface = cairo_image_surface_create( + FivIoImage *image = fiv_io_image_new( config->input.has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, config->input.width, config->input.height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); + if (!image) { + set_error(error, "image allocation failure"); return NULL; } @@ -1466,10 +1480,9 @@ load_libwebp_nonanimated(WebPDecoderConfig *config, const WebPData *wd, config->output.width = config->input.width; config->output.height = config->input.height; config->output.is_external_memory = true; - config->output.u.RGBA.rgba = cairo_image_surface_get_data(surface); - config->output.u.RGBA.stride = cairo_image_surface_get_stride(surface); - config->output.u.RGBA.size = - config->output.u.RGBA.stride * cairo_image_surface_get_height(surface); + config->output.u.RGBA.rgba = image->data; + config->output.u.RGBA.stride = image->stride; + config->output.u.RGBA.size = config->output.u.RGBA.stride * image->height; bool premultiply = !ctx->screen_profile; if (G_BYTE_ORDER == G_LITTLE_ENDIAN) @@ -1480,43 +1493,34 @@ load_libwebp_nonanimated(WebPDecoderConfig *config, const WebPData *wd, WebPIDecoder *idec = WebPIDecode(NULL, 0, config); if (!idec) { set_error(error, "WebP decoding error"); - cairo_surface_destroy(surface); + fiv_io_image_unref(image); return NULL; } VP8StatusCode err = WebPIUpdate(idec, wd->bytes, wd->size); - cairo_surface_mark_dirty(surface); int x = 0, y = 0, w = 0, h = 0; (void) WebPIDecodedArea(idec, &x, &y, &w, &h); WebPIDelete(idec); if (err == VP8_STATUS_OK) - return surface; + return image; if (err != VP8_STATUS_SUSPENDED) { g_set_error(error, FIV_IO_ERROR, FIV_IO_ERROR_OPEN, "%s: %s", "WebP decoding error", load_libwebp_error(err)); - cairo_surface_destroy(surface); + fiv_io_image_unref(image); return NULL; } add_warning(ctx, "image file is truncated"); if (config->input.has_alpha) - return surface; + return image; // Always use transparent black, rather than opaque black. - cairo_surface_t *masked = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, config->input.width, config->input.height); - cairo_t *cr = cairo_create(masked); - cairo_set_source_surface(cr, surface, 0, 0); - cairo_rectangle(cr, x, y, w, h); - cairo_clip(cr); - cairo_paint(cr); - cairo_destroy(cr); - cairo_surface_destroy(surface); - return masked; + image->format = CAIRO_FORMAT_ARGB32; + return image; } -static cairo_surface_t * +static FivIoImage * load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info, int *last_timestamp, GError **error) { @@ -1527,38 +1531,39 @@ load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info, return NULL; } - bool is_opaque = (info->bgcolor & 0xFF) == 0xFF; uint64_t area = info->canvas_width * info->canvas_height; - cairo_surface_t *surface = cairo_image_surface_create( - is_opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32, + FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_RGB24, info->canvas_width, info->canvas_height); - - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); + if (!image) { + set_error(error, "image allocation failure"); return NULL; } - uint32_t *dst = (uint32_t *) cairo_image_surface_get_data(surface); + uint32_t *dst = (uint32_t *) image->data; if (G_BYTE_ORDER == G_LITTLE_ENDIAN) { memcpy(dst, buf, area * sizeof *dst); } else { - uint32_t *src = (uint32_t *) buf; - for (uint64_t i = 0; i < area; i++) - *dst++ = GUINT32_FROM_LE(*src++); + const uint32_t *src = (const uint32_t *) buf; + for (uint64_t i = 0; i < area; i++) { + uint32_t value = *src++; + *dst++ = GUINT32_FROM_LE(value); + } } - cairo_surface_mark_dirty(surface); + // info->bgcolor is not reliable. + for (const uint32_t *p = dst, *end = dst + area; p < end; p++) + if ((~*p & 0xff000000)) { + image->format = CAIRO_FORMAT_ARGB32; + break; + } // This API is confusing and awkward. - cairo_surface_set_user_data(surface, &fiv_io_key_frame_duration, - (void *) (intptr_t) (timestamp - *last_timestamp), NULL); + image->frame_duration = timestamp - *last_timestamp; *last_timestamp = timestamp; - return surface; + return image; } -static cairo_surface_t * +static FivIoImage * load_libwebp_animated( const WebPData *wd, const FivIoOpenContext *ctx, GError **error) { @@ -1572,7 +1577,7 @@ load_libwebp_animated( WebPAnimDecoder *dec = WebPAnimDecoderNew(wd, &options); WebPAnimDecoderGetInfo(dec, &info); - cairo_surface_t *frames = NULL, *frames_tail = NULL; + FivIoImage *frames = NULL, *frames_tail = NULL; if (info.canvas_width > INT_MAX || info.canvas_height > INT_MAX) { set_error(error, "image dimensions overflow"); goto fail; @@ -1580,30 +1585,27 @@ load_libwebp_animated( int last_timestamp = 0; while (WebPAnimDecoderHasMoreFrames(dec)) { - cairo_surface_t *surface = + FivIoImage *image = load_libwebp_frame(dec, &info, &last_timestamp, error); - if (!surface) { - g_clear_pointer(&frames, cairo_surface_destroy); + if (!image) { + g_clear_pointer(&frames, fiv_io_image_unref); goto fail; } if (frames_tail) - cairo_surface_set_user_data(frames_tail, &fiv_io_key_frame_next, - surface, (cairo_destroy_func_t) cairo_surface_destroy); + frames_tail->frame_next = image; else - frames = surface; + frames = image; - cairo_surface_set_user_data( - surface, &fiv_io_key_frame_previous, frames_tail, NULL); - frames_tail = surface; + image->frame_previous = frames_tail; + frames_tail = image; } if (frames) { - cairo_surface_set_user_data( - frames, &fiv_io_key_frame_previous, frames_tail, NULL); + frames->frame_previous = frames_tail; } else { set_error(error, "the animation has no frames"); - g_clear_pointer(&frames, cairo_surface_destroy); + g_clear_pointer(&frames, fiv_io_image_unref); } fail: @@ -1611,7 +1613,7 @@ fail: return frames; } -static cairo_surface_t * +static FivIoImage * open_libwebp( const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) { @@ -1631,7 +1633,7 @@ open_libwebp( return NULL; } - cairo_surface_t *result = config.input.has_animation + FivIoImage *result = config.input.has_animation ? load_libwebp_animated(&wd, ctx, error) : load_libwebp_nonanimated(&config, &wd, ctx, error); if (!result) @@ -1646,90 +1648,321 @@ open_libwebp( } // Releasing the demux chunk iterator is actually a no-op. - // TODO(p): Avoid copy-pasting the chunk transfer code. WebPChunkIterator chunk_iter = {}; uint32_t flags = WebPDemuxGetI(demux, WEBP_FF_FORMAT_FLAGS); if ((flags & EXIF_FLAG) && WebPDemuxGetChunk(demux, "EXIF", 1, &chunk_iter)) { - cairo_surface_set_user_data(result, &fiv_io_key_exif, - g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), - (cairo_destroy_func_t) g_bytes_unref); + result->exif = + g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size); WebPDemuxReleaseChunkIterator(&chunk_iter); } if ((flags & ICCP_FLAG) && WebPDemuxGetChunk(demux, "ICCP", 1, &chunk_iter)) { - cairo_surface_set_user_data(result, &fiv_io_key_icc, - g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), - (cairo_destroy_func_t) g_bytes_unref); + result->icc = + g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size); WebPDemuxReleaseChunkIterator(&chunk_iter); } if ((flags & XMP_FLAG) && WebPDemuxGetChunk(demux, "XMP ", 1, &chunk_iter)) { - cairo_surface_set_user_data(result, &fiv_io_key_xmp, - g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), - (cairo_destroy_func_t) g_bytes_unref); + result->xmp = + g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size); WebPDemuxReleaseChunkIterator(&chunk_iter); } if (WebPDemuxGetChunk(demux, "THUM", 1, &chunk_iter)) { - cairo_surface_set_user_data(result, &fiv_io_key_thum, - g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size), - (cairo_destroy_func_t) g_bytes_unref); + result->thum = + g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size); WebPDemuxReleaseChunkIterator(&chunk_iter); } - if (flags & ANIMATION_FLAG) { - cairo_surface_set_user_data(result, &fiv_io_key_loops, - (void *) (uintptr_t) WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT), - NULL); - } + if (flags & ANIMATION_FLAG) + result->loops = WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT); WebPDemuxDelete(demux); - if (ctx->screen_profile) { - fiv_io_profile_xrgb32_page(result, ctx->screen_profile); - fiv_io_premultiply_argb32_page(result); - } + if (ctx->screen_profile) + fiv_io_cmm_argb32_premultiply_page( + ctx->cmm, result, ctx->screen_profile); fail: WebPFreeDecBuffer(&config.output); return result; } -// --- Optional dependencies --------------------------------------------------- +// --- TIFF/EP + DNG ----------------------------------------------------------- +// In Nikon NEF files, which claim to be TIFF/EP-compatible, IFD0 is a tiny +// uncompressed thumbnail with SubIFDs that, aside from raw sensor data, +// typically contain a nearly full-size JPEG preview. +// +// LibRaw takes too long a time to render something that will never be as good +// as that large preview--e.g., due to exposure correction or denoising. +// While since version 0.21.0 the library provides an API that would allow us +// to extract the JPEG, a little bit of custom processing won't hurt either. +// TODO(p): Though it can also extract thumbnails from many more formats, +// so maybe keep this code as a fallback for old or missing LibRaw. +// +// Note that libtiff can only read the horrible IFD0 thumbnail. +// (TIFFSetSubDirectory() requires an ImageLength tag that's missing from JPEG +// SubIFDs, and TIFFReadCustomDirectory() takes a privately defined struct that +// may not be omitted.) -#ifdef HAVE_LIBRAW // --------------------------------------------------------- +static bool +tiffer_find(const struct tiffer *self, uint16_t tag, struct tiffer_entry *entry) +{ + // Note that we could employ binary search, because tags must be ordered: + // - TIFF 6.0: Sort Order + // - ISO/DIS 12234-2: 4.1.2, 5.1 + // - CIPA DC-007-2009 (Multi-Picture Format): 5.2.3., 5.2.4. + // - CIPA DC-008-2019 (Exif 2.32): 4.6.2. + // However, it doesn't seem to warrant the ugly code. + struct tiffer T = *self; + while (tiffer_next_entry(&T, entry)) { + if (entry->tag == tag) + return true; + } + *entry = (struct tiffer_entry) {}; + return false; +} -static cairo_surface_t * -open_libraw(const char *data, gsize len, GError **error) +static bool +tiffer_find_integer(const struct tiffer *self, uint16_t tag, int64_t *i) { - // https://github.com/LibRaw/LibRaw/issues/418 - libraw_data_t *iprc = libraw_init( - LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK); - if (!iprc) { - set_error(error, "failed to obtain a LibRaw handle"); + struct tiffer_entry entry = {}; + return tiffer_find(self, tag, &entry) && tiffer_integer(self, &entry, i); +} + +// In case of failure, an entry with a zero "remaining_count" is returned. +static struct tiffer_entry +tiff_ep_subifds_init(const struct tiffer *T) +{ + struct tiffer_entry entry = {}; + (void) tiffer_find(T, TIFF_SubIFDs, &entry); + return entry; +} + +static bool +tiff_ep_subifds_next( + const struct tiffer *T, struct tiffer_entry *subifds, struct tiffer *subT) +{ + // XXX: Except for a zero "remaining_count", all conditions are errors, + // and should perhaps be reported. + int64_t offset = 0; + if (!tiffer_integer(T, subifds, &offset) || + offset < 0 || offset > UINT32_MAX || !tiffer_subifd(T, offset, subT)) + return false; + + (void) tiffer_next_value(subifds); + return true; +} + +static bool +tiff_ep_find_main(const struct tiffer *T, struct tiffer *outputT) +{ + // This is a mandatory field. + int64_t type = 0; + if (!tiffer_find_integer(T, TIFF_NewSubfileType, &type)) + return false; + + // This is the main image. + // (See DNG rather than ISO/DIS 12234-2 for values.) + if (type == 0) { + *outputT = *T; + return true; + } + + struct tiffer_entry subifds = tiff_ep_subifds_init(T); + struct tiffer subT = {}; + while (tiff_ep_subifds_next(T, &subifds, &subT)) + if (tiff_ep_find_main(&subT, outputT)) + return true; + return false; +} + +struct tiff_ep_jpeg { + const uint8_t *jpeg; ///< JPEG data stream + size_t jpeg_length; ///< JPEG data stream length + int64_t pixels; ///< Number of pixels in the JPEG +}; + +static void +tiff_ep_find_jpeg_evaluate(const struct tiffer *T, struct tiff_ep_jpeg *out) +{ + // This is a mandatory field. + int64_t compression = 0; + if (!tiffer_find_integer(T, TIFF_Compression, &compression)) + return; + + uint16_t tag_pointer = 0, tag_length = 0; + switch (compression) { + // This is how Exif specifies it, which doesn't follow TIFF 6.0. + case TIFF_Compression_JPEG: + tag_pointer = TIFF_JPEGInterchangeFormat; + tag_length = TIFF_JPEGInterchangeFormatLength; + break; + // Theoretically, there may be more strips, but this is not expected. + case TIFF_Compression_JPEGDatastream: + tag_pointer = TIFF_StripOffsets; + tag_length = TIFF_StripByteCounts; + break; + default: + return; + } + + int64_t ipointer = 0, ilength = 0; + if (!tiffer_find_integer(T, tag_pointer, &ipointer) || ipointer <= 0 || + !tiffer_find_integer(T, tag_length, &ilength) || ilength <= 0 || + ipointer > T->end - T->begin || + T->end - T->begin - ipointer < ilength) + return; + + // Note that to get the largest JPEG, + // we don't need to descend into Exif thumbnails. + // TODO(p): Consider DNG 1.2.0.0 PreviewColorSpace. + // But first, try to find some real-world files with it. + const uint8_t *jpeg = T->begin + ipointer; + size_t jpeg_length = ilength; + + struct jpeg_metadata meta = {}; + parse_jpeg_metadata((const char *) jpeg, jpeg_length, &meta); + int64_t pixels = meta.width * meta.height; + if (pixels > out->pixels) { + out->jpeg = jpeg; + out->jpeg_length = jpeg_length; + out->pixels = pixels; + } +} + +static bool +tiff_ep_find_jpeg(const struct tiffer *T, struct tiff_ep_jpeg *out) +{ + // This is a mandatory field. + int64_t type = 0; + if (!tiffer_find_integer(T, TIFF_NewSubfileType, &type)) + return false; + + // This is a thumbnail of the main image. + // (See DNG rather than ISO/DIS 12234-2 for values.) + if (type == 1) + tiff_ep_find_jpeg_evaluate(T, out); + + struct tiffer_entry subifds = tiff_ep_subifds_init(T); + struct tiffer subT = {}; + while (tiff_ep_subifds_next(T, &subifds, &subT)) + if (!tiff_ep_find_jpeg(&subT, out)) + return false; + return true; +} + +static FivIoImage * +load_tiff_ep( + const struct tiffer *T, const FivIoOpenContext *ctx, GError **error) +{ + // ISO/DIS 12234-2 is a fuck-up that says this should be in "IFD0", + // but it might have intended to say "all top-level IFDs". + // The DNG specification shares the same problem. + // + // In any case, chained TIFFs are relatively rare. + struct tiffer_entry entry = {}; + bool is_tiffep = tiffer_find(T, TIFF_TIFF_EPStandardID, &entry) && + entry.type == TIFFER_BYTE && entry.remaining_count == 4 && + entry.p[0] == 1 && !entry.p[1] && !entry.p[2] && !entry.p[3]; + + // Apple ProRAW, e.g., does not claim TIFF/EP compatibility, + // but we should still be able to make sense of it. + bool is_supported_dng = tiffer_find(T, TIFF_DNGBackwardVersion, &entry) && + entry.type == TIFFER_BYTE && entry.remaining_count == 4 && + entry.p[0] == 1 && entry.p[1] <= 6 && !entry.p[2] && !entry.p[3]; + if (!is_tiffep && !is_supported_dng) { + set_error(error, "not a supported TIFF/EP or DNG image"); return NULL; } -#if 0 - // TODO(p): Consider setting this--the image is still likely to be - // rendered suboptimally, so why not make it faster. - iprc->params.half_size = 1; -#endif + struct tiffer fullT = {}; + if (!tiff_ep_find_main(T, &fullT)) { + set_error(error, "could not find a main image"); + return NULL; + } - // TODO(p): Check if we need to set anything for autorotation (sizes.flip). - iprc->params.use_camera_wb = 1; - iprc->params.output_color = 1; // sRGB, TODO(p): Is this used? - iprc->params.output_bps = 8; // This should be the default value. + int64_t width = 0, height = 0; + if (!tiffer_find_integer(&fullT, TIFF_ImageWidth, &width) || + !tiffer_find_integer(&fullT, TIFF_ImageLength, &height) || + width <= 0 || height <= 0) { + set_error(error, "missing or invalid main image dimensions"); + return NULL; + } - int err = 0; - if ((err = libraw_open_buffer(iprc, (void *) data, len))) { - set_error(error, libraw_strerror(err)); - libraw_close(iprc); + struct tiff_ep_jpeg out = {}; + if (!tiff_ep_find_jpeg(T, &out)) { + set_error(error, "error looking for a full-size JPEG preview"); + return NULL; + } + + // Nikon NEFs seem to generally have a preview above 99 percent, + // (though some of them may not even reach 50 percent). + // Be a bit more generous than that with our crop tolerance. + // TODO(p): Also take into account DNG DefaultCropSize, if present. + if (out.pixels / ((double) width * height) < 0.95) { + set_error(error, "could not find a large enough JPEG preview"); return NULL; } - // TODO(p): Do we need to check iprc->idata.raw_count? Maybe for TIFFs? + FivIoImage *image = open_libjpeg_turbo( + (const char *) out.jpeg, out.jpeg_length, ctx, error); + if (!image) + return NULL; + + // Note that Exif may override this later in fiv_io_open_from_data(). + // TODO(p): Try to use the Orientation field nearest to the target IFD. + // IFD0 just happens to be fine for Nikon NEF. + int64_t orientation = 0; + if (tiffer_find_integer(T, TIFF_Orientation, &orientation) && + orientation >= 1 && orientation <= 8) { + image->orientation = orientation; + } + + // XXX: AdobeRGB Nikon NEFs can only be distinguished by a ColorSpace tag + // from within their MakerNote. + return image; +} + +static FivIoImage * +open_tiff_ep( + const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) +{ + // -Wunused-function, we might want to give this its own compile unit. + (void) tiffer_real; + + struct tiffer T = {}; + if (!tiffer_init(&T, (const uint8_t *) data, len)) { + set_error(error, "not a TIFF file"); + return NULL; + } + + FivIoImage *result = NULL, *result_tail = NULL; + while (tiffer_next_ifd(&T)) { + if (!try_append_page( + load_tiff_ep(&T, ctx, error), &result, &result_tail)) { + g_clear_pointer(&result, fiv_io_image_unref); + return NULL; + } + if (ctx->first_frame_only) + break; + + // TODO(p): Try to adjust tiffer so that this isn't necessary. + struct tiffer_entry dummy = {}; + while (tiffer_next_entry(&T, &dummy)) + ; + } + return result; +} + +// --- Optional dependencies --------------------------------------------------- + +#ifdef HAVE_LIBRAW // --------------------------------------------------------- + +static FivIoImage * +load_libraw(libraw_data_t *iprc, GError **error) +{ + int err = 0; if ((err = libraw_unpack(iprc))) { set_error(error, libraw_strerror(err)); - libraw_close(iprc); return NULL; } @@ -1737,7 +1970,6 @@ open_libraw(const char *data, gsize len, GError **error) // TODO(p): I'm not sure when this is necessary or useful yet. if ((err = libraw_adjust_sizes_info_only(iprc))) { set_error(error, libraw_strerror(err)); - libraw_close(iprc); return NULL; } #endif @@ -1745,7 +1977,6 @@ open_libraw(const char *data, gsize len, GError **error) // TODO(p): Documentation says I should look at the code and do it myself. if ((err = libraw_dcraw_process(iprc))) { set_error(error, libraw_strerror(err)); - libraw_close(iprc); return NULL; } @@ -1754,7 +1985,6 @@ open_libraw(const char *data, gsize len, GError **error) libraw_processed_image_t *image = libraw_dcraw_make_mem_image(iprc, &err); if (!image) { set_error(error, libraw_strerror(err)); - libraw_close(iprc); return NULL; } @@ -1762,26 +1992,18 @@ open_libraw(const char *data, gsize len, GError **error) if (image->colors != 3 || image->bits != 8) { set_error(error, "unexpected number of colours, or bit depth"); libraw_dcraw_clear_mem(image); - libraw_close(iprc); return NULL; } - int width = image->width, height = image->height; - cairo_surface_t *surface = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); + FivIoImage *I = + fiv_io_image_new(CAIRO_FORMAT_RGB24, image->width, image->height); + if (!I) { + set_error(error, "image allocation failure"); libraw_dcraw_clear_mem(image); - libraw_close(iprc); return NULL; } - // Starting to modify pixel data directly. Probably an unnecessary call. - cairo_surface_flush(surface); - - uint32_t *pixels = (uint32_t *) cairo_image_surface_get_data(surface); + uint32_t *pixels = (uint32_t *) I->data; unsigned char *p = image->data; for (ushort y = 0; y < image->height; y++) { for (ushort x = 0; x < image->width; x++) { @@ -1791,12 +2013,55 @@ open_libraw(const char *data, gsize len, GError **error) } } - // Pixel data has been written, need to let Cairo know. - cairo_surface_mark_dirty(surface); - libraw_dcraw_clear_mem(image); + return I; +} + +static FivIoImage * +open_libraw( + const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) +{ + // https://github.com/LibRaw/LibRaw/issues/418 + libraw_data_t *iprc = libraw_init( + LIBRAW_OPIONS_NO_MEMERR_CALLBACK | LIBRAW_OPIONS_NO_DATAERR_CALLBACK); + if (!iprc) { + set_error(error, "failed to obtain a LibRaw handle"); + return NULL; + } + + // TODO(p): Check if we need to set anything for autorotation (sizes.flip). + iprc->params.use_camera_wb = 1; + iprc->params.output_color = 1; // sRGB, TODO(p): Is this used? + iprc->params.output_bps = 8; // This should be the default value. + + int err = 0; + FivIoImage *result = NULL, *result_tail = NULL; + if ((err = libraw_open_buffer(iprc, (const void *) data, len))) { + set_error(error, libraw_strerror(err)); + goto out; + } + if (!try_append_page(load_libraw(iprc, error), &result, &result_tail) || + ctx->first_frame_only) + goto out; + + for (unsigned i = 1; i < iprc->idata.raw_count; i++) { + iprc->rawparams.shot_select = i; + + // This library is terrible, we need to start again. + if ((err = libraw_open_buffer(iprc, (const void *) data, len))) { + set_error(error, libraw_strerror(err)); + g_clear_pointer(&result, fiv_io_image_unref); + goto out; + } + if (!try_append_page(load_libraw(iprc, error), &result, &result_tail)) { + g_clear_pointer(&result, fiv_io_image_unref); + goto out; + } + } + +out: libraw_close(iprc); - return surface; + return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile); } #endif // HAVE_LIBRAW --------------------------------------------------------- @@ -1810,16 +2075,16 @@ typedef struct { } FivIoRenderClosureResvg; static void -load_resvg_destroy(void *closure) +load_resvg_destroy(FivIoRenderClosure *closure) { - FivIoRenderClosureResvg *self = closure; + FivIoRenderClosureResvg *self = (void *) closure; resvg_tree_destroy(self->tree); g_free(self); } -static cairo_surface_t * -load_resvg_render_internal( - FivIoRenderClosureResvg *self, double scale, GError **error) +static FivIoImage * +load_resvg_render_internal(FivIoRenderClosureResvg *self, double scale, + FivIoCmm *cmm, FivIoProfile *target, GError **error) { double w = ceil(self->width * scale), h = ceil(self->height * scale); if (w > SHRT_MAX || h > SHRT_MAX) { @@ -1827,38 +2092,37 @@ load_resvg_render_internal( return NULL; } - cairo_surface_t *surface = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); + FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_ARGB32, w, h); + if (!image) { + set_error(error, "image allocation failure"); return NULL; } - uint32_t *pixels = (uint32_t *) cairo_image_surface_get_data(surface); + uint32_t *pixels = (uint32_t *) image->data; +#if RESVG_MAJOR_VERSION == 0 && RESVG_MINOR_VERSION < 33 resvg_fit_to fit_to = { scale == 1 ? RESVG_FIT_TO_TYPE_ORIGINAL : RESVG_FIT_TO_TYPE_ZOOM, scale}; resvg_render(self->tree, fit_to, resvg_transform_identity(), - cairo_image_surface_get_width(surface), - cairo_image_surface_get_height(surface), (char *) pixels); + image->width, image->height, (char *) pixels); +#else + resvg_render(self->tree, (resvg_transform) {.a = scale, .d = scale}, + image->width, image->height, (char *) pixels); +#endif - // TODO(p): Also apply colour management, we'll need to un-premultiply. for (int i = 0; i < w * h; i++) { uint32_t rgba = g_ntohl(pixels[i]); pixels[i] = rgba << 24 | rgba >> 8; } - - cairo_surface_mark_dirty(surface); - return surface; + return fiv_io_cmm_finish(cmm, image, target); } -static cairo_surface_t * -load_resvg_render(FivIoRenderClosure *closure, double scale) +static FivIoImage * +load_resvg_render(FivIoRenderClosure *closure, + FivIoCmm *cmm, FivIoProfile *target, double scale) { FivIoRenderClosureResvg *self = (FivIoRenderClosureResvg *) closure; - return load_resvg_render_internal(self, scale, NULL); + return load_resvg_render_internal(self, scale, cmm, target, NULL); } static const char * @@ -1882,7 +2146,7 @@ load_resvg_error(int err) } } -static cairo_surface_t * +static FivIoImage * open_resvg( const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) { @@ -1911,19 +2175,20 @@ open_resvg( FivIoRenderClosureResvg *closure = g_malloc0(sizeof *closure); closure->parent.render = load_resvg_render; + closure->parent.destroy = load_resvg_destroy; closure->tree = tree; closure->width = size.width; closure->height = size.height; - cairo_surface_t *surface = load_resvg_render_internal(closure, 1., error); - if (!surface) { - load_resvg_destroy(closure); + FivIoImage *image = load_resvg_render_internal( + closure, 1., ctx->cmm, ctx->screen_profile, error); + if (!image) { + load_resvg_destroy(&closure->parent); return NULL; } - cairo_surface_set_user_data( - surface, &fiv_io_key_render, closure, load_resvg_destroy); - return surface; + image->render = &closure->parent; + return image; } #endif // HAVE_RESVG ---------------------------------------------------------- @@ -1937,43 +2202,54 @@ typedef struct { } FivIoRenderClosureLibrsvg; static void -load_librsvg_destroy(void *closure) +load_librsvg_destroy(FivIoRenderClosure *closure) { - FivIoRenderClosureLibrsvg *self = closure; + FivIoRenderClosureLibrsvg *self = (void *) closure; g_object_unref(self->handle); g_free(self); } -static cairo_surface_t * -load_librsvg_render(FivIoRenderClosure *closure, double scale) +static FivIoImage * +load_librsvg_render_internal(FivIoRenderClosureLibrsvg *self, double scale, + FivIoCmm *cmm, FivIoProfile *target, GError **error) { - FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure; RsvgRectangle viewport = {.x = 0, .y = 0, .width = self->width * scale, .height = self->height * scale}; - cairo_surface_t *surface = cairo_image_surface_create( + FivIoImage *image = fiv_io_image_new( CAIRO_FORMAT_ARGB32, ceil(viewport.width), ceil(viewport.height)); + if (!image) { + set_error(error, "image allocation failure"); + return NULL; + } - GError *error = NULL; + cairo_surface_t *surface = fiv_io_image_to_surface_noref(image); cairo_t *cr = cairo_create(surface); - (void) rsvg_handle_render_document(self->handle, cr, &viewport, &error); + cairo_surface_destroy(surface); + gboolean success = + rsvg_handle_render_document(self->handle, cr, &viewport, error); + cairo_status_t status = cairo_status(cr); cairo_destroy(cr); - if (error) { - g_debug("%s", error->message); - g_error_free(error); - cairo_surface_destroy(surface); + if (!success) { + fiv_io_image_unref(image); return NULL; } - - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - g_debug("%s", cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); + if (status != CAIRO_STATUS_SUCCESS) { + set_error(error, cairo_status_to_string(status)); + fiv_io_image_unref(image); return NULL; } - return surface; + return fiv_io_cmm_finish(cmm, image, target); } -static cairo_surface_t * +static FivIoImage * +load_librsvg_render(FivIoRenderClosure *closure, + FivIoCmm *cmm, FivIoProfile *target, double scale) +{ + FivIoRenderClosureLibrsvg *self = (FivIoRenderClosureLibrsvg *) closure; + return load_librsvg_render_internal(self, scale, cmm, target, NULL); +} + +static FivIoImage * open_librsvg( const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) { @@ -2010,32 +2286,24 @@ open_librsvg( h = viewbox.height; } - // librsvg rasterizes filters, so this method isn't fully appropriate. - // It might be worth removing altogether. - cairo_rectangle_t extents = { - .x = 0, .y = 0, .width = ceil(w), .height = ceil(h)}; - cairo_surface_t *surface = - cairo_recording_surface_create(CAIRO_CONTENT_COLOR_ALPHA, &extents); - - cairo_t *cr = cairo_create(surface); - RsvgRectangle viewport = {.x = 0, .y = 0, .width = w, .height = h}; - if (!rsvg_handle_render_document(handle, cr, &viewport, error)) { - cairo_surface_destroy(surface); - cairo_destroy(cr); - g_object_unref(handle); - return NULL; - } - - cairo_destroy(cr); - FivIoRenderClosureLibrsvg *closure = g_malloc0(sizeof *closure); closure->parent.render = load_librsvg_render; + closure->parent.destroy = load_librsvg_destroy; closure->handle = handle; closure->width = w; closure->height = h; - cairo_surface_set_user_data( - surface, &fiv_io_key_render, closure, load_librsvg_destroy); - return surface; + + // librsvg rasterizes filters, so rendering to a recording Cairo surface + // has been abandoned. + FivIoImage *image = load_librsvg_render_internal( + closure, 1., ctx->cmm, ctx->screen_profile, error); + if (!image) { + load_librsvg_destroy(&closure->parent); + return NULL; + } + + image->render = &closure->parent; + return image; } #endif // HAVE_LIBRSVG -------------------------------------------------------- @@ -2108,8 +2376,9 @@ static const XcursorFile fiv_io_xcursor_adaptor = { .seek = fiv_io_xcursor_seek, }; -static cairo_surface_t * -open_xcursor(const char *data, gsize len, GError **error) +static FivIoImage * +open_xcursor( + const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) { if (len > G_MAXLONG) { set_error(error, "size overflow"); @@ -2130,54 +2399,46 @@ open_xcursor(const char *data, gsize len, GError **error) } // Interpret cursors as animated pages. - cairo_surface_t *pages = NULL, *frames_head = NULL, *frames_tail = NULL; + FivIoImage *pages = NULL, *frames_head = NULL, *frames_tail = NULL; // XXX: Assuming that all "nominal sizes" have the same dimensions. XcursorDim last_nominal = -1; for (int i = 0; i < images->nimage; i++) { XcursorImage *image = images->images[i]; + FivIoImage *I = + fiv_io_image_new(CAIRO_FORMAT_ARGB32, image->width, image->height); + if (!I) { + add_warning(ctx, "%s", "image allocation failure"); + break; + } + // The library automatically byte swaps in _XcursorReadImage(). - cairo_surface_t *surface = cairo_image_surface_create_for_data( - (unsigned char *) image->pixels, CAIRO_FORMAT_ARGB32, - image->width, image->height, image->width * sizeof *image->pixels); - cairo_surface_set_user_data(surface, &fiv_io_key_frame_duration, - (void *) (intptr_t) image->delay, NULL); + memcpy(I->data, image->pixels, I->stride * I->height); + I->frame_duration = image->delay; if (pages && image->size == last_nominal) { - cairo_surface_set_user_data( - surface, &fiv_io_key_frame_previous, frames_tail, NULL); - cairo_surface_set_user_data(frames_tail, &fiv_io_key_frame_next, - surface, (cairo_destroy_func_t) cairo_surface_destroy); + I->frame_previous = frames_tail; + frames_tail->frame_next = I; } else if (frames_head) { - cairo_surface_set_user_data( - frames_head, &fiv_io_key_frame_previous, frames_tail, NULL); - - cairo_surface_set_user_data(frames_head, &fiv_io_key_page_next, - surface, (cairo_destroy_func_t) cairo_surface_destroy); - cairo_surface_set_user_data( - surface, &fiv_io_key_page_previous, frames_head, NULL); - frames_head = surface; + frames_head->frame_previous = frames_tail; + + frames_head->page_next = I; + I->page_previous = frames_head; + frames_head = I; } else { - pages = frames_head = surface; + pages = frames_head = I; } - frames_tail = surface; + frames_tail = I; last_nominal = image->size; } - if (!pages) { - XcursorImagesDestroy(images); + XcursorImagesDestroy(images); + if (!pages) return NULL; - } // Wrap around animations in the last page. - cairo_surface_set_user_data( - frames_head, &fiv_io_key_frame_previous, frames_tail, NULL); - - // There is no need to copy data, assign it to the surface. - static cairo_user_data_key_t key = {}; - cairo_surface_set_user_data( - pages, &key, images, (cairo_destroy_func_t) XcursorImagesDestroy); + frames_head->frame_previous = frames_tail; // Do not bother doing colour correction, there is no correct rendering. return pages; @@ -2186,10 +2447,10 @@ open_xcursor(const char *data, gsize len, GError **error) #endif // HAVE_XCURSOR -------------------------------------------------------- #ifdef HAVE_LIBHEIF //--------------------------------------------------------- -static cairo_surface_t * +static FivIoImage * load_libheif_image(struct heif_image_handle *handle, GError **error) { - cairo_surface_t *surface = NULL; + FivIoImage *I = NULL; int has_alpha = heif_image_handle_has_alpha_channel(handle); int bit_depth = heif_image_handle_get_luma_bits_per_pixel(handle); if (bit_depth < 0) { @@ -2212,13 +2473,10 @@ load_libheif_image(struct heif_image_handle *handle, GError **error) int w = heif_image_get_width(image, heif_channel_interleaved); int h = heif_image_get_height(image, heif_channel_interleaved); - surface = cairo_image_surface_create( + I = fiv_io_image_new( has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, w, h); - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); - cairo_surface_destroy(surface); - surface = NULL; + if (!I) { + set_error(error, "image allocation failure"); goto fail_process; } @@ -2226,11 +2484,8 @@ load_libheif_image(struct heif_image_handle *handle, GError **error) int src_stride = 0; const uint8_t *src = heif_image_get_plane_readonly( image, heif_channel_interleaved, &src_stride); - int dst_stride = cairo_image_surface_get_stride(surface); - const uint8_t *dst = cairo_image_surface_get_data(surface); - for (int y = 0; y < h; y++) { - uint32_t *dstp = (uint32_t *) (dst + dst_stride * y); + uint32_t *dstp = (uint32_t *) (I->data + I->stride * y); const uint32_t *srcp = (const uint32_t *) (src + src_stride * y); for (int x = 0; x < w; x++) { uint32_t rgba = g_ntohl(srcp[x]); @@ -2240,7 +2495,7 @@ load_libheif_image(struct heif_image_handle *handle, GError **error) // TODO(p): Test real behaviour on real transparent images. if (has_alpha && !heif_image_handle_is_premultiplied_alpha(handle)) - fiv_io_premultiply_argb32(surface); + fiv_io_premultiply_argb32(I); heif_item_id exif_id = 0; if (heif_image_handle_get_list_of_metadata_block_IDs( @@ -2252,9 +2507,7 @@ load_libheif_image(struct heif_image_handle *handle, GError **error) g_warning("%s", err.message); g_free(exif); } else { - cairo_surface_set_user_data(surface, &fiv_io_key_exif, - g_bytes_new_take(exif, exif_len), - (cairo_destroy_func_t) g_bytes_unref); + I->exif = g_bytes_new_take(exif, exif_len); } } @@ -2268,26 +2521,22 @@ load_libheif_image(struct heif_image_handle *handle, GError **error) g_warning("%s", err.message); g_free(icc); } else { - cairo_surface_set_user_data(surface, &fiv_io_key_icc, - g_bytes_new_take(icc, icc_len), - (cairo_destroy_func_t) g_bytes_unref); + I->icc = g_bytes_new_take(icc, icc_len); } } - cairo_surface_mark_dirty(surface); - fail_process: heif_image_release(image); fail_decode: heif_decoding_options_free(opts); fail: - return surface; + return I; } static void load_libheif_aux_images(const FivIoOpenContext *ioctx, - struct heif_image_handle *top, cairo_surface_t **result, - cairo_surface_t **result_tail) + struct heif_image_handle *top, + FivIoImage **result, FivIoImage **result_tail) { // Include the depth image, we have no special processing for it now. int filter = LIBHEIF_AUX_IMAGE_FILTER_OMIT_ALPHA; @@ -2317,14 +2566,14 @@ load_libheif_aux_images(const FivIoOpenContext *ioctx, g_free(ids); } -static cairo_surface_t * +static FivIoImage * open_libheif( const char *data, gsize len, const FivIoOpenContext *ioctx, GError **error) { // libheif will throw C++ exceptions on allocation failures. // The library is generally awful through and through. struct heif_context *ctx = heif_context_alloc(); - cairo_surface_t *result = NULL, *result_tail = NULL; + FivIoImage *result = NULL, *result_tail = NULL; struct heif_error err; err = heif_context_read_from_memory_without_copy(ctx, data, len, NULL); @@ -2356,14 +2605,14 @@ open_libheif( heif_image_handle_release(handle); } if (!result) { - g_clear_pointer(&result, cairo_surface_destroy); + g_clear_pointer(&result, fiv_io_image_unref); set_error(error, "empty or unsupported image"); } g_free(ids); fail_read: heif_context_free(ctx); - return fiv_io_profile_finalize(result, ioctx->screen_profile); + return fiv_io_cmm_finish(ioctx->cmm, result, ioctx->screen_profile); } #endif // HAVE_LIBHEIF -------------------------------------------------------- @@ -2470,7 +2719,7 @@ fiv_io_tiff_warning(G_GNUC_UNUSED thandle_t h, // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static cairo_surface_t * +static FivIoImage * load_libtiff_directory(TIFF *tiff, GError **error) { char emsg[1024] = ""; @@ -2486,22 +2735,26 @@ load_libtiff_directory(TIFF *tiff, GError **error) return NULL; } - cairo_surface_t *surface = NULL; + FivIoImage *I = NULL; if (image.width > G_MAXINT || image.height >= G_MAXINT || G_MAXUINT32 / image.width < image.height) { set_error(error, "image dimensions too large"); goto fail; } - surface = cairo_image_surface_create(image.alpha != EXTRASAMPLE_UNSPECIFIED + I = fiv_io_image_new(image.alpha != EXTRASAMPLE_UNSPECIFIED ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24, image.width, image.height); + if (!I) { + set_error(error, "image allocation failure"); + goto fail; + } image.req_orientation = ORIENTATION_LEFTTOP; - uint32_t *raster = (uint32_t *) cairo_image_surface_get_data(surface); + uint32_t *raster = (uint32_t *) I->data; if (!TIFFRGBAImageGet(&image, raster, image.width, image.height)) { - g_clear_pointer(&surface, cairo_surface_destroy); + g_clear_pointer(&I, fiv_io_image_unref); goto fail; } @@ -2513,40 +2766,36 @@ load_libtiff_directory(TIFF *tiff, GError **error) } // It seems that neither GIMP nor Photoshop use unassociated alpha. if (image.alpha == EXTRASAMPLE_UNASSALPHA) - fiv_io_premultiply_argb32(surface); + fiv_io_premultiply_argb32(I); - cairo_surface_mark_dirty(surface); // XXX: The whole file is essentially an Exif, any ideas? + // TODO(p): TIFF has a number of fields that an ICC profile can be + // constructed from--it's not a good idea to blindly default to sRGB + // if we don't find an ICC profile. const uint32_t meta_len = 0; const void *meta = NULL; - if (TIFFGetField(tiff, TIFFTAG_ICCPROFILE, &meta_len, &meta)) { - cairo_surface_set_user_data(surface, &fiv_io_key_icc, - g_bytes_new(meta, meta_len), (cairo_destroy_func_t) g_bytes_unref); - } - if (TIFFGetField(tiff, TIFFTAG_XMLPACKET, &meta_len, &meta)) { - cairo_surface_set_user_data(surface, &fiv_io_key_xmp, - g_bytes_new(meta, meta_len), (cairo_destroy_func_t) g_bytes_unref); - } + if (TIFFGetField(tiff, TIFFTAG_ICCPROFILE, &meta_len, &meta)) + I->icc = g_bytes_new(meta, meta_len); + if (TIFFGetField(tiff, TIFFTAG_XMLPACKET, &meta_len, &meta)) + I->xmp = g_bytes_new(meta, meta_len); // Don't ask. The API is high, alright, I'm just not sure about the level. uint16_t orientation = 0; if (TIFFGetField(tiff, TIFFTAG_ORIENTATION, &orientation)) { if (orientation == 5 || orientation == 7) - cairo_surface_set_user_data( - surface, &fiv_io_key_orientation, (void *) (uintptr_t) 5, NULL); + I->orientation = 5; if (orientation == 6 || orientation == 8) - cairo_surface_set_user_data( - surface, &fiv_io_key_orientation, (void *) (uintptr_t) 7, NULL); + I->orientation = 7; } fail: TIFFRGBAImageEnd(&image); // TODO(p): It's possible to implement ClipPath easily with Cairo. - return surface; + return I; } -static cairo_surface_t * +static FivIoImage * open_libtiff( const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) { @@ -2562,42 +2811,19 @@ open_libtiff( .len = len, }; - cairo_surface_t *result = NULL, *result_tail = NULL; + FivIoImage *result = NULL, *result_tail = NULL; TIFF *tiff = TIFFClientOpen(ctx->uri, "rm" /* Avoid mmap. */, &h, fiv_io_tiff_read, fiv_io_tiff_write, fiv_io_tiff_seek, fiv_io_tiff_close, fiv_io_tiff_size, NULL, NULL); if (!tiff) goto fail; - // In Nikon NEF files, IFD0 is a tiny uncompressed thumbnail with SubIFDs-- - // two of them JPEGs, the remaining one is raw. libtiff cannot read either - // of those better versions. - // - // TODO(p): If NewSubfileType is ReducedImage, and it has SubIFDs compressed - // as old JPEG (6), decode JPEGInterchangeFormat/JPEGInterchangeFormatLength - // with libjpeg-turbo and insert them as the starting pages. - // - // This is not possible with libtiff directly, because TIFFSetSubDirectory() - // requires an ImageLength tag that's missing, and TIFFReadCustomDirectory() - // takes a privately defined struct that cannot be omitted. - // - // TODO(p): Samsung Android DNGs also claim to be TIFF/EP, but use a smaller - // uncompressed YCbCr image. Apple ProRAW uses the new JPEG Compression (7), - // with a weird Orientation. It also uses that value for its raw data. - uint32_t subtype = 0; - uint16_t subifd_count = 0; - const uint64_t *subifd_offsets = NULL; - if (TIFFGetField(tiff, TIFFTAG_SUBFILETYPE, &subtype) && - (subtype & FILETYPE_REDUCEDIMAGE) && - TIFFGetField(tiff, TIFFTAG_SUBIFD, &subifd_count, &subifd_offsets) && - subifd_count > 0 && subifd_offsets) { - } - do { // We inform about unsupported directories, but do not fail on them. GError *err = NULL; if (!try_append_page( - load_libtiff_directory(tiff, &err), &result, &result_tail)) { + load_libtiff_directory(tiff, &err), &result, &result_tail) && + err) { add_warning(ctx, "%s", err->message); g_error_free(err); } @@ -2606,7 +2832,7 @@ open_libtiff( fail: if (h.error) { - g_clear_pointer(&result, cairo_surface_destroy); + g_clear_pointer(&result, fiv_io_image_unref); set_error(error, h.error); g_free(h.error); } else if (!result) { @@ -2617,28 +2843,25 @@ fail: TIFFSetWarningHandlerExt(whe); TIFFSetErrorHandler(eh); TIFFSetWarningHandler(wh); - - // TODO(p): Colour management even for un/associated alpha channels. - // Note that TIFF has a number of fields that an ICC profile can be - // constructed from--it's not a good idea to blindly assume sRGB. - return fiv_io_profile_finalize(result, ctx->screen_profile); + return fiv_io_cmm_finish(ctx->cmm, result, ctx->screen_profile); } #endif // HAVE_LIBTIFF -------------------------------------------------------- #ifdef HAVE_GDKPIXBUF // ------------------------------------------------------ -static cairo_surface_t * +static FivIoImage * load_gdkpixbuf_argb32_unpremultiplied(GdkPixbuf *pixbuf) { int w = gdk_pixbuf_get_width(pixbuf); int h = gdk_pixbuf_get_height(pixbuf); - cairo_surface_t *surface = - cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); + FivIoImage *image = fiv_io_image_new(CAIRO_FORMAT_ARGB32, w, h); + if (!image) + return NULL; guint length = 0; guchar *src = gdk_pixbuf_get_pixels_with_length(pixbuf, &length); int src_stride = gdk_pixbuf_get_rowstride(pixbuf); - uint32_t *dst = (uint32_t *) cairo_image_surface_get_data(surface); + uint32_t *dst = (uint32_t *) image->data; for (int y = 0; y < h; y++) { const guchar *p = src + y * src_stride; for (int x = 0; x < w; x++) { @@ -2646,11 +2869,10 @@ load_gdkpixbuf_argb32_unpremultiplied(GdkPixbuf *pixbuf) p += 4; } } - cairo_surface_mark_dirty(surface); - return surface; + return image; } -static cairo_surface_t * +static FivIoImage * open_gdkpixbuf( const char *data, gsize len, const FivIoOpenContext *ctx, GError **error) { @@ -2668,16 +2890,33 @@ open_gdkpixbuf( gdk_pixbuf_get_n_channels(pixbuf) == 4 && gdk_pixbuf_get_bits_per_sample(pixbuf) == 8; - cairo_surface_t *surface = NULL; - if (custom_argb32) - surface = load_gdkpixbuf_argb32_unpremultiplied(pixbuf); - else - surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 1, NULL); - - cairo_status_t surface_status = cairo_surface_status(surface); - if (surface_status != CAIRO_STATUS_SUCCESS) { - set_error(error, cairo_status_to_string(surface_status)); + FivIoImage *image = NULL; + if (custom_argb32) { + image = load_gdkpixbuf_argb32_unpremultiplied(pixbuf); + } else if ((image = fiv_io_image_new(CAIRO_FORMAT_ARGB32, + gdk_pixbuf_get_width(pixbuf), gdk_pixbuf_get_height(pixbuf)))) { + // TODO(p): Ideally, don't go through Cairo at all. + cairo_surface_t *surface = fiv_io_image_to_surface_noref(image); + cairo_t *cr = cairo_create(surface); cairo_surface_destroy(surface); + + // Don't depend on GDK being initialized, to speed up thumbnailing + // (calling gdk_cairo_surface_create_from_pixbuf() would). + gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0); + cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); + cairo_paint(cr); + + // If the source was opaque, so will be the destination. + if (cairo_pattern_get_surface(cairo_get_source(cr), &surface) == + CAIRO_STATUS_SUCCESS) { + if (cairo_surface_get_content(surface) == CAIRO_CONTENT_COLOR) + image->format = CAIRO_FORMAT_RGB24; + } + cairo_destroy(cr); + } + + if (!image) { + set_error(error, "image allocation failure"); g_object_unref(pixbuf); return NULL; } @@ -2686,52 +2925,29 @@ open_gdkpixbuf( if (orientation && strlen(orientation) == 1) { int n = *orientation - '0'; if (n >= 1 && n <= 8) - cairo_surface_set_user_data( - surface, &fiv_io_key_orientation, (void *) (uintptr_t) n, NULL); + image->orientation = n; } const char *icc_profile = gdk_pixbuf_get_option(pixbuf, "icc-profile"); if (icc_profile) { gsize out_len = 0; guchar *raw = g_base64_decode(icc_profile, &out_len); - if (raw) { - cairo_surface_set_user_data(surface, &fiv_io_key_icc, - g_bytes_new_take(raw, out_len), - (cairo_destroy_func_t) g_bytes_unref); - } + if (raw) + image->icc = g_bytes_new_take(raw, out_len); } g_object_unref(pixbuf); - if (custom_argb32) { - fiv_io_profile_xrgb32_page(surface, ctx->screen_profile); - fiv_io_premultiply_argb32_page(surface); - } else { - surface = fiv_io_profile_finalize(surface, ctx->screen_profile); - } - return surface; + if (custom_argb32) + fiv_io_cmm_argb32_premultiply_page( + ctx->cmm, image, ctx->screen_profile); + else + image = fiv_io_cmm_finish(ctx->cmm, image, ctx->screen_profile); + return image; } #endif // HAVE_GDKPIXBUF ------------------------------------------------------ -// TODO(p): Check that all cairo_surface_set_user_data() calls succeed. -cairo_user_data_key_t fiv_io_key_exif; -cairo_user_data_key_t fiv_io_key_orientation; -cairo_user_data_key_t fiv_io_key_icc; -cairo_user_data_key_t fiv_io_key_xmp; -cairo_user_data_key_t fiv_io_key_thum; -cairo_user_data_key_t fiv_io_key_text; - -cairo_user_data_key_t fiv_io_key_frame_next; -cairo_user_data_key_t fiv_io_key_frame_previous; -cairo_user_data_key_t fiv_io_key_frame_duration; -cairo_user_data_key_t fiv_io_key_loops; - -cairo_user_data_key_t fiv_io_key_page_next; -cairo_user_data_key_t fiv_io_key_page_previous; - -cairo_user_data_key_t fiv_io_key_render; - -cairo_surface_t * +FivIoImage * fiv_io_open(const FivIoOpenContext *ctx, GError **error) { // TODO(p): Don't always load everything into memory, test type first, @@ -2752,56 +2968,69 @@ fiv_io_open(const FivIoOpenContext *ctx, GError **error) gchar *data = NULL; gsize len = 0; - if (!g_file_load_contents(file, NULL, &data, &len, NULL, error)) + gboolean success = + g_file_load_contents(file, NULL, &data, &len, NULL, error); + g_object_unref(file); + if (!success) return NULL; - cairo_surface_t *surface = fiv_io_open_from_data(data, len, ctx, error); + FivIoImage *image = fiv_io_open_from_data(data, len, ctx, error); g_free(data); - return surface; + return image; } -cairo_surface_t * +FivIoImage * fiv_io_open_from_data( const char *data, size_t len, const FivIoOpenContext *ctx, GError **error) { wuffs_base__slice_u8 prefix = wuffs_base__make_slice_u8((uint8_t *) data, len); - cairo_surface_t *surface = NULL; + FivIoImage *image = NULL; switch (wuffs_base__magic_number_guess_fourcc(prefix, true /* closed */)) { case WUFFS_BASE__FOURCC__BMP: // Note that BMP can redirect into another format, // which is so far unsupported here. - surface = open_wuffs_using( + image = open_wuffs_using( wuffs_bmp__decoder__alloc_as__wuffs_base__image_decoder, data, len, ctx, error); break; case WUFFS_BASE__FOURCC__GIF: - surface = open_wuffs_using( + image = open_wuffs_using( wuffs_gif__decoder__alloc_as__wuffs_base__image_decoder, data, len, ctx, error); break; case WUFFS_BASE__FOURCC__PNG: - surface = open_wuffs_using( + image = open_wuffs_using( wuffs_png__decoder__alloc_as__wuffs_base__image_decoder, data, len, ctx, error); break; case WUFFS_BASE__FOURCC__TGA: - surface = open_wuffs_using( + image = open_wuffs_using( wuffs_tga__decoder__alloc_as__wuffs_base__image_decoder, data, len, ctx, error); break; case WUFFS_BASE__FOURCC__JPEG: - surface = ctx->enhance - ? open_libjpeg_enhanced(data, len, ctx, error) - : open_libjpeg_turbo(data, len, ctx, error); + image = open_libjpeg_turbo(data, len, ctx, error); break; case WUFFS_BASE__FOURCC__WEBP: - surface = open_libwebp(data, len, ctx, error); + image = open_libwebp(data, len, ctx, error); break; default: + // Try to extract full-size previews from TIFF/EP-compatible raws, + // but allow for running the full render. +#ifdef HAVE_LIBRAW // --------------------------------------------------------- + if (!ctx->enhance) { +#endif // HAVE_LIBRAW --------------------------------------------------------- + if ((image = open_tiff_ep(data, len, ctx, error))) + break; + if (error) { + g_debug("%s", (*error)->message); + g_clear_error(error); + } #ifdef HAVE_LIBRAW // --------------------------------------------------------- - if ((surface = open_libraw(data, len, error))) + } + if ((image = open_libraw(data, len, ctx, error))) break; // TODO(p): We should try to pass actual processing errors through, @@ -2812,7 +3041,7 @@ fiv_io_open_from_data( } #endif // HAVE_LIBRAW --------------------------------------------------------- #ifdef HAVE_RESVG // ---------------------------------------------------------- - if ((surface = open_resvg(data, len, ctx, error))) + if ((image = open_resvg(data, len, ctx, error))) break; if (error) { g_debug("%s", (*error)->message); @@ -2820,7 +3049,7 @@ fiv_io_open_from_data( } #endif // HAVE_RESVG ---------------------------------------------------------- #ifdef HAVE_LIBRSVG // -------------------------------------------------------- - if ((surface = open_librsvg(data, len, ctx, error))) + if ((image = open_librsvg(data, len, ctx, error))) break; // XXX: It doesn't look like librsvg can return sensible errors. @@ -2830,7 +3059,7 @@ fiv_io_open_from_data( } #endif // HAVE_LIBRSVG -------------------------------------------------------- #ifdef HAVE_XCURSOR //--------------------------------------------------------- - if ((surface = open_xcursor(data, len, error))) + if ((image = open_xcursor(data, len, ctx, error))) break; if (error) { g_debug("%s", (*error)->message); @@ -2838,7 +3067,7 @@ fiv_io_open_from_data( } #endif // HAVE_XCURSOR -------------------------------------------------------- #ifdef HAVE_LIBHEIF //--------------------------------------------------------- - if ((surface = open_libheif(data, len, ctx, error))) + if ((image = open_libheif(data, len, ctx, error))) break; if (error) { g_debug("%s", (*error)->message); @@ -2847,7 +3076,7 @@ fiv_io_open_from_data( #endif // HAVE_LIBHEIF -------------------------------------------------------- #ifdef HAVE_LIBTIFF //--------------------------------------------------------- // This needs to be positioned after LibRaw. - if ((surface = open_libtiff(data, len, ctx, error))) + if ((image = open_libtiff(data, len, ctx, error))) break; if (error) { g_debug("%s", (*error)->message); @@ -2860,10 +3089,12 @@ fiv_io_open_from_data( #ifdef HAVE_GDKPIXBUF // ------------------------------------------------------ // This is used as a last resort, the rest above is special-cased. - if (!surface) { + if (!image) { GError *err = NULL; - if ((surface = open_gdkpixbuf(data, len, ctx, &err))) { + if ((image = open_gdkpixbuf(data, len, ctx, &err))) { g_clear_error(error); + } else if (!err) { + // Contrary to documentation, this is a possible outcome (libheif). } else if (err->code == GDK_PIXBUF_ERROR_UNKNOWN_TYPE) { g_error_free(err); } else { @@ -2876,17 +3107,13 @@ fiv_io_open_from_data( // gdk-pixbuf only gives out this single field--cater to its limitations, // since we'd really like to have it. // TODO(p): The Exif orientation should be ignored in JPEG-XL at minimum. - GBytes *exif = NULL; gsize exif_len = 0; gconstpointer exif_data = NULL; - if (surface && - (exif = cairo_surface_get_user_data(surface, &fiv_io_key_exif)) && - (exif_data = g_bytes_get_data(exif, &exif_len))) { - cairo_surface_set_user_data(surface, &fiv_io_key_orientation, - (void *) (uintptr_t) fiv_io_exif_orientation(exif_data, exif_len), - NULL); + if (image && image->exif && + (exif_data = g_bytes_get_data(image->exif, &exif_len))) { + image->orientation = fiv_io_exif_orientation(exif_data, exif_len); } - return surface; + return image; } // --- Thumbnail passing utilities --------------------------------------------- @@ -2956,380 +3183,112 @@ fiv_io_deserialize(GBytes *bytes, guint64 *user_data) return surface; } -// --- Filesystem -------------------------------------------------------------- - -#include "xdg.h" - -static void -model_entry_finalize(FivIoModelEntry *entry) -{ - g_free(entry->uri); - g_free(entry->target_uri); - g_free(entry->collate_key); -} - -struct _FivIoModel { - GObject parent_instance; - GPatternSpec **supported_patterns; - - GFile *directory; ///< Currently loaded directory - GFileMonitor *monitor; ///< "directory" monitoring - GArray *subdirs; ///< "directory" contents - GArray *files; ///< "directory" contents - - FivIoModelSort sort_field; ///< How to sort - gboolean sort_descending; ///< Whether to sort in reverse - gboolean filtering; ///< Only show non-hidden, supported -}; - -G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT) - -enum { - PROP_FILTERING = 1, - PROP_SORT_FIELD, - PROP_SORT_DESCENDING, - N_PROPERTIES -}; - -static GParamSpec *model_properties[N_PROPERTIES]; - -enum { - FILES_CHANGED, - SUBDIRECTORIES_CHANGED, - LAST_SIGNAL, -}; - -// Globals are, sadly, the canonical way of storing signal numbers. -static guint model_signals[LAST_SIGNAL]; - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static gboolean -model_supports(FivIoModel *self, const char *filename) -{ - gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL); - if (!utf8) - return FALSE; - - gchar *lc = g_utf8_strdown(utf8, -1); - gsize lc_length = strlen(lc); - gchar *reversed = g_utf8_strreverse(lc, lc_length); - g_free(utf8); - - // fnmatch() uses the /locale encoding/, and isn't present on Windows. - // TODO(p): Consider using g_file_info_get_display_name() for direct UTF-8. - gboolean result = FALSE; - for (GPatternSpec **p = self->supported_patterns; *p; p++) - if ((result = g_pattern_spec_match(*p, lc_length, lc, reversed))) - break; - - g_free(lc); - g_free(reversed); - return result; -} - -static inline int -model_compare_entries(FivIoModel *self, - const FivIoModelEntry *entry1, GFile *file1, - const FivIoModelEntry *entry2, GFile *file2) -{ - if (g_file_has_prefix(file1, file2)) - return +1; - if (g_file_has_prefix(file2, file1)) - return -1; - - int result = 0; - switch (self->sort_field) { - case FIV_IO_MODEL_SORT_MTIME: - result -= entry1->mtime_msec < entry2->mtime_msec; - result += entry1->mtime_msec > entry2->mtime_msec; - if (result != 0) - break; - - // Fall-through - case FIV_IO_MODEL_SORT_NAME: - case FIV_IO_MODEL_SORT_COUNT: - result = strcmp(entry1->collate_key, entry2->collate_key); - } - return self->sort_descending ? -result : +result; -} - -static gint -model_compare(gconstpointer a, gconstpointer b, gpointer user_data) +static cairo_status_t +write_to_byte_array( + void *closure, const unsigned char *data, unsigned int length) { - const FivIoModelEntry *entry1 = a; - const FivIoModelEntry *entry2 = b; - GFile *file1 = g_file_new_for_uri(entry1->uri); - GFile *file2 = g_file_new_for_uri(entry2->uri); - int result = model_compare_entries(user_data, entry1, file1, entry2, file2); - g_object_unref(file1); - g_object_unref(file2); - return result; + g_byte_array_append(closure, data, length); + return CAIRO_STATUS_SUCCESS; } -static void -model_resort(FivIoModel *self) +GBytes * +fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error) { - g_array_sort_with_data(self->subdirs, model_compare, self); - g_array_sort_with_data(self->files, model_compare, self); - - g_signal_emit(self, model_signals[FILES_CHANGED], 0); - g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0); -} - -static gboolean -model_reload(FivIoModel *self, GError **error) -{ - g_array_set_size(self->subdirs, 0); - g_array_set_size(self->files, 0); - - GFileEnumerator *enumerator = g_file_enumerate_children(self->directory, - G_FILE_ATTRIBUTE_STANDARD_TYPE "," - G_FILE_ATTRIBUTE_STANDARD_NAME "," - G_FILE_ATTRIBUTE_STANDARD_TARGET_URI "," - G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN "," - G_FILE_ATTRIBUTE_TIME_MODIFIED "," - G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC, - G_FILE_QUERY_INFO_NONE, NULL, error); - if (!enumerator) { - // Note that this has had a side-effect of clearing all entries. - g_signal_emit(self, model_signals[FILES_CHANGED], 0); - g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED], 0); - return FALSE; - } - - GFileInfo *info = NULL; - GFile *child = NULL; - GError *e = NULL; - while (TRUE) { - if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, &e) && - e) { - g_warning("%s", e->message); - g_clear_error(&e); - continue; - } - - if (!info) - break; - if (self->filtering && g_file_info_get_is_hidden(info)) - continue; + g_return_val_if_fail( + surface && cairo_surface_get_type(surface) == CAIRO_SURFACE_TYPE_IMAGE, + NULL); - FivIoModelEntry entry = {.uri = g_file_get_uri(child), - .target_uri = g_strdup(g_file_info_get_attribute_string( - info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI))}; - GDateTime *mtime = g_file_info_get_modification_date_time(info); - if (mtime) { - entry.mtime_msec = g_date_time_to_unix(mtime) * 1000 + - g_date_time_get_microsecond(mtime) / 1000; - g_date_time_unref(mtime); + cairo_format_t format = cairo_image_surface_get_format(surface); + if (format == CAIRO_FORMAT_ARGB32) { + const uint32_t *data = + (const uint32_t *) cairo_image_surface_get_data(surface); + + bool all_solid = true; + for (size_t len = cairo_image_surface_get_width(surface) * + cairo_image_surface_get_height(surface); len--; ) { + if ((data[len] >> 24) != 0xFF) + all_solid = false; } + if (all_solid) + format = CAIRO_FORMAT_RGB24; + } + + if (format != CAIRO_FORMAT_RGB24) { +#if CAIRO_HAS_PNG_FUNCTIONS + GByteArray *ba = g_byte_array_new(); + cairo_status_t status = + cairo_surface_write_to_png_stream(surface, write_to_byte_array, ba); + if (status == CAIRO_STATUS_SUCCESS) + return g_byte_array_free_to_bytes(ba); + g_byte_array_unref(ba); +#endif - gchar *parse_name = g_file_get_parse_name(child); - // TODO(p): Make it possible to use g_utf8_collate_key() instead, - // which does not use natural sorting. - entry.collate_key = g_utf8_collate_key_for_filename(parse_name, -1); - g_free(parse_name); - - const char *name = g_file_info_get_name(info); - if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY) - g_array_append_val(self->subdirs, entry); - else if (!self->filtering || model_supports(self, name)) - g_array_append_val(self->files, entry); - else - model_entry_finalize(&entry); + // Last resort: remove transparency by painting over black. + cairo_surface_t *converted = + cairo_image_surface_create(CAIRO_FORMAT_RGB24, + cairo_image_surface_get_width(surface), + cairo_image_surface_get_height(surface)); + cairo_t *cr = cairo_create(converted); + cairo_set_source_surface(cr, surface, 0, 0); + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + cairo_paint(cr); + cairo_destroy(cr); + GBytes *result = fiv_io_serialize_for_search(converted, error); + cairo_surface_destroy(converted); + return result; } - g_object_unref(enumerator); - - // We also emit change signals there, indirectly. - model_resort(self); - return TRUE; -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -fiv_io_model_finalize(GObject *gobject) -{ - FivIoModel *self = FIV_IO_MODEL(gobject); - for (GPatternSpec **p = self->supported_patterns; *p; p++) - g_pattern_spec_free(*p); - g_free(self->supported_patterns); - - g_clear_object(&self->directory); - g_clear_object(&self->monitor); - g_array_free(self->subdirs, TRUE); - g_array_free(self->files, TRUE); - - G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject); -} -static void -fiv_io_model_get_property( - GObject *object, guint property_id, GValue *value, GParamSpec *pspec) -{ - FivIoModel *self = FIV_IO_MODEL(object); - switch (property_id) { - case PROP_FILTERING: - g_value_set_boolean(value, self->filtering); - break; - case PROP_SORT_FIELD: - g_value_set_int(value, self->sort_field); - break; - case PROP_SORT_DESCENDING: - g_value_set_boolean(value, self->sort_descending); - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + tjhandle enc = tjInitCompress(); + if (!enc) { + set_error(error, tjGetErrorStr2(enc)); + return NULL; } -} -static void -fiv_io_model_set_property( - GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) -{ - FivIoModel *self = FIV_IO_MODEL(object); - switch (property_id) { - case PROP_FILTERING: - if (self->filtering != g_value_get_boolean(value)) { - self->filtering = !self->filtering; - g_object_notify_by_pspec(object, model_properties[property_id]); - (void) model_reload(self, NULL /* error */); - } - break; - case PROP_SORT_FIELD: - if ((int) self->sort_field != g_value_get_int(value)) { - self->sort_field = g_value_get_int(value); - g_object_notify_by_pspec(object, model_properties[property_id]); - model_resort(self); - } - break; - case PROP_SORT_DESCENDING: - if (self->sort_descending != g_value_get_boolean(value)) { - self->sort_descending = !self->sort_descending; - g_object_notify_by_pspec(object, model_properties[property_id]); - model_resort(self); - } - break; - default: - G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + unsigned char *jpeg = NULL; + unsigned long length = 0; + if (tjCompress2(enc, cairo_image_surface_get_data(surface), + cairo_image_surface_get_width(surface), + cairo_image_surface_get_stride(surface), + cairo_image_surface_get_height(surface), + (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TJPF_BGRX : TJPF_XRGB), + &jpeg, &length, TJSAMP_444, 90, 0)) { + set_error(error, tjGetErrorStr2(enc)); + tjFree(jpeg); + tjDestroy(enc); + return NULL; } -} -static void -fiv_io_model_class_init(FivIoModelClass *klass) -{ - GObjectClass *object_class = G_OBJECT_CLASS(klass); - object_class->get_property = fiv_io_model_get_property; - object_class->set_property = fiv_io_model_set_property; - object_class->finalize = fiv_io_model_finalize; - - model_properties[PROP_FILTERING] = g_param_spec_boolean( - "filtering", "Filtering", "Only show non-hidden, supported entries", - TRUE, G_PARAM_READWRITE); - // TODO(p): GObject enumerations are annoying, but this should be one. - model_properties[PROP_SORT_FIELD] = g_param_spec_int( - "sort-field", "Sort field", "Sort order", - FIV_IO_MODEL_SORT_MIN, FIV_IO_MODEL_SORT_MAX, - FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE); - model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean( - "sort-descending", "Sort descending", "Use reverse sort order", - FALSE, G_PARAM_READWRITE); - g_object_class_install_properties( - object_class, N_PROPERTIES, model_properties); - - // TODO(p): Arguments something like: index, added, removed. - model_signals[FILES_CHANGED] = - g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0, - NULL, NULL, NULL, G_TYPE_NONE, 0); - model_signals[SUBDIRECTORIES_CHANGED] = - g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0, - NULL, NULL, NULL, G_TYPE_NONE, 0); -} - -// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -static void -fiv_io_model_init(FivIoModel *self) -{ - self->filtering = TRUE; - - char **types = fiv_io_all_supported_media_types(); - char **globs = extract_mime_globs((const char **) types); - g_strfreev(types); - - gsize n = g_strv_length(globs); - self->supported_patterns = - g_malloc0_n(n + 1, sizeof *self->supported_patterns); - while (n--) - self->supported_patterns[n] = g_pattern_spec_new(globs[n]); - g_strfreev(globs); - - self->files = g_array_new(FALSE, TRUE, sizeof(FivIoModelEntry)); - self->subdirs = g_array_new(FALSE, TRUE, sizeof(FivIoModelEntry)); - g_array_set_clear_func( - self->subdirs, (GDestroyNotify) model_entry_finalize); - g_array_set_clear_func( - self->files, (GDestroyNotify) model_entry_finalize); -} - -gboolean -fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error) -{ - g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE); - g_return_val_if_fail(G_IS_FILE(directory), FALSE); - - g_clear_object(&self->directory); - g_clear_object(&self->monitor); - self->directory = g_object_ref(directory); - - // TODO(p): Process the ::changed signal. - self->monitor = g_file_monitor_directory( - directory, G_FILE_MONITOR_WATCH_MOVES, NULL, NULL /* error */); - return model_reload(self, error); -} - -GFile * -fiv_io_model_get_location(FivIoModel *self) -{ - g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL); - return self->directory; -} - -const FivIoModelEntry * -fiv_io_model_get_files(FivIoModel *self, gsize *len) -{ - *len = self->files->len; - return (const FivIoModelEntry *) self->files->data; -} - -const FivIoModelEntry * -fiv_io_model_get_subdirs(FivIoModel *self, gsize *len) -{ - *len = self->subdirs->len; - return (const FivIoModelEntry *) self->subdirs->data; + tjDestroy(enc); + return g_bytes_new_with_free_func( + jpeg, length, (GDestroyNotify) tjFree, jpeg); } // --- Export ------------------------------------------------------------------ unsigned char * fiv_io_encode_webp( - cairo_surface_t *surface, const WebPConfig *config, size_t *len) + FivIoImage *image, const WebPConfig *config, size_t *len) { - cairo_format_t format = cairo_image_surface_get_format(surface); - int w = cairo_image_surface_get_width(surface); - int h = cairo_image_surface_get_height(surface); - if (format != CAIRO_FORMAT_ARGB32 && - format != CAIRO_FORMAT_RGB24) { - cairo_surface_t *converted = - cairo_image_surface_create((format = CAIRO_FORMAT_ARGB32), w, h); - cairo_t *cr = cairo_create(converted); + if (image->format != CAIRO_FORMAT_ARGB32 && + image->format != CAIRO_FORMAT_RGB24) { + FivIoImage *converted = + fiv_io_image_new(CAIRO_FORMAT_ARGB32, image->width, image->height); + + cairo_surface_t *surface = fiv_io_image_to_surface_noref(converted); + cairo_t *cr = cairo_create(surface); + cairo_surface_destroy(surface); + + surface = fiv_io_image_to_surface_noref(image); cairo_set_source_surface(cr, surface, 0, 0); + cairo_surface_destroy(surface); cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); cairo_paint(cr); cairo_destroy(cr); - surface = converted; + image = converted; } else { - surface = cairo_surface_reference(surface); + image = fiv_io_image_ref(image); } WebPMemoryWriter writer = {}; @@ -3339,27 +3298,26 @@ fiv_io_encode_webp( goto fail; picture.use_argb = true; - picture.width = w; - picture.height = h; + picture.width = image->width; + picture.height = image->height; if (!WebPPictureAlloc(&picture)) goto fail; // Cairo uses a similar internal format, so we should be able to // copy it over and fix up the minor differences. // This is written to be easy to follow rather than fast. - int stride = cairo_image_surface_get_stride(surface); - if (picture.argb_stride != w || - picture.argb_stride * (int) sizeof *picture.argb != stride || - INT_MAX / picture.argb_stride < h) + if (picture.argb_stride != (int) image->width || + picture.argb_stride * sizeof *picture.argb != image->stride || + UINT32_MAX / picture.argb_stride < image->height) goto fail_compatibility; uint32_t *argb = - memcpy(picture.argb, cairo_image_surface_get_data(surface), stride * h); - if (format == CAIRO_FORMAT_ARGB32) - for (int i = h * picture.argb_stride; i-- > 0; argb++) + memcpy(picture.argb, image->data, image->stride * image->height); + if (image->format == CAIRO_FORMAT_ARGB32) + for (int i = image->height * picture.argb_stride; i-- > 0; argb++) *argb = wuffs_base__color_u32_argb_premul__as__color_u32_argb_nonpremul(*argb); else - for (int i = h * picture.argb_stride; i-- > 0; argb++) + for (int i = image->height * picture.argb_stride; i-- > 0; argb++) *argb |= 0xFF000000; // TODO(p): Prevent or propagate VP8_ENC_ERROR_BAD_DIMENSION. @@ -3371,13 +3329,13 @@ fiv_io_encode_webp( fail_compatibility: WebPPictureFree(&picture); fail: - cairo_surface_destroy(surface); + fiv_io_image_unref(image); *len = writer.size; return writer.mem; } static WebPData -encode_lossless_webp(cairo_surface_t *surface) +encode_lossless_webp(FivIoImage *image) { WebPData bitstream = {}; WebPConfig config = {}; @@ -3388,12 +3346,12 @@ encode_lossless_webp(cairo_surface_t *surface) if (!WebPValidateConfig(&config)) return bitstream; - bitstream.bytes = fiv_io_encode_webp(surface, &config, &bitstream.size); + bitstream.bytes = fiv_io_encode_webp(image, &config, &bitstream.size); return bitstream; } static gboolean -encode_webp_image(WebPMux *mux, cairo_surface_t *frame) +encode_webp_image(WebPMux *mux, FivIoImage *frame) { WebPData bitstream = encode_lossless_webp(frame); gboolean ok = WebPMuxSetImage(mux, &bitstream, true) == WEBP_MUX_OK; @@ -3402,15 +3360,13 @@ encode_webp_image(WebPMux *mux, cairo_surface_t *frame) } static gboolean -encode_webp_animation(WebPMux *mux, cairo_surface_t *page) +encode_webp_animation(WebPMux *mux, FivIoImage *page) { gboolean ok = TRUE; - for (cairo_surface_t *frame = page; ok && frame; frame = - cairo_surface_get_user_data(frame, &fiv_io_key_frame_next)) { + for (FivIoImage *frame = page; ok && frame; frame = frame->frame_next) { WebPMuxFrameInfo info = { .bitstream = encode_lossless_webp(frame), - .duration = (intptr_t) cairo_surface_get_user_data( - frame, &fiv_io_key_frame_duration), + .duration = frame->frame_duration, .id = WEBP_CHUNK_ANMF, .dispose_method = WEBP_MUX_DISPOSE_NONE, .blend_method = WEBP_MUX_NO_BLEND, @@ -3420,8 +3376,7 @@ encode_webp_animation(WebPMux *mux, cairo_surface_t *page) } WebPMuxAnimParams params = { .bgcolor = 0x00000000, // BGRA, curiously. - .loop_count = (uintptr_t) - cairo_surface_get_user_data(page, &fiv_io_key_loops), + .loop_count = page->loops, }; return ok && WebPMuxSetAnimationParams(mux, ¶ms) == WEBP_MUX_OK; } @@ -3439,7 +3394,7 @@ set_metadata(WebPMux *mux, const char *fourcc, GBytes *data) } gboolean -fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, FivIoProfile target, +fiv_io_save(FivIoImage *page, FivIoImage *frame, FivIoProfile *target, const char *path, GError **error) { g_return_val_if_fail(page != NULL, FALSE); @@ -3449,17 +3404,14 @@ fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, FivIoProfile target, WebPMux *mux = WebPMuxNew(); if (frame) ok = encode_webp_image(mux, frame); - else if (!cairo_surface_get_user_data(page, &fiv_io_key_frame_next)) + else if (!page->frame_next) ok = encode_webp_image(mux, page); else ok = encode_webp_animation(mux, page); - ok = ok && set_metadata(mux, "EXIF", - cairo_surface_get_user_data(page, &fiv_io_key_exif)); - ok = ok && set_metadata(mux, "ICCP", - cairo_surface_get_user_data(page, &fiv_io_key_icc)); - ok = ok && set_metadata(mux, "XMP ", - cairo_surface_get_user_data(page, &fiv_io_key_xmp)); + ok = ok && set_metadata(mux, "EXIF", page->exif); + ok = ok && set_metadata(mux, "ICCP", page->icc); + ok = ok && set_metadata(mux, "XMP ", page->xmp); GBytes *iccp = NULL; if (ok && target && (iccp = fiv_io_profile_to_bytes(target))) @@ -3484,71 +3436,62 @@ fiv_io_save(cairo_surface_t *page, cairo_surface_t *frame, FivIoProfile target, // --- Metadata ---------------------------------------------------------------- void -fiv_io_orientation_dimensions(cairo_surface_t *surface, - FivIoOrientation orientation, double *w, double *h) -{ - cairo_rectangle_t extents = {}; - switch (cairo_surface_get_type(surface)) { - case CAIRO_SURFACE_TYPE_IMAGE: - extents.width = cairo_image_surface_get_width(surface); - extents.height = cairo_image_surface_get_height(surface); - break; - case CAIRO_SURFACE_TYPE_RECORDING: - if (!cairo_recording_surface_get_extents(surface, &extents)) - cairo_recording_surface_ink_extents(surface, - &extents.x, &extents.y, &extents.width, &extents.height); - break; - default: - g_assert_not_reached(); - } - +fiv_io_orientation_dimensions( + const FivIoImage *image, FivIoOrientation orientation, double *w, double *h) +{ switch (orientation) { case FivIoOrientation90: case FivIoOrientationMirror90: case FivIoOrientation270: case FivIoOrientationMirror270: - *w = extents.height; - *h = extents.width; + *w = image->height; + *h = image->width; break; default: - *w = extents.width; - *h = extents.height; + *w = image->width; + *h = image->height; } } cairo_matrix_t -fiv_io_orientation_apply(cairo_surface_t *surface, +fiv_io_orientation_apply(const FivIoImage *image, FivIoOrientation orientation, double *width, double *height) { - fiv_io_orientation_dimensions(surface, orientation, width, height); + fiv_io_orientation_dimensions(image, orientation, width, height); + return fiv_io_orientation_matrix(orientation, *width, *height); +} +cairo_matrix_t +fiv_io_orientation_matrix( + FivIoOrientation orientation, double width, double height) +{ cairo_matrix_t matrix = {}; cairo_matrix_init_identity(&matrix); switch (orientation) { case FivIoOrientation90: cairo_matrix_rotate(&matrix, -M_PI_2); - cairo_matrix_translate(&matrix, -*width, 0); + cairo_matrix_translate(&matrix, -width, 0); break; case FivIoOrientation180: cairo_matrix_scale(&matrix, -1, -1); - cairo_matrix_translate(&matrix, -*width, -*height); + cairo_matrix_translate(&matrix, -width, -height); break; case FivIoOrientation270: cairo_matrix_rotate(&matrix, +M_PI_2); - cairo_matrix_translate(&matrix, 0, -*height); + cairo_matrix_translate(&matrix, 0, -height); break; case FivIoOrientationMirror0: cairo_matrix_scale(&matrix, -1, +1); - cairo_matrix_translate(&matrix, -*width, 0); + cairo_matrix_translate(&matrix, -width, 0); break; case FivIoOrientationMirror90: cairo_matrix_rotate(&matrix, +M_PI_2); cairo_matrix_scale(&matrix, -1, +1); - cairo_matrix_translate(&matrix, -*width, -*height); + cairo_matrix_translate(&matrix, -width, -height); break; case FivIoOrientationMirror180: cairo_matrix_scale(&matrix, +1, -1); - cairo_matrix_translate(&matrix, 0, -*height); + cairo_matrix_translate(&matrix, 0, -height); break; case FivIoOrientationMirror270: cairo_matrix_rotate(&matrix, -M_PI_2); @@ -3562,49 +3505,28 @@ fiv_io_orientation_apply(cairo_surface_t *surface, FivIoOrientation fiv_io_exif_orientation(const guint8 *tiff, gsize len) { - // libtiff also knows how to do this, but it's not a lot of code. // The "Orientation" tag/field is part of Baseline TIFF 6.0 (1992), // it just so happens that Exif is derived from this format. // There is no other meaningful placement for this than right in IFD0, // describing the main image. - const uint8_t *end = tiff + len, - le[4] = {'I', 'I', 42, 0}, - be[4] = {'M', 'M', 0, 42}; - - uint16_t (*u16)(const uint8_t *) = NULL; - uint32_t (*u32)(const uint8_t *) = NULL; - if (tiff + 8 > end) { - return FivIoOrientationUnknown; - } else if (!memcmp(tiff, le, sizeof le)) { - u16 = wuffs_base__peek_u16le__no_bounds_check; - u32 = wuffs_base__peek_u32le__no_bounds_check; - } else if (!memcmp(tiff, be, sizeof be)) { - u16 = wuffs_base__peek_u16be__no_bounds_check; - u32 = wuffs_base__peek_u32be__no_bounds_check; - } else { - return FivIoOrientationUnknown; - } - - const uint8_t *ifd0 = tiff + u32(tiff + 4); - if (ifd0 + 2 > end) + struct tiffer T = {}; + if (!tiffer_init(&T, tiff, len) || !tiffer_next_ifd(&T)) return FivIoOrientationUnknown; - uint16_t fields = u16(ifd0); - enum { BYTE = 1, ASCII, SHORT, LONG, RATIONAL, - SBYTE, UNDEFINED, SSHORT, SLONG, SRATIONAL, FLOAT, DOUBLE }; - enum { Orientation = 274 }; - for (const guint8 *p = ifd0 + 2; fields-- && p + 12 <= end; p += 12) { - uint16_t tag = u16(p), type = u16(p + 2), value16 = u16(p + 8); - uint32_t count = u32(p + 4); - if (G_UNLIKELY(tag == Orientation && type == SHORT && count == 1 && - value16 >= 1 && value16 <= 8)) - return value16; + struct tiffer_entry entry = {}; + while (tiffer_next_entry(&T, &entry)) { + int64_t orientation = 0; + if (G_UNLIKELY(entry.tag == TIFF_Orientation) && + entry.type == TIFFER_SHORT && entry.remaining_count == 1 && + tiffer_integer(&T, &entry, &orientation) && + orientation >= 1 && orientation <= 8) + return orientation; } return FivIoOrientationUnknown; } gboolean -fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error) +fiv_io_save_metadata(const FivIoImage *page, const char *path, GError **error) { g_return_val_if_fail(page != NULL, FALSE); @@ -3619,14 +3541,12 @@ fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error) // (standalone) with trailing nonsense. fprintf(fp, "\xFF\001Exiv2"); - GBytes *data = NULL; gsize len = 0; gconstpointer p = NULL; // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 // I don't care if Exiv2 supports it this way. - if ((data = cairo_surface_get_user_data(page, &fiv_io_key_exif)) && - (p = g_bytes_get_data(data, &len))) { + if (page->exif && (p = g_bytes_get_data(page->exif, &len))) { while (len) { gsize chunk = MIN(len, 0xFFFF - 2 - 6); uint8_t header[10] = "\xFF\xE1\000\000Exif\000\000"; @@ -3642,8 +3562,7 @@ fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error) } // https://www.color.org/specification/ICC1v43_2010-12.pdf B.4 - if ((data = cairo_surface_get_user_data(page, &fiv_io_key_icc)) && - (p = g_bytes_get_data(data, &len))) { + if (page->icc && (p = g_bytes_get_data(page->icc, &len))) { gsize limit = 0xFFFF - 2 - 12; uint8_t current = 0, total = (len + limit - 1) / limit; while (len) { @@ -3665,8 +3584,7 @@ fiv_io_save_metadata(cairo_surface_t *page, const char *path, GError **error) // Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 // If the main segment overflows, then it's a sign of bad luck, // because 1.1.3.1 is way too complex. - if ((data = cairo_surface_get_user_data(page, &fiv_io_key_xmp)) && - (p = g_bytes_get_data(data, &len))) { + if (page->xmp && (p = g_bytes_get_data(page->xmp, &len))) { while (len) { gsize chunk = MIN(len, 0xFFFF - 2 - 29); uint8_t header[33] = |