| /* Copyright 2020 Emmanuel Gil Peyrot. All rights reserved. |
| SPDX-License-Identifier: BSD-2-Clause |
| */ |
| |
| #include <avif/avif.h> |
| #include <stdlib.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, min_quantizer, max_quantizer, alpha_quantizer; |
| long quality = 52; /* 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; |
| } |
| } |
| |
| max_quantizer = AVIF_QUANTIZER_WORST_QUALITY * (100 - (int)quality) / 100; |
| min_quantizer = 0; |
| alpha_quantizer = 0; |
| |
| if ( max_quantizer > 20 ) { |
| min_quantizer = max_quantizer - 20; |
| |
| if (max_quantizer > 40) { |
| alpha_quantizer = max_quantizer - 40; |
| } |
| } |
| |
| avif = avifImageCreate(width, height, 8, 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->minQuantizer = min_quantizer; |
| encoder->maxQuantizer = max_quantizer; |
| encoder->minQuantizerAlpha = 0; |
| encoder->maxQuantizerAlpha = alpha_quantizer; |
| 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; |
| } |
| |