| /* Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. | |
| SPDX-License-Identifier: BSD-2-Clause | |
| */ | |
| #include <avif/avif.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #define GDK_PIXBUF_ENABLE_BACKEND | |
| #include <gdk-pixbuf/gdk-pixbuf-io.h> | |
| #include <gdk-pixbuf/gdk-pixbuf.h> | |
| G_MODULE_EXPORT void fill_vtable (GdkPixbufModule *module); | |
| G_MODULE_EXPORT void fill_info (GdkPixbufFormat *info); | |
| struct avif_context | |
| { | |
| GdkPixbuf *pixbuf; | |
| GdkPixbufModuleSizeFunc size_func; | |
| GdkPixbufModuleUpdatedFunc updated_func; | |
| GdkPixbufModulePreparedFunc prepared_func; | |
| gpointer user_data; | |
| avifDecoder *decoder; | |
| GByteArray *data; | |
| GBytes *bytes; | |
| }; | |
| static void | |
| avif_context_free (struct avif_context *context) | |
| { | |
| if (!context) | |
| return; | |
| if (context->decoder) { | |
| avifDecoderDestroy (context->decoder); | |
| context->decoder = NULL; | |
| } | |
| if (context->data) { | |
| g_byte_array_unref (context->data); | |
| context->bytes = NULL; | |
| } | |
| if (context->bytes) { | |
| g_bytes_unref (context->bytes); | |
| context->bytes = NULL; | |
| } | |
| if (context->pixbuf) { | |
| g_object_unref (context->pixbuf); | |
| context->pixbuf = NULL; | |
| } | |
| g_free (context); | |
| } | |
| static gboolean | |
| avif_context_try_load (struct avif_context *context, GError **error) | |
| { | |
| avifResult ret; | |
| avifDecoder *decoder = context->decoder; | |
| avifImage *image; | |
| avifRGBImage rgb; | |
| const uint8_t *data; | |
| size_t size; | |
| int width, height; | |
| GdkPixbuf *output; | |
| data = g_bytes_get_data (context->bytes, &size); | |
| ret = avifDecoderSetIOMemory (decoder, data, size); | |
| if (ret != AVIF_RESULT_OK) { | |
| g_set_error (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE, | |
| "Couldn't decode image: %s", avifResultToString (ret)); | |
| return FALSE; | |
| } | |
| ret = avifDecoderParse (decoder); | |
| if (ret != AVIF_RESULT_OK) { | |
| g_set_error (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE, | |
| "Couldn't decode image: %s", avifResultToString (ret)); | |
| return FALSE; | |
| } | |
| if (decoder->imageCount > 1) { | |
| g_set_error_literal (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, | |
| "Image sequences not yet implemented"); | |
| return FALSE; | |
| } | |
| ret = avifDecoderNextImage (decoder); | |
| if (ret == AVIF_RESULT_NO_IMAGES_REMAINING) { | |
| /* No more images, bail out. Verify that you got the expected amount of images decoded. */ | |
| return TRUE; | |
| } else if (ret != AVIF_RESULT_OK) { | |
| g_set_error (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, | |
| "Failed to decode all frames: %s", avifResultToString (ret)); | |
| return FALSE; | |
| } | |
| image = decoder->image; | |
| width = image->width; | |
| height = image->height; | |
| avifRGBImageSetDefaults (&rgb, image); | |
| rgb.depth = 8; | |
| if (image->alphaPlane) { | |
| rgb.format = AVIF_RGB_FORMAT_RGBA; | |
| output = gdk_pixbuf_new (GDK_COLORSPACE_RGB, | |
| TRUE, 8, width, height); | |
| } else { | |
| rgb.format = AVIF_RGB_FORMAT_RGB; | |
| output = gdk_pixbuf_new (GDK_COLORSPACE_RGB, | |
| FALSE, 8, width, height); | |
| } | |
| if (output == NULL) { | |
| g_set_error_literal (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, | |
| "Insufficient memory to open AVIF file"); | |
| return FALSE; | |
| } | |
| rgb.pixels = gdk_pixbuf_get_pixels (output); | |
| rgb.rowBytes = gdk_pixbuf_get_rowstride (output); | |
| ret = avifImageYUVToRGB (image, &rgb); | |
| if (ret != AVIF_RESULT_OK) { | |
| g_set_error (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, | |
| "Failed to convert YUV to RGB: %s", avifResultToString (ret)); | |
| g_object_unref (output); | |
| return FALSE; | |
| } | |
| /* transformations */ | |
| if (image->transformFlags & AVIF_TRANSFORM_CLAP) { | |
| if ((image->clap.widthD > 0) && (image->clap.heightD > 0) && (image->clap.horizOffD > 0) && (image->clap.vertOffD > 0)) { | |
| int new_width, new_height; | |
| new_width = (int) ((double) (image->clap.widthN) / (image->clap.widthD) + 0.5); | |
| if (new_width > width) { | |
| new_width = width; | |
| } | |
| new_height = (int) ((double) (image->clap.heightN) / (image->clap.heightD) + 0.5); | |
| if (new_height > height) { | |
| new_height = height; | |
| } | |
| if (new_width > 0 && new_height > 0) { | |
| int offx, offy; | |
| GdkPixbuf *output_cropped; | |
| GdkPixbuf *cropped_copy; | |
| offx = ((double) ((int32_t) image->clap.horizOffN)) / (image->clap.horizOffD) + (width - new_width) / 2.0 + 0.5; | |
| if (offx < 0) { | |
| offx = 0; | |
| } else if (offx > (width - new_width)) { | |
| offx = width - new_width; | |
| } | |
| offy = ((double) ((int32_t) image->clap.vertOffN)) / (image->clap.vertOffD) + (height - new_height) / 2.0 + 0.5; | |
| if (offy < 0) { | |
| offy = 0; | |
| } else if (offy > (height - new_height)) { | |
| offy = height - new_height; | |
| } | |
| output_cropped = gdk_pixbuf_new_subpixbuf (output, offx, offy, new_width, new_height); | |
| cropped_copy = gdk_pixbuf_copy (output_cropped); | |
| g_clear_object (&output_cropped); | |
| if (cropped_copy) { | |
| g_object_unref (output); | |
| output = cropped_copy; | |
| } | |
| } | |
| } else { | |
| /* Zero values, we need to avoid 0 divide. */ | |
| g_warning ("Wrong values in avifCleanApertureBox\n"); | |
| } | |
| } | |
| if (image->transformFlags & AVIF_TRANSFORM_IROT) { | |
| GdkPixbuf *output_rotated = NULL; | |
| switch (image->irot.angle) { | |
| case 1: | |
| output_rotated = gdk_pixbuf_rotate_simple (output, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE); | |
| break; | |
| case 2: | |
| output_rotated = gdk_pixbuf_rotate_simple (output, GDK_PIXBUF_ROTATE_UPSIDEDOWN); | |
| break; | |
| case 3: | |
| output_rotated = gdk_pixbuf_rotate_simple (output, GDK_PIXBUF_ROTATE_CLOCKWISE); | |
| break; | |
| } | |
| if (output_rotated) { | |
| g_object_unref (output); | |
| output = output_rotated; | |
| } | |
| } | |
| if (image->transformFlags & AVIF_TRANSFORM_IMIR) { | |
| GdkPixbuf *output_mirrored = NULL; | |
| switch (image->imir.axis) { | |
| case 0: | |
| output_mirrored = gdk_pixbuf_flip (output, FALSE); | |
| break; | |
| case 1: | |
| output_mirrored = gdk_pixbuf_flip (output, TRUE); | |
| break; | |
| } | |
| if (output_mirrored) { | |
| g_object_unref (output); | |
| output = output_mirrored; | |
| } | |
| } | |
| /* width, height could be different after applied transformations */ | |
| width = gdk_pixbuf_get_width (output); | |
| height = gdk_pixbuf_get_height (output); | |
| if (context->size_func) { | |
| (*context->size_func) (&width, &height, context->user_data); | |
| } | |
| if (width == 0 || height == 0) { | |
| g_set_error_literal (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_CORRUPT_IMAGE, | |
| "Transformed AVIF has zero width or height"); | |
| g_object_unref (output); | |
| return FALSE; | |
| } | |
| if (width < gdk_pixbuf_get_width (output) || height < gdk_pixbuf_get_height (output)) { | |
| GdkPixbuf *output_scaled = NULL; | |
| output_scaled = gdk_pixbuf_scale_simple (output, width, height, GDK_INTERP_HYPER); | |
| if (output_scaled) { | |
| g_object_unref (output); | |
| output = output_scaled; | |
| } | |
| } | |
| if (image->icc.size != 0) { | |
| gchar *icc_base64 = g_base64_encode ((const guchar *) image->icc.data, image->icc.size); | |
| gdk_pixbuf_set_option (output, "icc-profile", icc_base64); | |
| g_free (icc_base64); | |
| } | |
| if (context->pixbuf) { | |
| g_object_unref (context->pixbuf); | |
| context->pixbuf = NULL; | |
| } | |
| context->pixbuf = output; | |
| context->prepared_func (context->pixbuf, NULL, context->user_data); | |
| return TRUE; | |
| } | |
| static gpointer | |
| begin_load (GdkPixbufModuleSizeFunc size_func, | |
| GdkPixbufModulePreparedFunc prepared_func, | |
| GdkPixbufModuleUpdatedFunc updated_func, | |
| gpointer user_data, | |
| GError **error) | |
| { | |
| struct avif_context *context; | |
| avifDecoder *decoder; | |
| g_assert (prepared_func != NULL); | |
| decoder = avifDecoderCreate (); | |
| if (!decoder) { | |
| g_set_error_literal (error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_INSUFFICIENT_MEMORY, | |
| "Couldn't allocate memory for decoder"); | |
| return NULL; | |
| } | |
| context = g_new0 (struct avif_context, 1); | |
| if (!context) | |
| return NULL; | |
| context->size_func = size_func; | |
| context->updated_func = updated_func; | |
| context->prepared_func = prepared_func; | |
| context->user_data = user_data; | |
| context->decoder = decoder; | |
| context->data = g_byte_array_sized_new (40000); | |
| return context; | |
| } | |
| static gboolean | |
| stop_load (gpointer data, GError **error) | |
| { | |
| struct avif_context *context = (struct avif_context *) data; | |
| gboolean ret; | |
| context->bytes = g_byte_array_free_to_bytes (context->data); | |
| context->data = NULL; | |
| ret = avif_context_try_load (context, error); | |
| avif_context_free (context); | |
| return ret; | |
| } | |
| static gboolean | |
| load_increment (gpointer data, const guchar *buf, guint size, GError **error) | |
| { | |
| struct avif_context *context = (struct avif_context *) data; | |
| g_byte_array_append (context->data, buf, size); | |
| if (error) | |
| *error = NULL; | |
| return TRUE; | |
| } | |
| static gboolean | |
| avif_is_save_option_supported (const gchar *option_key) | |
| { | |
| if (g_strcmp0 (option_key, "quality") == 0) { | |
| return TRUE; | |
| } | |
| return FALSE; | |
| } | |
| static gboolean | |
| avif_image_saver (FILE *f, | |
| GdkPixbuf *pixbuf, | |
| gchar **keys, | |
| gchar **values, | |
| GError **error) | |
| { | |
| int width, height; | |
| long quality = 68; /* default; must be between 0 and 100 */ | |
| gboolean save_alpha; | |
| avifImage *avif; | |
| avifRGBImage rgb; | |
| avifResult res; | |
| avifRWData raw = AVIF_DATA_EMPTY; | |
| avifEncoder *encoder; | |
| guint maxThreads; | |
| if (f == NULL || pixbuf == NULL) { | |
| return FALSE; | |
| } | |
| if (keys && *keys) { | |
| gchar **kiter = keys; | |
| gchar **viter = values; | |
| while (*kiter) { | |
| if (strcmp (*kiter, "quality") == 0) { | |
| char *endptr = NULL; | |
| quality = strtol (*viter, &endptr, 10); | |
| if (endptr == *viter) { | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_BAD_OPTION, | |
| "AVIF quality must be a value between 0 and 100; value \"%s\" could not be parsed.", | |
| *viter); | |
| return FALSE; | |
| } | |
| if (quality < 0 || quality > 100) { | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_BAD_OPTION, | |
| "AVIF quality must be a value between 0 and 100; value \"%ld\" is not allowed.", | |
| quality); | |
| return FALSE; | |
| } | |
| } else { | |
| g_warning ("Unrecognized parameter (%s) passed to AVIF saver.", *kiter); | |
| } | |
| ++kiter; | |
| ++viter; | |
| } | |
| } | |
| if (gdk_pixbuf_get_bits_per_sample (pixbuf) != 8) { | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_UNKNOWN_TYPE, | |
| "Sorry, only 8bit images are supported by this AVIF saver"); | |
| return FALSE; | |
| } | |
| width = gdk_pixbuf_get_width (pixbuf); | |
| height = gdk_pixbuf_get_height (pixbuf); | |
| if (width == 0 || height == 0) { | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_CORRUPT_IMAGE, | |
| "Empty image, nothing to save"); | |
| return FALSE; | |
| } | |
| save_alpha = gdk_pixbuf_get_has_alpha (pixbuf); | |
| if (save_alpha) { | |
| if (gdk_pixbuf_get_n_channels (pixbuf) != 4) { | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_UNKNOWN_TYPE, | |
| "Unsupported number of channels"); | |
| return FALSE; | |
| } | |
| } else { | |
| if (gdk_pixbuf_get_n_channels (pixbuf) != 3) { | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_UNKNOWN_TYPE, | |
| "Unsupported number of channels"); | |
| return FALSE; | |
| } | |
| } | |
| avif = avifImageCreate (width, height, 8, quality >= 90 ? AVIF_PIXEL_FORMAT_YUV444 : AVIF_PIXEL_FORMAT_YUV420); | |
| avif->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; | |
| avifRGBImageSetDefaults (&rgb, avif); | |
| rgb.depth = 8; | |
| rgb.pixels = (uint8_t *) gdk_pixbuf_read_pixels (pixbuf); | |
| rgb.rowBytes = gdk_pixbuf_get_rowstride (pixbuf); | |
| if (save_alpha) { | |
| rgb.format = AVIF_RGB_FORMAT_RGBA; | |
| } else { | |
| rgb.format = AVIF_RGB_FORMAT_RGB; | |
| } | |
| res = avifImageRGBToYUV (avif, &rgb); | |
| if (res != AVIF_RESULT_OK) { | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_FAILED, | |
| "Problem in RGB->YUV conversion: %s", avifResultToString (res)); | |
| avifImageDestroy (avif); | |
| return FALSE; | |
| } | |
| maxThreads = g_get_num_processors (); | |
| encoder = avifEncoderCreate (); | |
| encoder->maxThreads = CLAMP (maxThreads, 1, 64); | |
| encoder->quality = (int) quality; | |
| if (save_alpha) { | |
| if (quality >= 50) { | |
| encoder->qualityAlpha = 100; | |
| } else { | |
| encoder->qualityAlpha = 75 + (int) quality / 2; | |
| } | |
| } | |
| encoder->speed = 6; | |
| res = avifEncoderWrite (encoder, avif, &raw); | |
| avifEncoderDestroy (encoder); | |
| avifImageDestroy (avif); | |
| if (res == AVIF_RESULT_OK) { | |
| fwrite (raw.data, 1, raw.size, f); | |
| avifRWDataFree (&raw); | |
| return TRUE; | |
| } | |
| g_set_error (error, | |
| GDK_PIXBUF_ERROR, | |
| GDK_PIXBUF_ERROR_FAILED, | |
| "AVIF encoder problem: %s", avifResultToString (res)); | |
| return FALSE; | |
| } | |
| G_MODULE_EXPORT void | |
| fill_vtable (GdkPixbufModule *module) | |
| { | |
| module->begin_load = begin_load; | |
| module->stop_load = stop_load; | |
| module->load_increment = load_increment; | |
| module->is_save_option_supported = avif_is_save_option_supported; | |
| module->save = avif_image_saver; | |
| } | |
| G_MODULE_EXPORT void | |
| fill_info (GdkPixbufFormat *info) | |
| { | |
| static GdkPixbufModulePattern signature[] = { | |
| { " ftypavif", "zzz ", 100 }, /* file begins with 'ftypavif' at offset 4 */ | |
| { NULL, NULL, 0 } | |
| }; | |
| static gchar *mime_types[] = { | |
| "image/avif", | |
| NULL | |
| }; | |
| static gchar *extensions[] = { | |
| "avif", | |
| NULL | |
| }; | |
| info->name = "avif"; | |
| info->signature = (GdkPixbufModulePattern *) signature; | |
| info->description = "AV1 Image File Format"; | |
| info->mime_types = (gchar **) mime_types; | |
| info->extensions = (gchar **) extensions; | |
| info->flags = GDK_PIXBUF_FORMAT_WRITABLE | GDK_PIXBUF_FORMAT_THREADSAFE; | |
| info->license = "BSD"; | |
| info->disabled = FALSE; | |
| } |