diff options
Diffstat (limited to 'plug-ins/common/file-heif.c')
-rw-r--r-- | plug-ins/common/file-heif.c | 2192 |
1 files changed, 2192 insertions, 0 deletions
diff --git a/plug-ins/common/file-heif.c b/plug-ins/common/file-heif.c new file mode 100644 index 0000000..7275fa2 --- /dev/null +++ b/plug-ins/common/file-heif.c @@ -0,0 +1,2192 @@ +/* + * GIMP HEIF loader / write plugin. + * Copyright (c) 2018 struktur AG, Dirk Farin <farin@struktur.de> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <libheif/heif.h> +#include <lcms2.h> +#include <gexiv2/gexiv2.h> + +#include <libgimp/gimp.h> +#include <libgimp/gimpui.h> + +#include "libgimp/stdplugins-intl.h" + + +#define LOAD_PROC "file-heif-load" +#define SAVE_PROC "file-heif-save" +#define SAVE_PROC_AV1 "file-heif-av1-save" +#define PLUG_IN_BINARY "file-heif" + + +typedef struct _SaveParams SaveParams; + +struct _SaveParams +{ + gint quality; + gboolean lossless; + gboolean save_profile; + gint save_bit_depth; +}; + + +/* local function prototypes */ + +static void init (void); +static void run (const gchar *name, + gint nparams, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals); + +static gint32 load_image (GFile *file, + gboolean interactive, + GError **error); +static gboolean save_image (GFile *file, + gint32 image_ID, + gint32 drawable_ID, + const SaveParams *params, + GError **error, + enum heif_compression_format compression); + +static gboolean load_dialog (struct heif_context *heif, + uint32_t *selected_image); +static gboolean save_dialog (SaveParams *params, + gint32 image_ID, + const gchar *procname); + + +GimpPlugInInfo PLUG_IN_INFO = +{ + init, /* init_proc */ + NULL, /* quit_proc */ + NULL, /* query_proc */ + run, /* run_proc */ +}; + + +MAIN () + + +static void +init (void) +{ + static const GimpParamDef load_args[] = + { + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, + { GIMP_PDB_STRING, "uri", "The URI of the file to load" }, + { GIMP_PDB_STRING, "raw-uri", "The URI of the file to load" } + }; + + static const GimpParamDef load_return_vals[] = + { + { GIMP_PDB_IMAGE, "image", "Output image" } + }; + + + static const GimpParamDef save_args[] = + { + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, + { GIMP_PDB_IMAGE, "image", "Input image" }, + { GIMP_PDB_DRAWABLE, "drawable", "Drawable to export" }, + { GIMP_PDB_STRING, "uri", "The URI of the file to export the image to" }, + { GIMP_PDB_STRING, "raw-uri", "The UTI of the file to export the image to" }, + { GIMP_PDB_INT32, "quality", "Quality factor (range: 0-100. 0 = worst, 100 = best)" }, + { GIMP_PDB_INT32, "lossless", "Use lossless compression (0 = lossy, 1 = lossless)" } + }; + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_init (NULL); +#endif + + if (heif_have_decoder_for_format (heif_compression_HEVC) +#if LIBHEIF_HAVE_VERSION(1,8,0) + || heif_have_decoder_for_format (heif_compression_AV1) +#endif + ) + { + GString *extensions = g_string_new (NULL); + GString *mimetypes = g_string_new (NULL); + GString *magic = g_string_new ("4,string,ftypmif1"); + + if (heif_have_decoder_for_format (heif_compression_HEVC)) + { + g_string_append (extensions, "heic,heif"); + g_string_append (mimetypes, "image/heif"); + g_string_append (magic, ",4,string,ftypheic,4,string,ftypheix," + "4,string,ftyphevc,4,string,ftypheim," + "4,string,ftypheis,4,string,ftyphevm," + "4,string,ftyphevs,4,string,ftypmsf1"); + } + +#if LIBHEIF_HAVE_VERSION(1,8,0) + if (heif_have_decoder_for_format (heif_compression_AV1)) + { + g_string_append_printf (extensions, "%savif", extensions->len ? "," : ""); + g_string_append_printf (mimetypes, "%simage/avif", mimetypes->len ? "," : ""); + g_string_append (magic, ",4,string,ftypavif"); + } +#endif + + gimp_install_procedure (LOAD_PROC, + _("Loads HEIF images"), + _("Load image stored in HEIF format (High " + "Efficiency Image File Format). Typical " + "suffices for HEIF files are .heif, .heic."), + "Dirk Farin <farin@struktur.de>", + "Dirk Farin <farin@struktur.de>", + "2018", + _("HEIF/HEIC"), + NULL, + GIMP_PLUGIN, + G_N_ELEMENTS (load_args), + G_N_ELEMENTS (load_return_vals), + load_args, load_return_vals); + + gimp_register_load_handler (LOAD_PROC, extensions->str, ""); + gimp_register_file_handler_mime (LOAD_PROC, mimetypes->str); + gimp_register_file_handler_uri (LOAD_PROC); + + gimp_register_magic_load_handler (LOAD_PROC, + extensions->str, "", + magic->str); + + g_string_free (magic, TRUE); + g_string_free (mimetypes, TRUE); + g_string_free (extensions, TRUE); + } + + if (heif_have_encoder_for_format (heif_compression_HEVC)) + { + gimp_install_procedure (SAVE_PROC, + _("Exports HEIF images"), + _("Save image in HEIF format (High Efficiency " + "Image File Format)."), + "Dirk Farin <farin@struktur.de>", + "Dirk Farin <farin@struktur.de>", + "2018", + _("HEIF/HEIC"), + "RGB*", + GIMP_PLUGIN, + G_N_ELEMENTS (save_args), 0, + save_args, NULL); + + gimp_register_save_handler (SAVE_PROC, "heic,heif", ""); + gimp_register_file_handler_mime (SAVE_PROC, "image/heif"); + gimp_register_file_handler_uri (SAVE_PROC); + } + +#if LIBHEIF_HAVE_VERSION(1,8,0) + if (heif_have_encoder_for_format (heif_compression_AV1)) + { + gimp_install_procedure (SAVE_PROC_AV1, + _("Exports AVIF images"), + _("Save image in AV1 Image File Format (AVIF)"), + "Daniel Novomesky <dnovomesky@gmail.com>", + "Daniel Novomesky <dnovomesky@gmail.com>", + "2020", + "HEIF/AVIF", + "RGB*", + GIMP_PLUGIN, + G_N_ELEMENTS (save_args), 0, + save_args, NULL); + + gimp_register_save_handler (SAVE_PROC_AV1, "avif", ""); + gimp_register_file_handler_mime (SAVE_PROC_AV1, "image/avif"); + gimp_register_file_handler_uri (SAVE_PROC_AV1); + } +#endif + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_deinit (); +#endif +} + +#define LOAD_HEIF_CANCEL -2 + +static void +run (const gchar *name, + gint n_params, + const GimpParam *param, + gint *nreturn_vals, + GimpParam **return_vals) +{ + static GimpParam values[2]; + GimpRunMode run_mode; + GimpPDBStatusType status = GIMP_PDB_SUCCESS; + GError *error = NULL; + + INIT_I18N (); + gegl_init (NULL, NULL); + + run_mode = param[0].data.d_int32; + + *nreturn_vals = 1; + *return_vals = values; + + values[0].type = GIMP_PDB_STATUS; + values[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + + if (run_mode == GIMP_RUN_INTERACTIVE) + gimp_ui_init (PLUG_IN_BINARY, FALSE); + + if (strcmp (name, LOAD_PROC) == 0) + { + GFile *file; + gboolean interactive; + + if (n_params != 3) + status = GIMP_PDB_CALLING_ERROR; + + file = g_file_new_for_uri (param[1].data.d_string); + interactive = (run_mode == GIMP_RUN_INTERACTIVE); + + if (status == GIMP_PDB_SUCCESS) + { + gint32 image_ID; + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_init (NULL); +#endif + + image_ID = load_image (file, interactive, &error); + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_deinit (); +#endif + + if (image_ID >= 0) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_IMAGE; + values[1].data.d_image = image_ID; + } + else if (image_ID == LOAD_HEIF_CANCEL) + { + status = GIMP_PDB_CANCEL; + } + else + { + status = GIMP_PDB_CALLING_ERROR; + } + } + + g_object_unref (file); + } + else if (strcmp (name, SAVE_PROC) == 0) + { + gint32 image_ID = param[1].data.d_int32; + gint32 drawable_ID = param[2].data.d_int32; + GimpExportReturn export = GIMP_EXPORT_CANCEL; + SaveParams params; + GimpMetadata *metadata = NULL; + GimpMetadataSaveFlags metadata_flags; + + metadata = gimp_image_metadata_save_prepare (image_ID, + "image/heif", + &metadata_flags); + + params.lossless = FALSE; + params.quality = 50; + params.save_profile = (metadata_flags & GIMP_METADATA_SAVE_COLOR_PROFILE) != 0; + params.save_bit_depth = 8; + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + export = gimp_export_image (&image_ID, &drawable_ID, "HEIF", + GIMP_EXPORT_CAN_HANDLE_RGB | + GIMP_EXPORT_CAN_HANDLE_ALPHA); + + if (export == GIMP_EXPORT_CANCEL) + { + values[0].data.d_status = GIMP_PDB_CANCEL; + g_clear_object (&metadata); + return; + } + break; + + default: + break; + } + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + gimp_get_data (SAVE_PROC, ¶ms); + + if (! save_dialog (¶ms, image_ID, name)) + status = GIMP_PDB_CANCEL; + break; + + case GIMP_RUN_WITH_LAST_VALS: + gimp_get_data (SAVE_PROC, ¶ms); + break; + + case GIMP_RUN_NONINTERACTIVE: + /* Make sure all the arguments are there! */ + if (n_params != 7) + { + status = GIMP_PDB_CALLING_ERROR; + } + else + { + params.quality = (param[5].data.d_int32); + params.lossless = (param[6].data.d_int32); + } + break; + } + + if (status == GIMP_PDB_SUCCESS) + { + GFile *file = g_file_new_for_uri (param[3].data.d_string); + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_init (NULL); +#endif + + if (save_image (file, image_ID, drawable_ID, + ¶ms, + &error, + heif_compression_HEVC)) + { + /* Exiv2 doesn't support HEIC format yet, following code doesn't work + if (metadata) + gimp_image_metadata_save_finish (image_ID, + "image/heif", + metadata, metadata_flags, + file, NULL); + */ + gimp_set_data (SAVE_PROC, ¶ms, sizeof (params)); + } + else + { + status = GIMP_PDB_EXECUTION_ERROR; + } + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_deinit (); +#endif + + g_object_unref (file); + } + + if (export == GIMP_EXPORT_EXPORT) + gimp_image_delete (image_ID); + + g_clear_object (&metadata); + } +#if LIBHEIF_HAVE_VERSION(1,8,0) + else if (strcmp (name, SAVE_PROC_AV1) == 0) + { + gint32 image_ID = param[1].data.d_int32; + gint32 drawable_ID = param[2].data.d_int32; + GimpExportReturn export = GIMP_EXPORT_CANCEL; + SaveParams params; + GimpMetadata *metadata = NULL; + GimpMetadataSaveFlags metadata_flags; + + metadata = gimp_image_metadata_save_prepare (image_ID, + "image/avif", + &metadata_flags); + + params.lossless = FALSE; + params.quality = 50; + params.save_profile = (metadata_flags & GIMP_METADATA_SAVE_COLOR_PROFILE) != 0; + params.save_bit_depth = 8; + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + export = gimp_export_image (&image_ID, &drawable_ID, "AVIF", + GIMP_EXPORT_CAN_HANDLE_RGB | + GIMP_EXPORT_CAN_HANDLE_ALPHA); + + if (export == GIMP_EXPORT_CANCEL) + { + values[0].data.d_status = GIMP_PDB_CANCEL; + g_clear_object (&metadata); + return; + } + break; + + default: + break; + } + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + gimp_get_data (SAVE_PROC_AV1, ¶ms); + + if (! save_dialog (¶ms, image_ID, name)) + status = GIMP_PDB_CANCEL; + break; + + case GIMP_RUN_WITH_LAST_VALS: + gimp_get_data (SAVE_PROC_AV1, ¶ms); + break; + + case GIMP_RUN_NONINTERACTIVE: + /* Make sure all the arguments are there! */ + if (n_params != 7) + { + status = GIMP_PDB_CALLING_ERROR; + } + else + { + params.quality = (param[5].data.d_int32); + params.lossless = (param[6].data.d_int32); + } + break; + } + + if (status == GIMP_PDB_SUCCESS) + { + GFile *file = g_file_new_for_uri (param[3].data.d_string); + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_init (NULL); +#endif + + if (save_image (file, image_ID, drawable_ID, + ¶ms, + &error, + heif_compression_AV1)) + { + gimp_set_data (SAVE_PROC_AV1, ¶ms, sizeof (params)); + } + else + { + status = GIMP_PDB_EXECUTION_ERROR; + } + +#if LIBHEIF_HAVE_VERSION(1,13,0) + heif_deinit (); +#endif + + g_object_unref (file); + } + + if (export == GIMP_EXPORT_EXPORT) + gimp_image_delete (image_ID); + + g_clear_object (&metadata); + } +#endif + else + { + status = GIMP_PDB_CALLING_ERROR; + } + + if (status != GIMP_PDB_SUCCESS && error) + { + *nreturn_vals = 2; + values[1].type = GIMP_PDB_STRING; + values[1].data.d_string = error->message; + } + + values[0].data.d_status = status; +} + +static goffset +get_file_size (GFile *file, + GError **error) +{ + GFileInfo *info; + goffset size = 1; + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_SIZE, + G_FILE_QUERY_INFO_NONE, + NULL, error); + if (info) + { + size = g_file_info_get_size (info); + + g_object_unref (info); + } + + return size; +} + +#if LIBHEIF_HAVE_VERSION(1,8,0) +static void +heifplugin_color_profile_set_tag (cmsHPROFILE profile, + cmsTagSignature sig, + const gchar *tag) +{ + cmsMLU *mlu; + + mlu = cmsMLUalloc (NULL, 1); + cmsMLUsetASCII (mlu, "en", "US", tag); + cmsWriteTag (profile, sig, mlu); + cmsMLUfree (mlu); +} + +static GimpColorProfile * +nclx_to_gimp_profile (const struct heif_color_profile_nclx *nclx) +{ + const gchar *primaries_name = ""; + const gchar *trc_name = ""; + cmsHPROFILE profile = NULL; + cmsCIExyY whitepoint; + cmsCIExyYTRIPLE primaries; + cmsToneCurve *curve[3]; + + cmsFloat64Number srgb_parameters[5] = + { 2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045 }; + + cmsFloat64Number rec709_parameters[5] = + { 2.2, 1.0 / 1.099, 0.099 / 1.099, 1.0 / 4.5, 0.081 }; + + if (nclx == NULL) + { + return NULL; + } + + if (nclx->color_primaries == heif_color_primaries_unspecified) + { + return NULL; + } + + if (nclx->color_primaries == heif_color_primaries_ITU_R_BT_709_5) + { + if (nclx->transfer_characteristics == heif_transfer_characteristic_IEC_61966_2_1) + { + return gimp_color_profile_new_rgb_srgb(); + } + + if (nclx->transfer_characteristics == heif_transfer_characteristic_linear) + { + return gimp_color_profile_new_rgb_srgb_linear(); + } + } + + whitepoint.x = nclx->color_primary_white_x; + whitepoint.y = nclx->color_primary_white_y; + whitepoint.Y = 1.0f; + + primaries.Red.x = nclx->color_primary_red_x; + primaries.Red.y = nclx->color_primary_red_y; + primaries.Red.Y = 1.0f; + + primaries.Green.x = nclx->color_primary_green_x; + primaries.Green.y = nclx->color_primary_green_y; + primaries.Green.Y = 1.0f; + + primaries.Blue.x = nclx->color_primary_blue_x; + primaries.Blue.y = nclx->color_primary_blue_y; + primaries.Blue.Y = 1.0f; + + switch (nclx->color_primaries) + { + case heif_color_primaries_ITU_R_BT_709_5: + primaries_name = "BT.709"; + break; + case heif_color_primaries_ITU_R_BT_470_6_System_M: + primaries_name = "BT.470-6 System M"; + break; + case heif_color_primaries_ITU_R_BT_470_6_System_B_G: + primaries_name = "BT.470-6 System BG"; + break; + case heif_color_primaries_ITU_R_BT_601_6: + primaries_name = "BT.601"; + break; + case heif_color_primaries_SMPTE_240M: + primaries_name = "SMPTE 240M"; + break; + case 8: + primaries_name = "Generic film"; + break; + case 9: + primaries_name = "BT.2020"; + break; + case 10: + primaries_name = "XYZ"; + break; + case 11: + primaries_name = "SMPTE RP 431-2"; + break; + case 12: + primaries_name = "SMPTE EG 432-1 (DCI P3)"; + break; + case 22: + primaries_name = "EBU Tech. 3213-E"; + break; + default: + g_warning ("%s: Unsupported color_primaries value %d.", + G_STRFUNC, nclx->color_primaries); + return NULL; + break; + } + + switch (nclx->transfer_characteristics) + { + case heif_transfer_characteristic_ITU_R_BT_709_5: + curve[0] = curve[1] = curve[2] = cmsBuildParametricToneCurve (NULL, 4, + rec709_parameters); + profile = cmsCreateRGBProfile (&whitepoint, &primaries, curve); + cmsFreeToneCurve (curve[0]); + trc_name = "Rec709 RGB"; + break; + case heif_transfer_characteristic_ITU_R_BT_470_6_System_M: + curve[0] = curve[1] = curve[2] = cmsBuildGamma (NULL, 2.2f); + profile = cmsCreateRGBProfile (&whitepoint, &primaries, curve); + cmsFreeToneCurve (curve[0]); + trc_name = "Gamma2.2 RGB"; + break; + case heif_transfer_characteristic_ITU_R_BT_470_6_System_B_G: + curve[0] = curve[1] = curve[2] = cmsBuildGamma (NULL, 2.8f); + profile = cmsCreateRGBProfile (&whitepoint, &primaries, curve); + cmsFreeToneCurve (curve[0]); + trc_name = "Gamma2.8 RGB"; + break; + case heif_transfer_characteristic_linear: + curve[0] = curve[1] = curve[2] = cmsBuildGamma (NULL, 1.0f); + profile = cmsCreateRGBProfile (&whitepoint, &primaries, curve); + cmsFreeToneCurve (curve[0]); + trc_name = "linear RGB"; + break; + case heif_transfer_characteristic_IEC_61966_2_1: + /* same as default */ + default: + curve[0] = curve[1] = curve[2] = cmsBuildParametricToneCurve (NULL, 4, + srgb_parameters); + profile = cmsCreateRGBProfile (&whitepoint, &primaries, curve); + cmsFreeToneCurve (curve[0]); + trc_name = "sRGB-TRC RGB"; + break; + } + + if (profile) + { + GimpColorProfile *new_profile; + gchar *description = g_strdup_printf ("%s %s", primaries_name, trc_name); + + heifplugin_color_profile_set_tag (profile, cmsSigProfileDescriptionTag, + description); + heifplugin_color_profile_set_tag (profile, cmsSigDeviceMfgDescTag, + "GIMP"); + heifplugin_color_profile_set_tag (profile, cmsSigDeviceModelDescTag, + description); + heifplugin_color_profile_set_tag (profile, cmsSigCopyrightTag, + "Public Domain"); + + new_profile = gimp_color_profile_new_from_lcms_profile (profile, NULL); + + cmsCloseProfile (profile); + g_free (description); + return new_profile; + } + + return NULL; +} +#endif + +gint32 +load_image (GFile *file, + gboolean interactive, + GError **error) +{ + GInputStream *input; + goffset file_size; + guchar *file_buffer; + gsize bytes_read; + struct heif_context *ctx; + struct heif_error err; + struct heif_image_handle *handle = NULL; + struct heif_image *img = NULL; + GimpColorProfile *profile = NULL; + gint n_images; + heif_item_id primary; + heif_item_id selected_image; + gboolean has_alpha; + gint width; + gint height; + gint32 image_ID; + gint32 layer_ID; + GeglBuffer *buffer; + const Babl *format; + const guint8 *data; + gint stride; + gint bit_depth = 8; + enum heif_chroma chroma = heif_chroma_interleaved_RGB; + GimpPrecision precision; + gboolean load_linear; + const char *encoding; + char *filename = g_file_get_parse_name (file); + + gimp_progress_init_printf (_("Opening '%s'"), + filename); + g_free (filename); + + file_size = get_file_size (file, error); + if (file_size <= 0) + return -1; + + input = G_INPUT_STREAM (g_file_read (file, NULL, error)); + if (! input) + return -1; + + file_buffer = g_malloc (file_size); + + if (! g_input_stream_read_all (input, file_buffer, file_size, + &bytes_read, NULL, error) && + bytes_read == 0) + { + g_free (file_buffer); + g_object_unref (input); + return -1; + } + + gimp_progress_update (0.25); + + ctx = heif_context_alloc (); + if (!ctx) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "cannot allocate heif_context"); + g_free (file_buffer); + g_object_unref (input); + return -1; + } + + err = heif_context_read_from_memory (ctx, file_buffer, file_size, NULL); + if (err.code) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Loading HEIF image failed: %s"), + err.message); + heif_context_free (ctx); + g_free (file_buffer); + g_object_unref (input); + + return -1; + } + + g_free (file_buffer); + g_object_unref (input); + + gimp_progress_update (0.5); + + /* analyze image content + * Is there more than one image? Which image is the primary image? + */ + + n_images = heif_context_get_number_of_top_level_images (ctx); + if (n_images == 0) + { + g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Loading HEIF image failed: " + "Input file contains no readable images")); + heif_context_free (ctx); + + return -1; + } + + err = heif_context_get_primary_image_ID (ctx, &primary); + if (err.code) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Loading HEIF image failed: %s"), + err.message); + heif_context_free (ctx); + + return -1; + } + + /* if primary image is no top level image or not present (invalid + * file), just take the first image + */ + + if (! heif_context_is_top_level_image_ID (ctx, primary)) + { + gint n = heif_context_get_list_of_top_level_image_IDs (ctx, &primary, 1); + g_assert (n == 1); + } + + selected_image = primary; + + /* if there are several images in the file and we are running + * interactive, let the user choose a picture + */ + + if (interactive && n_images > 1) + { + if (! load_dialog (ctx, &selected_image)) + { + heif_context_free (ctx); + + return LOAD_HEIF_CANCEL; + } + } + + /* load the picture */ + + err = heif_context_get_image_handle (ctx, selected_image, &handle); + if (err.code) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Loading HEIF image failed: %s"), + err.message); + heif_context_free (ctx); + + return -1; + } + + has_alpha = heif_image_handle_has_alpha_channel (handle); + +#if LIBHEIF_HAVE_VERSION(1,8,0) + bit_depth = heif_image_handle_get_luma_bits_per_pixel (handle); + if (bit_depth < 0) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Input image has undefined bit-depth"); + heif_image_handle_release (handle); + heif_context_free (ctx); + + return -1; + } +#endif + + if (bit_depth == 8) + { + if (has_alpha) + { + chroma = heif_chroma_interleaved_RGBA; + } + else + { + chroma = heif_chroma_interleaved_RGB; + } + } + else /* high bit depth */ + { +#if LIBHEIF_HAVE_VERSION(1,8,0) +#if ( G_BYTE_ORDER == G_LITTLE_ENDIAN ) + if (has_alpha) + { + chroma = heif_chroma_interleaved_RRGGBBAA_LE; + } + else + { + chroma = heif_chroma_interleaved_RRGGBB_LE; + } +#else + if (has_alpha) + { + chroma = heif_chroma_interleaved_RRGGBBAA_BE; + } + else + { + chroma = heif_chroma_interleaved_RRGGBB_BE; + } +#endif +#endif + } + + err = heif_decode_image (handle, + &img, + heif_colorspace_RGB, + chroma, + NULL); + if (err.code) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Loading HEIF image failed: %s"), + err.message); + heif_image_handle_release (handle); + heif_context_free (ctx); + + return -1; + } + +#if LIBHEIF_HAVE_VERSION(1,4,0) + switch (heif_image_handle_get_color_profile_type (handle)) + { + case heif_color_profile_type_not_present: + break; + case heif_color_profile_type_rICC: + case heif_color_profile_type_prof: + /* I am unsure, but it looks like both these types represent an + * ICC color profile. XXX + */ + { + void *profile_data; + size_t profile_size; + + profile_size = heif_image_handle_get_raw_color_profile_size (handle); + profile_data = g_malloc0 (profile_size); + err = heif_image_handle_get_raw_color_profile (handle, profile_data); + + if (err.code) + g_warning ("%s: color profile loading failed and discarded.", + G_STRFUNC); + else + profile = gimp_color_profile_new_from_icc_profile ((guint8 *) profile_data, + profile_size, NULL); + + g_free (profile_data); + } + break; +#if LIBHEIF_HAVE_VERSION(1,8,0) + case heif_color_profile_type_nclx: + { + struct heif_color_profile_nclx *nclx = NULL; + + err = heif_image_handle_get_nclx_color_profile (handle, &nclx); + if (err.code) + { + g_warning ("%s: NCLX profile loading failed and discarded.", + G_STRFUNC); + } + else + { + profile = nclx_to_gimp_profile (nclx); + heif_nclx_color_profile_free (nclx); + } + } + break; +#endif + default: + g_warning ("%s: unknown color profile type has been discarded.", + G_STRFUNC); + break; + } +#endif /* LIBHEIF_HAVE_VERSION(1,4,0) */ + + gimp_progress_update (0.75); + + width = heif_image_get_width (img, heif_channel_interleaved); + height = heif_image_get_height (img, heif_channel_interleaved); + + /* create GIMP image and copy HEIF image into the GIMP image + * (converting it to RGB) + */ + + if (profile) + { + load_linear = gimp_color_profile_is_linear (profile); + } + else + { + load_linear = FALSE; + } + + if (load_linear) + { + if (bit_depth == 8) + { + precision = GIMP_PRECISION_U8_LINEAR; + encoding = has_alpha ? "RGBA u8" : "RGB u8"; + } + else + { + precision = GIMP_PRECISION_U16_LINEAR; + encoding = has_alpha ? "RGBA u16" : "RGB u16"; + } + } + else /* non-linear profiles */ + { + if (bit_depth == 8) + { + precision = GIMP_PRECISION_U8_GAMMA; + encoding = has_alpha ? "R'G'B'A u8" : "R'G'B' u8"; + } + else + { + precision = GIMP_PRECISION_U16_GAMMA; + encoding = has_alpha ? "R'G'B'A u16" : "R'G'B' u16"; + } + } + + image_ID = gimp_image_new_with_precision (width, height, GIMP_RGB, precision); + gimp_image_set_filename (image_ID, g_file_get_uri (file)); + + if (profile) + { + if (gimp_color_profile_is_rgb (profile)) + { + gimp_image_set_color_profile (image_ID, profile); + } + else if (gimp_color_profile_is_gray (profile)) + { + g_warning ("Gray ICC profile was not applied to the imported image."); + } + else + { + g_warning ("ICC profile was not applied to the imported image."); + } + } + + layer_ID = gimp_layer_new (image_ID, + _("image content"), + width, height, + has_alpha ? GIMP_RGBA_IMAGE : GIMP_RGB_IMAGE, + 100.0, + gimp_image_get_default_new_layer_mode (image_ID)); + + gimp_image_insert_layer (image_ID, layer_ID, -1, 0); + + buffer = gimp_drawable_get_buffer (layer_ID); + + data = heif_image_get_plane_readonly (img, heif_channel_interleaved, + &stride); + + format = babl_format_with_space (encoding, + gegl_buffer_get_format (buffer)); + + if (bit_depth == 8) + { + gegl_buffer_set (buffer, + GEGL_RECTANGLE (0, 0, width, height), + 0, format, data, stride); + } + else /* high bit depth */ + { + uint16_t *data16; + const uint16_t *src16; + uint16_t *dest16; + gint x, y, rowentries; + int tmp_pixelval; + + if (has_alpha) + { + rowentries = width * 4; + } + else /* no alpha */ + { + rowentries = width * 3; + } + + data16 = g_malloc_n (height, rowentries * 2); + dest16 = data16; + + switch (bit_depth) + { + case 10: + for (y = 0; y < height; y++) + { + src16 = (const uint16_t *) (y * stride + data); + for (x = 0; x < rowentries; x++) + { + tmp_pixelval = (int) ( ( (float) (0x03ff & (*src16)) / 1023.0f) * 65535.0f + 0.5f); + *dest16 = CLAMP (tmp_pixelval, 0, 65535); + dest16++; + src16++; + } + } + break; + case 12: + for (y = 0; y < height; y++) + { + src16 = (const uint16_t *) (y * stride + data); + for (x = 0; x < rowentries; x++) + { + tmp_pixelval = (int) ( ( (float) (0x0fff & (*src16)) / 4095.0f) * 65535.0f + 0.5f); + *dest16 = CLAMP (tmp_pixelval, 0, 65535); + dest16++; + src16++; + } + } + break; + default: + for (y = 0; y < height; y++) + { + src16 = (const uint16_t *) (y * stride + data); + for (x = 0; x < rowentries; x++) + { + *dest16 = *src16; + dest16++; + src16++; + } + } + break; + } + + gegl_buffer_set (buffer, + GEGL_RECTANGLE (0, 0, width, height), + 0, format, data16, GEGL_AUTO_ROWSTRIDE); + + g_free (data16); + } + + g_object_unref (buffer); + + { + size_t exif_data_size = 0; + uint8_t *exif_data = NULL; + size_t xmp_data_size = 0; + uint8_t *xmp_data = NULL; + gint n_metadata; + heif_item_id metadata_id; + + n_metadata = + heif_image_handle_get_list_of_metadata_block_IDs (handle, + "Exif", + &metadata_id, 1); + if (n_metadata > 0) + { + exif_data_size = heif_image_handle_get_metadata_size (handle, + metadata_id); + exif_data = g_alloca (exif_data_size); + + err = heif_image_handle_get_metadata (handle, metadata_id, exif_data); + if (err.code != 0) + { + exif_data = NULL; + exif_data_size = 0; + } + } + + n_metadata = + heif_image_handle_get_list_of_metadata_block_IDs (handle, + "mime", + &metadata_id, 1); + if (n_metadata > 0) + { + if (g_strcmp0 ( + heif_image_handle_get_metadata_content_type (handle, metadata_id), + "application/rdf+xml") == 0) + { + xmp_data_size = heif_image_handle_get_metadata_size (handle, + metadata_id); + xmp_data = g_alloca (xmp_data_size); + + err = heif_image_handle_get_metadata (handle, metadata_id, xmp_data); + if (err.code != 0) + { + xmp_data = NULL; + xmp_data_size = 0; + } + } + } + + if (exif_data || xmp_data) + { + GimpMetadata *metadata = gimp_metadata_new (); + GimpMetadataLoadFlags flags = GIMP_METADATA_LOAD_COMMENT | GIMP_METADATA_LOAD_RESOLUTION; + + if (exif_data) + { + const guint8 tiffHeaderBE[4] = { 'M', 'M', 0, 42 }; + const guint8 tiffHeaderLE[4] = { 'I', 'I', 42, 0 }; + GExiv2Metadata *exif_metadata = GEXIV2_METADATA (metadata); + const guint8 *tiffheader = exif_data; + glong new_exif_size = exif_data_size; + + while (new_exif_size >= 4) /*Searching for TIFF Header*/ + { + if (tiffheader[0] == tiffHeaderBE[0] && tiffheader[1] == tiffHeaderBE[1] && + tiffheader[2] == tiffHeaderBE[2] && tiffheader[3] == tiffHeaderBE[3]) + { + break; + } + if (tiffheader[0] == tiffHeaderLE[0] && tiffheader[1] == tiffHeaderLE[1] && + tiffheader[2] == tiffHeaderLE[2] && tiffheader[3] == tiffHeaderLE[3]) + { + break; + } + new_exif_size--; + tiffheader++; + } + + if (new_exif_size > 4) /* TIFF header + some data found*/ + { + if (! gexiv2_metadata_open_buf (exif_metadata, tiffheader, new_exif_size, error)) + { + g_printerr ("%s: Failed to set EXIF metadata: %s\n", G_STRFUNC, (*error)->message); + g_clear_error (error); + } + } + else + { + g_printerr ("%s: EXIF metadata not set\n", G_STRFUNC); + } + } + + + if (xmp_data) + { + if (!gimp_metadata_set_from_xmp (metadata, xmp_data, xmp_data_size, error)) + { + g_printerr ("%s: Failed to set XMP metadata: %s\n", G_STRFUNC, (*error)->message); + g_clear_error (error); + } + } + + gexiv2_metadata_set_orientation (GEXIV2_METADATA (metadata), + GEXIV2_ORIENTATION_NORMAL); + gexiv2_metadata_set_metadata_pixel_width (GEXIV2_METADATA (metadata), + width); + gexiv2_metadata_set_metadata_pixel_height (GEXIV2_METADATA (metadata), + height); + gimp_image_metadata_load_finish (image_ID, "image/heif", + metadata, flags, + interactive); + } + } + + if (profile) + g_object_unref (profile); + + heif_image_handle_release (handle); + heif_context_free (ctx); + heif_image_release (img); + + gimp_progress_update (1.0); + + return image_ID; +} + +static struct heif_error +write_callback (struct heif_context *ctx, + const void *data, + size_t size, + void *userdata) +{ + GOutputStream *output = userdata; + GError *error = NULL; + struct heif_error heif_error; + + heif_error.code = heif_error_Ok; + heif_error.subcode = heif_suberror_Unspecified; + heif_error.message = ""; + + if (! g_output_stream_write_all (output, data, size, NULL, NULL, &error)) + { + heif_error.code = 99; /* hmm */ + heif_error.message = error->message; + } + + return heif_error; +} + +static gboolean +save_image (GFile *file, + gint32 image_ID, + gint32 drawable_ID, + const SaveParams *params, + GError **error, + enum heif_compression_format compression) +{ + struct heif_image *image = NULL; + struct heif_context *context = heif_context_alloc (); + struct heif_encoder *encoder = NULL; + const struct heif_encoder_descriptor *encoder_descriptor; + const char *encoder_name; + struct heif_image_handle *handle = NULL; + struct heif_writer writer; + struct heif_error err; + GOutputStream *output; + GeglBuffer *buffer; + const gchar *encoding; + const Babl *format; + guint8 *data; + gint stride; + gint width; + gint height; + gboolean has_alpha; + gboolean out_linear = FALSE; + char *filename; + + if (!context) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "cannot allocate heif_context"); + return FALSE; + } + + if (compression == heif_compression_HEVC) + { + if (heif_context_get_encoder_descriptors (context, + heif_compression_HEVC, + NULL, + &encoder_descriptor, 1) == 1) + { + encoder_name = heif_encoder_descriptor_get_id_name (encoder_descriptor); + } + else + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unable to find suitable HEIF encoder"); + heif_context_free (context); + return FALSE; + } + } + else /* AV1 compression */ + { + if (heif_context_get_encoder_descriptors (context, + compression, + "aom", /* we prefer aom rather than rav1e */ + &encoder_descriptor, 1) == 1) + { + encoder_name = heif_encoder_descriptor_get_id_name (encoder_descriptor); + } + else if (heif_context_get_encoder_descriptors (context, + compression, + NULL, + &encoder_descriptor, 1) == 1) + { + encoder_name = heif_encoder_descriptor_get_id_name (encoder_descriptor); + } + else + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unable to find suitable AVIF encoder"); + heif_context_free (context); + return FALSE; + } + } + + filename = g_file_get_parse_name (file); + gimp_progress_init_printf (_("Exporting '%s' using %s encoder"), + filename, encoder_name); + g_free (filename); + + width = gimp_drawable_width (drawable_ID); + height = gimp_drawable_height (drawable_ID); + + has_alpha = gimp_drawable_has_alpha (drawable_ID); + + switch (params->save_bit_depth) + { + case 8: + err = heif_image_create (width, height, + heif_colorspace_RGB, + has_alpha ? + heif_chroma_interleaved_RGBA : + heif_chroma_interleaved_RGB, + &image); + break; +#if LIBHEIF_HAVE_VERSION(1,8,0) + case 10: + case 12: +#if ( G_BYTE_ORDER == G_LITTLE_ENDIAN ) + err = heif_image_create (width, height, + heif_colorspace_RGB, + has_alpha ? + heif_chroma_interleaved_RRGGBBAA_LE : + heif_chroma_interleaved_RRGGBB_LE, + &image); +#else + err = heif_image_create (width, height, + heif_colorspace_RGB, + has_alpha ? + heif_chroma_interleaved_RRGGBBAA_BE : + heif_chroma_interleaved_RRGGBB_BE, + &image); +#endif + break; +#endif + default: + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unsupported bit depth: %d", + params->save_bit_depth); + heif_context_free (context); + return FALSE; + break; + } + + if (err.code != 0) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Encoding HEIF image failed: %s"), + err.message); + heif_context_free (context); + return FALSE; + } + +#if LIBHEIF_HAVE_VERSION(1,4,0) + if (params->save_profile) + { + GimpColorProfile *profile = NULL; + const guint8 *icc_data; + gsize icc_length; + + profile = gimp_image_get_color_profile (image_ID); + if (profile && gimp_color_profile_is_linear (profile)) + out_linear = TRUE; + + if (! profile) + { + profile = gimp_image_get_effective_color_profile (image_ID); + + if (gimp_color_profile_is_linear (profile)) + { + if (gimp_image_get_precision (image_ID) != GIMP_PRECISION_U8_LINEAR) + { + /* If stored data was linear, let's convert the profile. */ + GimpColorProfile *saved_profile; + + saved_profile = gimp_color_profile_new_srgb_trc_from_color_profile (profile); + g_clear_object (&profile); + profile = saved_profile; + } + else + { + /* Keep linear profile as-is for 8-bit linear image. */ + out_linear = TRUE; + } + } + } + + icc_data = gimp_color_profile_get_icc_profile (profile, &icc_length); + heif_image_set_raw_color_profile (image, "prof", icc_data, icc_length); + + g_object_unref (profile); + } + else + { +#if LIBHEIF_HAVE_VERSION(1,8,0) + /* We save as sRGB */ + struct heif_color_profile_nclx nclx_profile; + + nclx_profile.version = 1; + nclx_profile.color_primaries = heif_color_primaries_ITU_R_BT_709_5; + nclx_profile.transfer_characteristics = heif_transfer_characteristic_IEC_61966_2_1; + nclx_profile.matrix_coefficients = heif_matrix_coefficients_ITU_R_BT_601_6; + nclx_profile.full_range_flag = 1; + + heif_image_set_nclx_color_profile (image, &nclx_profile); + + out_linear = FALSE; +#endif + } +#endif /* LIBHEIF_HAVE_VERSION(1,4,0) */ + + if (params->save_bit_depth > 8) + { + uint16_t *data16; + const uint16_t *src16; + uint16_t *dest16; + gint x, y, rowentries; + int tmp_pixelval; + + if (has_alpha) + { + rowentries = width * 4; + + if (out_linear) + encoding = "RGBA u16"; + else + encoding = "R'G'B'A u16"; + } + else /* no alpha */ + { + rowentries = width * 3; + + if (out_linear) + encoding = "RGB u16"; + else + encoding = "R'G'B' u16"; + } + + data16 = g_malloc_n (height, rowentries * 2); + src16 = data16; + + format = babl_format (encoding); + + buffer = gimp_drawable_get_buffer (drawable_ID); + + gegl_buffer_get (buffer, + GEGL_RECTANGLE (0, 0, width, height), + 1.0, format, data16, GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + g_object_unref (buffer); + + heif_image_add_plane (image, heif_channel_interleaved, + width, height, params->save_bit_depth); + + data = heif_image_get_plane (image, heif_channel_interleaved, &stride); + + switch (params->save_bit_depth) + { + case 10: + for (y = 0; y < height; y++) + { + dest16 = (uint16_t *) (y * stride + data); + for (x = 0; x < rowentries; x++) + { + tmp_pixelval = (int) ( ( (float) (*src16) / 65535.0f) * 1023.0f + 0.5f); + *dest16 = CLAMP (tmp_pixelval, 0, 1023); + dest16++; + src16++; + } + } + break; + case 12: + for (y = 0; y < height; y++) + { + dest16 = (uint16_t *) (y * stride + data); + for (x = 0; x < rowentries; x++) + { + tmp_pixelval = (int) ( ( (float) (*src16) / 65535.0f) * 4095.0f + 0.5f); + *dest16 = CLAMP (tmp_pixelval, 0, 4095); + dest16++; + src16++; + } + } + break; + default: + for (y = 0; y < height; y++) + { + dest16 = (uint16_t *) (y * stride + data); + for (x = 0; x < rowentries; x++) + { + *dest16 = *src16; + dest16++; + src16++; + } + } + break; + } + g_free (data16); + } + else /* save_bit_depth == 8 */ + { +#if LIBHEIF_HAVE_VERSION(1,8,0) + heif_image_add_plane (image, heif_channel_interleaved, + width, height, 8); +#else + /* old style settings */ + heif_image_add_plane (image, heif_channel_interleaved, + width, height, has_alpha ? 32 : 24); +#endif + + data = heif_image_get_plane (image, heif_channel_interleaved, &stride); + + buffer = gimp_drawable_get_buffer (drawable_ID); + + if (has_alpha) + { + if (out_linear) + encoding = "RGBA u8"; + else + encoding = "R'G'B'A u8"; + } + else + { + if (out_linear) + encoding = "RGB u8"; + else + encoding = "R'G'B' u8"; + } + format = babl_format (encoding); + + gegl_buffer_get (buffer, + GEGL_RECTANGLE (0, 0, width, height), + 1.0, format, data, stride, GEGL_ABYSS_NONE); + + g_object_unref (buffer); + } + + gimp_progress_update (0.33); + + /* encode to HEIF file */ + + err = heif_context_get_encoder (context, + encoder_descriptor, + &encoder); + + if (err.code != 0) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unable to get an encoder instance"); + heif_image_release (image); + heif_context_free (context); + return FALSE; + } + + /* workaround for a bug in libheif when heif_encoder_set_lossless is not working + (known problem with encoding via rav1e) */ + if (params->lossless) + { + heif_encoder_set_lossy_quality (encoder, 100); + } + else + { + heif_encoder_set_lossy_quality (encoder, params->quality); + } + + heif_encoder_set_lossless (encoder, params->lossless); + /* heif_encoder_set_logging_level (encoder, logging_level); */ + +#if LIBHEIF_HAVE_VERSION(1,10,0) + if (params->lossless) + { + err = heif_encoder_set_parameter_string (encoder, "chroma", "444"); + if (err.code != 0) + { + g_printerr ("Failed to set chroma=444 for %s encoder: %s", encoder_name, err.message); + } + } +#endif + + err = heif_context_encode_image (context, + image, + encoder, + NULL, + &handle); + if (err.code != 0) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Encoding HEIF image failed: %s"), + err.message); + heif_encoder_release (encoder); + heif_image_release (image); + heif_context_free (context); + return FALSE; + } + + heif_image_handle_release (handle); + + gimp_progress_update (0.66); + + writer.writer_api_version = 1; + writer.write = write_callback; + + output = G_OUTPUT_STREAM (g_file_replace (file, + NULL, FALSE, G_FILE_CREATE_NONE, + NULL, error)); + if (! output) + { + heif_encoder_release (encoder); + heif_image_release (image); + heif_context_free (context); + return FALSE; + } + + err = heif_context_write (context, &writer, output); + + if (err.code != 0) + { + GCancellable *cancellable = g_cancellable_new (); + + g_cancellable_cancel (cancellable); + g_output_stream_close (output, cancellable, NULL); + g_object_unref (cancellable); + + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + _("Writing HEIF image failed: %s"), + err.message); + + heif_encoder_release (encoder); + heif_image_release (image); + heif_context_free (context); + return FALSE; + } + + g_object_unref (output); + + heif_encoder_release (encoder); + heif_image_release (image); + heif_context_free (context); + + gimp_progress_update (1.0); + + return TRUE; +} + + +/* the load dialog */ + +#define MAX_THUMBNAIL_SIZE 320 + +typedef struct _HeifImage HeifImage; + +struct _HeifImage +{ + uint32_t ID; + gchar caption[100]; + struct heif_image *thumbnail; + gint width; + gint height; +}; + +static gboolean +load_thumbnails (struct heif_context *heif, + HeifImage *images) +{ + guint32 *IDs; + gint n_images; + gint i; + + n_images = heif_context_get_number_of_top_level_images (heif); + + /* get list of all (top level) image IDs */ + + IDs = g_alloca (n_images * sizeof (guint32)); + + heif_context_get_list_of_top_level_image_IDs (heif, IDs, n_images); + + + /* Load a thumbnail for each image. */ + + for (i = 0; i < n_images; i++) + { + struct heif_image_handle *handle = NULL; + struct heif_error err; + gint width; + gint height; + struct heif_image_handle *thumbnail_handle = NULL; + heif_item_id thumbnail_ID; + gint n_thumbnails; + struct heif_image *thumbnail_img = NULL; + gint thumbnail_width; + gint thumbnail_height; + + images[i].ID = IDs[i]; + images[i].caption[0] = 0; + images[i].thumbnail = NULL; + + /* get image handle */ + + err = heif_context_get_image_handle (heif, IDs[i], &handle); + if (err.code) + { + gimp_message (err.message); + continue; + } + + /* generate image caption */ + + width = heif_image_handle_get_width (handle); + height = heif_image_handle_get_height (handle); + + if (heif_image_handle_is_primary_image (handle)) + { + g_snprintf (images[i].caption, sizeof (images[i].caption), + "%dx%d (%s)", width, height, _("primary")); + } + else + { + g_snprintf (images[i].caption, sizeof (images[i].caption), + "%dx%d", width, height); + } + + /* get handle to thumbnail image + * + * if there is no thumbnail image, just the the image itself + * (will be scaled down later) + */ + + n_thumbnails = heif_image_handle_get_list_of_thumbnail_IDs (handle, + &thumbnail_ID, + 1); + + if (n_thumbnails > 0) + { + err = heif_image_handle_get_thumbnail (handle, thumbnail_ID, + &thumbnail_handle); + if (err.code) + { + gimp_message (err.message); + continue; + } + } + else + { + err = heif_context_get_image_handle (heif, IDs[i], &thumbnail_handle); + if (err.code) + { + gimp_message (err.message); + continue; + } + } + + /* decode the thumbnail image */ + + err = heif_decode_image (thumbnail_handle, + &thumbnail_img, + heif_colorspace_RGB, + heif_chroma_interleaved_RGB, + NULL); + if (err.code) + { + gimp_message (err.message); + continue; + } + + /* if thumbnail image size exceeds the maximum, scale it down */ + + thumbnail_width = heif_image_handle_get_width (thumbnail_handle); + thumbnail_height = heif_image_handle_get_height (thumbnail_handle); + + if (thumbnail_width > MAX_THUMBNAIL_SIZE || + thumbnail_height > MAX_THUMBNAIL_SIZE) + { + /* compute scaling factor to fit into a max sized box */ + + gfloat factor_h = thumbnail_width / (gfloat) MAX_THUMBNAIL_SIZE; + gfloat factor_v = thumbnail_height / (gfloat) MAX_THUMBNAIL_SIZE; + gint new_width, new_height; + struct heif_image *scaled_img = NULL; + + if (factor_v > factor_h) + { + new_height = MAX_THUMBNAIL_SIZE; + new_width = thumbnail_width / factor_v; + } + else + { + new_height = thumbnail_height / factor_h; + new_width = MAX_THUMBNAIL_SIZE; + } + + /* scale the image */ + + err = heif_image_scale_image (thumbnail_img, + &scaled_img, + new_width, new_height, + NULL); + if (err.code) + { + gimp_message (err.message); + continue; + } + + /* release the old image and only keep the scaled down version */ + + heif_image_release (thumbnail_img); + thumbnail_img = scaled_img; + + thumbnail_width = new_width; + thumbnail_height = new_height; + } + + heif_image_handle_release (thumbnail_handle); + heif_image_handle_release (handle); + + /* remember the HEIF thumbnail image (we need it for the GdkPixbuf) */ + + images[i].thumbnail = thumbnail_img; + + images[i].width = thumbnail_width; + images[i].height = thumbnail_height; + } + + return TRUE; +} + +static void +load_dialog_item_activated (GtkIconView *icon_view, + GtkTreePath *path, + GtkDialog *dialog) +{ + gtk_dialog_response (dialog, GTK_RESPONSE_OK); +} + +static gboolean +load_dialog (struct heif_context *heif, + uint32_t *selected_image) +{ + GtkWidget *dialog; + GtkWidget *main_vbox; + GtkWidget *frame; + HeifImage *heif_images; + GtkListStore *list_store; + GtkTreeIter iter; + GtkWidget *scrolled_window; + GtkWidget *icon_view; + GtkCellRenderer *renderer; + gint n_images; + gint i; + gint selected_idx = -1; + gboolean run = FALSE; + + n_images = heif_context_get_number_of_top_level_images (heif); + + heif_images = g_alloca (n_images * sizeof (HeifImage)); + + if (! load_thumbnails (heif, heif_images)) + return FALSE; + + dialog = gimp_dialog_new (_("Load HEIF Image"), PLUG_IN_BINARY, + NULL, 0, + gimp_standard_help_func, LOAD_PROC, + + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_OK"), GTK_RESPONSE_OK, + + NULL); + + main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12); + gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), + main_vbox, TRUE, TRUE, 0); + + frame = gimp_frame_new (_("Select Image")); + gtk_box_pack_start (GTK_BOX (main_vbox), frame, TRUE, TRUE, 0); + gtk_widget_show (frame); + + /* prepare list store with all thumbnails and caption */ + + list_store = gtk_list_store_new (2, G_TYPE_STRING, GDK_TYPE_PIXBUF); + + for (i = 0; i < n_images; i++) + { + GdkPixbuf *pixbuf; + const guint8 *data; + gint stride; + + gtk_list_store_append (list_store, &iter); + gtk_list_store_set (list_store, &iter, 0, heif_images[i].caption, -1); + + data = heif_image_get_plane_readonly (heif_images[i].thumbnail, + heif_channel_interleaved, + &stride); + + pixbuf = gdk_pixbuf_new_from_data (data, + GDK_COLORSPACE_RGB, + FALSE, + 8, + heif_images[i].width, + heif_images[i].height, + stride, + NULL, + NULL); + + gtk_list_store_set (list_store, &iter, 1, pixbuf, -1); + } + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_SHADOW_IN); + gtk_widget_set_size_request (scrolled_window, + 2 * MAX_THUMBNAIL_SIZE, + 1.5 * MAX_THUMBNAIL_SIZE); + gtk_container_add (GTK_CONTAINER (frame), scrolled_window); + gtk_widget_show (scrolled_window); + + icon_view = gtk_icon_view_new_with_model (GTK_TREE_MODEL (list_store)); + gtk_container_add (GTK_CONTAINER (scrolled_window), icon_view); + gtk_widget_show (icon_view); + + renderer = gtk_cell_renderer_pixbuf_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (icon_view), renderer, FALSE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (icon_view), renderer, + "pixbuf", 1, + NULL); + + renderer = gtk_cell_renderer_text_new (); + gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (icon_view), renderer, FALSE); + gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (icon_view), renderer, + "text", 0, + NULL); + g_object_set (renderer, + "alignment", PANGO_ALIGN_CENTER, + "wrap-mode", PANGO_WRAP_WORD_CHAR, + "xalign", 0.5, + "yalign", 0.0, + NULL); + + g_signal_connect (icon_view, "item-activated", + G_CALLBACK (load_dialog_item_activated), + dialog); + + /* pre-select the primary image */ + + for (i = 0; i < n_images; i++) + { + if (heif_images[i].ID == *selected_image) + { + selected_idx = i; + break; + } + } + + if (selected_idx != -1) + { + GtkTreePath *path = gtk_tree_path_new_from_indices (selected_idx, -1); + + gtk_icon_view_select_path (GTK_ICON_VIEW (icon_view), path); + gtk_tree_path_free (path); + } + + gtk_widget_show (main_vbox); + gtk_widget_show (dialog); + + run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK); + + if (run) + { + GList *selected_items = + gtk_icon_view_get_selected_items (GTK_ICON_VIEW (icon_view)); + + if (selected_items) + { + GtkTreePath *path = selected_items->data; + gint *indices = gtk_tree_path_get_indices (path); + + *selected_image = heif_images[indices[0]].ID; + + g_list_free_full (selected_items, + (GDestroyNotify) gtk_tree_path_free); + } + } + + gtk_widget_destroy (dialog); + + /* release thumbnail images */ + + for (i = 0 ; i < n_images; i++) + heif_image_release (heif_images[i].thumbnail); + + return run; +} + + +/* the save dialog */ + +static void +save_dialog_lossless_button_toggled (GtkToggleButton *source, + GtkWidget *hbox) +{ + gboolean lossless = gtk_toggle_button_get_active (source); + + gtk_widget_set_sensitive (hbox, ! lossless); +} + +gboolean +save_dialog (SaveParams *params, + gint32 image_ID, + const gchar *procname) +{ + GtkWidget *dialog; + GtkWidget *main_vbox; + GtkWidget *hbox; + GtkWidget *label; + GtkWidget *lossless_button; + GtkWidget *frame; +#if LIBHEIF_HAVE_VERSION(1,4,0) + GtkWidget *profile_button; +#endif + GtkWidget *quality_slider; +#if LIBHEIF_HAVE_VERSION(1,8,0) + GtkWidget *table; + GtkWidget *label2; + GtkWidget *combo; +#endif + gboolean run = FALSE; + + dialog = gimp_export_dialog_new (g_strcmp0 (procname, SAVE_PROC_AV1) == 0? + "AVIF" : "HEIF", + PLUG_IN_BINARY, procname); + + main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (main_vbox), 12); + gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (dialog)), + main_vbox, TRUE, TRUE, 0); + + frame = gimp_frame_new (NULL); + gtk_box_pack_start (GTK_BOX (main_vbox), frame, FALSE, FALSE, 0); + + lossless_button = gtk_check_button_new_with_mnemonic (_("Nearly _lossless")); + gtk_frame_set_label_widget (GTK_FRAME (frame), lossless_button); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6); + label = gtk_label_new_with_mnemonic (_("_Quality:")); + quality_slider = gtk_hscale_new_with_range (0, 100, 5); + gtk_scale_set_value_pos (GTK_SCALE (quality_slider), GTK_POS_RIGHT); + gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (hbox), quality_slider, TRUE, TRUE, 0); + gtk_container_add (GTK_CONTAINER (frame), hbox); + + gtk_range_set_value (GTK_RANGE (quality_slider), params->quality); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (lossless_button), + params->lossless); + gtk_widget_set_sensitive (hbox, !params->lossless); + gtk_label_set_mnemonic_widget (GTK_LABEL (label), quality_slider); + + g_signal_connect (lossless_button, "toggled", + G_CALLBACK (save_dialog_lossless_button_toggled), + hbox); + +#if LIBHEIF_HAVE_VERSION(1,8,0) + switch (gimp_image_get_precision (image_ID)) + { + case GIMP_PRECISION_U8_LINEAR: + case GIMP_PRECISION_U8_GAMMA: + /* image is 8bit depth */ + params->save_bit_depth = 8; + break; + default: + /* high bit depth */ + if (params->save_bit_depth < 12) + { + params->save_bit_depth = 10; + } + else if (params->save_bit_depth > 12) + { + params->save_bit_depth = 12; + } + break; + } + + table = gtk_table_new (1, 2, FALSE); + gtk_table_set_col_spacings (GTK_TABLE (table), 6); + gtk_box_pack_start (GTK_BOX (main_vbox), table, FALSE, FALSE, 0); + gtk_widget_show (table); + + label2 = gtk_label_new (_("Bit depth:")); + gtk_label_set_xalign (GTK_LABEL (label2), 0.0); + gtk_table_attach (GTK_TABLE (table), label2, 0, 1, 0, 1, + GTK_FILL, GTK_FILL, 0, 0); + gtk_widget_show (label2); + + combo = gimp_int_combo_box_new (_("8 bit/channel"), 8, + _("10 bit/channel"), 10, + _("12 bit/channel"), 12, + NULL); + gtk_table_attach (GTK_TABLE (table), combo, 1, 2, 0, 1, + GTK_FILL | GTK_EXPAND, GTK_FILL, 0, 0); + gtk_widget_show (combo); + gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (combo), params->save_bit_depth); +#endif + +#if LIBHEIF_HAVE_VERSION(1,4,0) + profile_button = gtk_check_button_new_with_mnemonic (_("Save color _profile")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (profile_button), + params->save_profile); + g_signal_connect (profile_button, "toggled", + G_CALLBACK (gimp_toggle_button_update), + ¶ms->save_profile); + gtk_box_pack_start (GTK_BOX (main_vbox), profile_button, FALSE, FALSE, 0); +#endif + + gtk_widget_show_all (dialog); + + run = (gimp_dialog_run (GIMP_DIALOG (dialog)) == GTK_RESPONSE_OK); + + if (run) + { + params->quality = gtk_range_get_value (GTK_RANGE (quality_slider)); + params->lossless = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (lossless_button)); +#if LIBHEIF_HAVE_VERSION(1,8,0) + gimp_int_combo_box_get_active (GIMP_INT_COMBO_BOX (combo), ¶ms->save_bit_depth); +#endif + } + + gtk_widget_destroy (dialog); + + return run; +} |