/* * PIKA HEIF loader / write plugin. * Copyright (c) 2018 struktur AG, Dirk Farin * * 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 . */ #include "config.h" #include #include #include #include #include #include #include "libpika/stdplugins-intl.h" #define LOAD_PROC "file-heif-load" #define LOAD_PROC_AV1 "file-heif-av1-load" #define SAVE_PROC "file-heif-save" #define SAVE_PROC_AV1 "file-heif-av1-save" #define PLUG_IN_BINARY "file-heif" typedef enum _HeifpluginEncoderSpeed { HEIFPLUGIN_ENCODER_SPEED_SLOW = 0, HEIFPLUGIN_ENCODER_SPEED_BALANCED = 1, HEIFPLUGIN_ENCODER_SPEED_FASTER = 2 } HeifpluginEncoderSpeed; typedef enum _HeifpluginExportFormat { HEIFPLUGIN_EXPORT_FORMAT_RGB = 0, HEIFPLUGIN_EXPORT_FORMAT_YUV444 = 1, HEIFPLUGIN_EXPORT_FORMAT_YUV422 = 2, HEIFPLUGIN_EXPORT_FORMAT_YUV420 = 3 } HeifpluginExportFormat; typedef struct _PikaHeif PikaHeif; typedef struct _PikaHeifClass PikaHeifClass; struct _PikaHeif { PikaPlugIn parent_instance; }; struct _PikaHeifClass { PikaPlugInClass parent_class; }; #define PIKA_HEIF_TYPE (pika_heif_get_type ()) #define PIKA_HEIF (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), PIKA_HEIF_TYPE, PikaHeif)) GType pika_heif_get_type (void) G_GNUC_CONST; static GList * heif_init_procedures (PikaPlugIn *plug_in); static PikaProcedure * heif_create_procedure (PikaPlugIn *plug_in, const gchar *name); static PikaValueArray * heif_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, const PikaValueArray *args, gpointer run_data); static PikaValueArray * heif_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, const PikaValueArray *args, gpointer run_data); #if LIBHEIF_HAVE_VERSION(1,8,0) static PikaValueArray * heif_av1_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, const PikaValueArray *args, gpointer run_data); #endif static PikaImage * load_image (GFile *file, gboolean interactive, PikaPDBStatusType *status, GError **error); static gboolean save_image (GFile *file, PikaImage *image, PikaDrawable *drawable, GObject *config, GError **error, enum heif_compression_format compression, PikaMetadata *metadata); static gboolean load_dialog (struct heif_context *heif, uint32_t *selected_image); static gboolean save_dialog (PikaProcedure *procedure, GObject *config, PikaImage *image); G_DEFINE_TYPE (PikaHeif, pika_heif, PIKA_TYPE_PLUG_IN) PIKA_MAIN (PIKA_HEIF_TYPE) DEFINE_STD_SET_I18N static void pika_heif_class_init (PikaHeifClass *klass) { PikaPlugInClass *plug_in_class = PIKA_PLUG_IN_CLASS (klass); plug_in_class->init_procedures = heif_init_procedures; plug_in_class->create_procedure = heif_create_procedure; plug_in_class->set_i18n = STD_SET_I18N; } static void pika_heif_init (PikaHeif *heif) { } static GList * heif_init_procedures (PikaPlugIn *plug_in) { GList *list = NULL; #if LIBHEIF_HAVE_VERSION(1,13,0) heif_init (NULL); #endif if (heif_have_decoder_for_format (heif_compression_HEVC)) { list = g_list_append (list, g_strdup (LOAD_PROC)); } if (heif_have_encoder_for_format (heif_compression_HEVC)) { list = g_list_append (list, g_strdup (SAVE_PROC)); } #if LIBHEIF_HAVE_VERSION(1,8,0) if (heif_have_decoder_for_format (heif_compression_AV1)) { list = g_list_append (list, g_strdup (LOAD_PROC_AV1)); } if (heif_have_encoder_for_format (heif_compression_AV1)) { list = g_list_append (list, g_strdup (SAVE_PROC_AV1)); } #endif #if LIBHEIF_HAVE_VERSION(1,13,0) heif_deinit (); #endif return list; } static PikaProcedure * heif_create_procedure (PikaPlugIn *plug_in, const gchar *name) { PikaProcedure *procedure = NULL; if (! strcmp (name, LOAD_PROC)) { procedure = pika_load_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, heif_load, NULL, NULL); pika_procedure_set_menu_label (procedure, _("HEIF/HEIC")); pika_procedure_set_documentation (procedure, _("Loads HEIF images"), _("Load image stored in HEIF format (High " "Efficiency Image File Format). Typical " "suffices for HEIF files are .heif, " ".heic."), name); pika_procedure_set_attribution (procedure, "Dirk Farin ", "Dirk Farin ", "2018"); pika_file_procedure_set_handles_remote (PIKA_FILE_PROCEDURE (procedure), TRUE); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/heif"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "heif,heic"); /* HEIF is an ISOBMFF format whose "brand" (the value after "ftyp") * can be of various values. * See also: https://gitlab.gnome.org/GNOME/pika/issues/2209 */ pika_file_procedure_set_magics (PIKA_FILE_PROCEDURE (procedure), "4,string,ftypheic,4,string,ftypheix," "4,string,ftyphevc,4,string,ftypheim," "4,string,ftypheis,4,string,ftyphevm," "4,string,ftyphevs,4,string,ftypmif1," "4,string,ftypmsf1"); } else if (! strcmp (name, SAVE_PROC)) { procedure = pika_save_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, heif_save, NULL, NULL); pika_procedure_set_image_types (procedure, "RGB*"); pika_procedure_set_menu_label (procedure, _("HEIF/HEIC")); pika_file_procedure_set_format_name (PIKA_FILE_PROCEDURE (procedure), "HEIF"); pika_procedure_set_documentation (procedure, _("Exports HEIF images"), _("Save image in HEIF format (High " "Efficiency Image File Format)."), name); pika_procedure_set_attribution (procedure, "Dirk Farin ", "Dirk Farin ", "2018"); pika_file_procedure_set_handles_remote (PIKA_FILE_PROCEDURE (procedure), TRUE); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/heif"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "heif,heic"); PIKA_PROC_ARG_INT (procedure, "quality", _("_Quality"), _("Quality factor (0 = worst, 100 = best)"), 0, 100, 50, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "lossless", _("L_ossless"), _("Use lossless compression"), FALSE, G_PARAM_READWRITE); PIKA_PROC_AUX_ARG_BOOLEAN (procedure, "save-color-profile", _("Save color prof_ile"), _("Save the image's color profile"), pika_export_color_profile (), G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "save-bit-depth", _("_Bit depth"), _("Bit depth of exported image"), 8, 12, 8, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "pixel-format", _("_Pixel format"), _("Format of color sub-sampling"), HEIFPLUGIN_EXPORT_FORMAT_RGB, HEIFPLUGIN_EXPORT_FORMAT_YUV420, HEIFPLUGIN_EXPORT_FORMAT_YUV420, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "encoder-speed", _("Enco_der speed"), _("Tradeoff between speed and compression"), HEIFPLUGIN_ENCODER_SPEED_SLOW, HEIFPLUGIN_ENCODER_SPEED_FASTER, HEIFPLUGIN_ENCODER_SPEED_BALANCED, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "save-exif", _("Save Exi_f"), _("Toggle saving Exif data"), pika_export_exif (), G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "save-xmp", _("Save _XMP"), _("Toggle saving XMP data"), pika_export_xmp (), G_PARAM_READWRITE); } #if LIBHEIF_HAVE_VERSION(1,8,0) else if (! strcmp (name, LOAD_PROC_AV1)) { procedure = pika_load_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, heif_load, NULL, NULL); pika_procedure_set_menu_label (procedure, "HEIF/AVIF"); pika_procedure_set_documentation (procedure, _("Loads AVIF images"), _("Load image stored in AV1 Image File Format (AVIF)"), name); pika_procedure_set_attribution (procedure, "Daniel Novomesky ", "Daniel Novomesky ", "2020"); pika_file_procedure_set_handles_remote (PIKA_FILE_PROCEDURE (procedure), TRUE); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/avif"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "avif"); pika_file_procedure_set_magics (PIKA_FILE_PROCEDURE (procedure), "4,string,ftypmif1,4,string,ftypavif"); pika_file_procedure_set_priority (PIKA_FILE_PROCEDURE (procedure), 100); } else if (! strcmp (name, SAVE_PROC_AV1)) { procedure = pika_save_procedure_new (plug_in, name, PIKA_PDB_PROC_TYPE_PLUGIN, heif_av1_save, NULL, NULL); pika_procedure_set_image_types (procedure, "RGB*"); pika_procedure_set_menu_label (procedure, "HEIF/AVIF"); pika_file_procedure_set_format_name (PIKA_FILE_PROCEDURE (procedure), "AVIF"); pika_procedure_set_documentation (procedure, _("Exports AVIF images"), _("Save image in AV1 Image File Format (AVIF)"), name); pika_procedure_set_attribution (procedure, "Daniel Novomesky ", "Daniel Novomesky ", "2020"); pika_file_procedure_set_handles_remote (PIKA_FILE_PROCEDURE (procedure), TRUE); pika_file_procedure_set_mime_types (PIKA_FILE_PROCEDURE (procedure), "image/avif"); pika_file_procedure_set_extensions (PIKA_FILE_PROCEDURE (procedure), "avif"); pika_file_procedure_set_priority (PIKA_FILE_PROCEDURE (procedure), 100); PIKA_PROC_ARG_INT (procedure, "quality", _("_Quality"), _("Quality factor (0 = worst, 100 = best)"), 0, 100, 50, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "lossless", _("L_ossless"), _("Use lossless compression"), FALSE, G_PARAM_READWRITE); PIKA_PROC_AUX_ARG_BOOLEAN (procedure, "save-color-profile", _("Save color prof_ile"), _("Save the image's color profile"), pika_export_color_profile (), G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "save-bit-depth", _("_Bit depth"), _("Bit depth of exported image"), 8, 12, 8, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "pixel-format", _("_Pixel format"), _("Format of color sub-sampling"), HEIFPLUGIN_EXPORT_FORMAT_RGB, HEIFPLUGIN_EXPORT_FORMAT_YUV420, HEIFPLUGIN_EXPORT_FORMAT_YUV420, G_PARAM_READWRITE); PIKA_PROC_ARG_INT (procedure, "encoder-speed", _("Enco_der speed"), _("Tradeoff between speed and compression"), HEIFPLUGIN_ENCODER_SPEED_SLOW, HEIFPLUGIN_ENCODER_SPEED_FASTER, HEIFPLUGIN_ENCODER_SPEED_BALANCED, G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "save-exif", _("Save Exi_f"), _("Toggle saving Exif data"), pika_export_exif (), G_PARAM_READWRITE); PIKA_PROC_ARG_BOOLEAN (procedure, "save-xmp", _("Save _XMP"), _("Toggle saving XMP data"), pika_export_xmp (), G_PARAM_READWRITE); } #endif return procedure; } static PikaValueArray * heif_load (PikaProcedure *procedure, PikaRunMode run_mode, GFile *file, const PikaValueArray *args, gpointer run_data) { PikaValueArray *return_vals; PikaPDBStatusType status = PIKA_PDB_SUCCESS; PikaImage *image; gboolean interactive; GError *error = NULL; gegl_init (NULL, NULL); interactive = (run_mode == PIKA_RUN_INTERACTIVE); if (interactive) pika_ui_init (PLUG_IN_BINARY); #if LIBHEIF_HAVE_VERSION(1,13,0) heif_init (NULL); #endif image = load_image (file, interactive, &status, &error); #if LIBHEIF_HAVE_VERSION(1,13,0) heif_deinit (); #endif if (! image) return pika_procedure_new_return_values (procedure, status, error); return_vals = pika_procedure_new_return_values (procedure, PIKA_PDB_SUCCESS, NULL); PIKA_VALUES_SET_IMAGE (return_vals, 1, image); return return_vals; } static PikaValueArray * heif_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, const PikaValueArray *args, gpointer run_data) { PikaProcedureConfig *config; PikaPDBStatusType status = PIKA_PDB_SUCCESS; PikaExportReturn export = PIKA_EXPORT_CANCEL; PikaMetadata *metadata; GError *error = NULL; gegl_init (NULL, NULL); config = pika_procedure_create_config (procedure); pika_procedure_config_begin_run (config, image, run_mode, args); switch (run_mode) { case PIKA_RUN_INTERACTIVE: case PIKA_RUN_WITH_LAST_VALS: pika_ui_init (PLUG_IN_BINARY); export = pika_export_image (&image, &n_drawables, &drawables, "HEIF", PIKA_EXPORT_CAN_HANDLE_RGB | PIKA_EXPORT_CAN_HANDLE_ALPHA); if (export == PIKA_EXPORT_CANCEL) return pika_procedure_new_return_values (procedure, PIKA_PDB_CANCEL, NULL); break; default: break; } if (n_drawables != 1) { g_set_error (&error, G_FILE_ERROR, 0, _("HEIF format does not support multiple layers.")); return pika_procedure_new_return_values (procedure, PIKA_PDB_CALLING_ERROR, error); } if (run_mode == PIKA_RUN_INTERACTIVE) { if (! save_dialog (procedure, G_OBJECT (config), image)) status = PIKA_PDB_CANCEL; } if (status == PIKA_PDB_SUCCESS) { PikaMetadataSaveFlags metadata_flags; metadata = pika_image_metadata_save_prepare (image, "image/heif", &metadata_flags); #if LIBHEIF_HAVE_VERSION(1,13,0) heif_init (NULL); #endif if (! save_image (file, image, drawables[0], G_OBJECT (config), &error, heif_compression_HEVC, metadata)) { status = PIKA_PDB_EXECUTION_ERROR; } #if LIBHEIF_HAVE_VERSION(1,13,0) heif_deinit (); #endif if (metadata) { g_object_unref (metadata); } } pika_procedure_config_end_run (config, status); g_object_unref (config); if (export == PIKA_EXPORT_EXPORT) { pika_image_delete (image); g_free (drawables); } return pika_procedure_new_return_values (procedure, status, error); } #if LIBHEIF_HAVE_VERSION(1,8,0) static PikaValueArray * heif_av1_save (PikaProcedure *procedure, PikaRunMode run_mode, PikaImage *image, gint n_drawables, PikaDrawable **drawables, GFile *file, const PikaValueArray *args, gpointer run_data) { PikaProcedureConfig *config; PikaPDBStatusType status = PIKA_PDB_SUCCESS; PikaExportReturn export = PIKA_EXPORT_CANCEL; PikaMetadata *metadata; GError *error = NULL; gegl_init (NULL, NULL); config = pika_procedure_create_config (procedure); pika_procedure_config_begin_run (config, image, run_mode, args); switch (run_mode) { case PIKA_RUN_INTERACTIVE: case PIKA_RUN_WITH_LAST_VALS: pika_ui_init (PLUG_IN_BINARY); export = pika_export_image (&image, &n_drawables, &drawables, "AVIF", PIKA_EXPORT_CAN_HANDLE_RGB | PIKA_EXPORT_CAN_HANDLE_ALPHA); if (export == PIKA_EXPORT_CANCEL) return pika_procedure_new_return_values (procedure, PIKA_PDB_CANCEL, NULL); break; default: break; } if (n_drawables != 1) { g_set_error (&error, G_FILE_ERROR, 0, _("HEIF format does not support multiple layers.")); return pika_procedure_new_return_values (procedure, PIKA_PDB_CALLING_ERROR, error); } if (run_mode == PIKA_RUN_INTERACTIVE) { if (! save_dialog (procedure, G_OBJECT (config), image)) status = PIKA_PDB_CANCEL; } if (status == PIKA_PDB_SUCCESS) { PikaMetadataSaveFlags metadata_flags; metadata = pika_image_metadata_save_prepare (image, "image/avif", &metadata_flags); #if LIBHEIF_HAVE_VERSION(1,13,0) heif_init (NULL); #endif if (! save_image (file, image, drawables[0], G_OBJECT (config), &error, heif_compression_AV1, metadata)) { status = PIKA_PDB_EXECUTION_ERROR; } #if LIBHEIF_HAVE_VERSION(1,13,0) heif_deinit (); #endif if (metadata) { g_object_unref (metadata); } } pika_procedure_config_end_run (config, status); g_object_unref (config); if (export == PIKA_EXPORT_EXPORT) { pika_image_delete (image); g_free (drawables); } return pika_procedure_new_return_values (procedure, status, error); } #endif 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_attribute_uint64 (info, G_FILE_ATTRIBUTE_STANDARD_SIZE); 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 PikaColorProfile * nclx_to_pika_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 pika_color_profile_new_rgb_srgb(); } if (nclx->transfer_characteristics == heif_transfer_characteristic_linear) { return pika_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) { PikaColorProfile *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, "PIKA"); heifplugin_color_profile_set_tag (profile, cmsSigDeviceModelDescTag, description); heifplugin_color_profile_set_tag (profile, cmsSigCopyrightTag, "Public Domain"); new_profile = pika_color_profile_new_from_lcms_profile (profile, NULL); cmsCloseProfile (profile); g_free (description); return new_profile; } return NULL; } #endif PikaImage * load_image (GFile *file, gboolean interactive, PikaPDBStatusType *status, 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; PikaColorProfile *profile = NULL; gint n_images; heif_item_id primary; heif_item_id selected_image; gboolean has_alpha; gint width; gint height; PikaImage *image; PikaLayer *layer; GeglBuffer *buffer; const Babl *format; const guint8 *data; gint stride; gint bit_depth = 8; enum heif_chroma chroma = heif_chroma_interleaved_RGB; PikaPrecision precision; gboolean load_linear; const char *encoding; pika_progress_init_printf (_("Opening '%s'"), pika_file_get_utf8_name (file)); *status = PIKA_PDB_EXECUTION_ERROR; file_size = get_file_size (file, error); if (file_size <= 0) return NULL; input = G_INPUT_STREAM (g_file_read (file, NULL, error)); if (! input) return NULL; 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 NULL; } pika_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 NULL; } 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 NULL; } g_free (file_buffer); g_object_unref (input); pika_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 NULL; } 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 NULL; } /* 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); *status = PIKA_PDB_CANCEL; return NULL; } } /* 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 NULL; } 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 NULL; } #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 NULL; } #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: ICC profile loading failed and discarded.", G_STRFUNC); else profile = pika_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_pika_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) */ pika_progress_update (0.75); width = heif_image_get_width (img, heif_channel_interleaved); height = heif_image_get_height (img, heif_channel_interleaved); /* create PIKA image and copy HEIF image into the PIKA image * (converting it to RGB) */ if (profile) { load_linear = pika_color_profile_is_linear (profile); } else { load_linear = FALSE; } if (load_linear) { if (bit_depth == 8) { precision = PIKA_PRECISION_U8_LINEAR; encoding = has_alpha ? "RGBA u8" : "RGB u8"; } else { precision = PIKA_PRECISION_U16_LINEAR; encoding = has_alpha ? "RGBA u16" : "RGB u16"; } } else /* non-linear profiles */ { if (bit_depth == 8) { precision = PIKA_PRECISION_U8_NON_LINEAR; encoding = has_alpha ? "R'G'B'A u8" : "R'G'B' u8"; } else { precision = PIKA_PRECISION_U16_NON_LINEAR; encoding = has_alpha ? "R'G'B'A u16" : "R'G'B' u16"; } } image = pika_image_new_with_precision (width, height, PIKA_RGB, precision); if (profile) { if (pika_color_profile_is_rgb (profile)) { pika_image_set_color_profile (image, profile); } else if (pika_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 = pika_layer_new (image, _("image content"), width, height, has_alpha ? PIKA_RGBA_IMAGE : PIKA_RGB_IMAGE, 100.0, pika_image_get_default_new_layer_mode (image)); pika_image_insert_layer (image, layer, NULL, 0); buffer = pika_drawable_get_buffer (PIKA_DRAWABLE (layer)); 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) { PikaMetadata *metadata = pika_metadata_new (); PikaMetadataLoadFlags flags = PIKA_METADATA_LOAD_COMMENT | PIKA_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 (!pika_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_try_set_orientation (GEXIV2_METADATA (metadata), GEXIV2_ORIENTATION_NORMAL, NULL); gexiv2_metadata_try_set_metadata_pixel_width (GEXIV2_METADATA (metadata), width, NULL); gexiv2_metadata_try_set_metadata_pixel_height (GEXIV2_METADATA (metadata), height, NULL); pika_image_metadata_load_finish (image, "image/heif", metadata, flags); } } if (profile) g_object_unref (profile); heif_image_handle_release (handle); heif_context_free (ctx); heif_image_release (img); pika_progress_update (1.0); if (image) { *status = PIKA_PDB_SUCCESS; } return image; } 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, PikaImage *image, PikaDrawable *drawable, GObject *config, GError **error, enum heif_compression_format compression, PikaMetadata *metadata) { struct heif_image *h_image = NULL; struct heif_context *context = heif_context_alloc (); struct heif_encoder *encoder = NULL; struct heif_encoding_options *encoder_options = 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; const Babl *space = NULL; guint8 *data; gint stride; gint width; gint height; gboolean has_alpha; gboolean out_linear = FALSE; gboolean lossless; gint quality; gboolean save_profile; gint save_bit_depth = 8; #if LIBHEIF_HAVE_VERSION(1,10,0) HeifpluginExportFormat pixel_format = HEIFPLUGIN_EXPORT_FORMAT_YUV420; #endif #if LIBHEIF_HAVE_VERSION(1,8,0) HeifpluginEncoderSpeed encoder_speed = HEIFPLUGIN_ENCODER_SPEED_BALANCED; const char *parameter_value; struct heif_color_profile_nclx nclx_profile; #endif gboolean save_exif = FALSE; gboolean save_xmp = FALSE; if (!context) { g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "cannot allocate heif_context"); return FALSE; } g_object_get (config, "lossless", &lossless, "quality", &quality, #if LIBHEIF_HAVE_VERSION(1,10,0) "pixel-format", &pixel_format, #endif #if LIBHEIF_HAVE_VERSION(1,8,0) "save-bit-depth", &save_bit_depth, "encoder-speed", &encoder_speed, #endif "save-color-profile", &save_profile, "save-exif", &save_exif, "save-xmp", &save_xmp, NULL); 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; } } pika_progress_init_printf (_("Exporting '%s' using %s encoder"), pika_file_get_utf8_name (file), encoder_name); width = pika_drawable_get_width (drawable); height = pika_drawable_get_height (drawable); has_alpha = pika_drawable_has_alpha (drawable); switch (save_bit_depth) { case 8: err = heif_image_create (width, height, heif_colorspace_RGB, has_alpha ? heif_chroma_interleaved_RGBA : heif_chroma_interleaved_RGB, &h_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, &h_image); #else err = heif_image_create (width, height, heif_colorspace_RGB, has_alpha ? heif_chroma_interleaved_RRGGBBAA_BE : heif_chroma_interleaved_RRGGBB_BE, &h_image); #endif break; #endif default: g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, "Unsupported bit depth: %d", 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 (save_profile) { PikaColorProfile *profile = NULL; const guint8 *icc_data; gsize icc_length; profile = pika_image_get_color_profile (image); if (profile && pika_color_profile_is_linear (profile)) out_linear = TRUE; if (! profile) { profile = pika_image_get_effective_color_profile (image); if (pika_color_profile_is_linear (profile)) { if (pika_image_get_precision (image) != PIKA_PRECISION_U8_LINEAR) { /* If stored data was linear, let's convert the profile. */ PikaColorProfile *saved_profile; saved_profile = pika_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; } } } #if LIBHEIF_HAVE_VERSION(1,10,0) if (pixel_format == HEIFPLUGIN_EXPORT_FORMAT_RGB) { nclx_profile.version = 1; nclx_profile.color_primaries = heif_color_primaries_unspecified; if (out_linear) { nclx_profile.transfer_characteristics = heif_transfer_characteristic_linear; } else { nclx_profile.transfer_characteristics = heif_transfer_characteristic_unspecified; } nclx_profile.matrix_coefficients = heif_matrix_coefficients_RGB_GBR; nclx_profile.full_range_flag = 1; heif_image_set_nclx_color_profile (h_image, &nclx_profile); } #endif icc_data = pika_color_profile_get_icc_profile (profile, &icc_length); heif_image_set_raw_color_profile (h_image, "prof", icc_data, icc_length); space = pika_color_profile_get_space (profile, PIKA_COLOR_RENDERING_INTENT_RELATIVE_COLORIMETRIC, error); if (error && *error) { /* Don't make this a hard failure yet output the error. */ g_printerr ("%s: error getting the profile space: %s", G_STRFUNC, (*error)->message); g_clear_error (error); } g_object_unref (profile); } else { #if LIBHEIF_HAVE_VERSION(1,8,0) /* We save as sRGB */ 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; #if LIBHEIF_HAVE_VERSION(1,10,0) if (pixel_format == HEIFPLUGIN_EXPORT_FORMAT_RGB) { nclx_profile.matrix_coefficients = heif_matrix_coefficients_RGB_GBR; } #endif heif_image_set_nclx_color_profile (h_image, &nclx_profile); space = babl_space ("sRGB"); out_linear = FALSE; #endif } #endif /* LIBHEIF_HAVE_VERSION(1,4,0) */ if (! space) space = pika_drawable_get_format (drawable); if (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_with_space (encoding, space); buffer = pika_drawable_get_buffer (drawable); 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 (h_image, heif_channel_interleaved, width, height, save_bit_depth); data = heif_image_get_plane (h_image, heif_channel_interleaved, &stride); switch (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 (h_image, heif_channel_interleaved, width, height, 8); #else /* old style settings */ heif_image_add_plane (h_image, heif_channel_interleaved, width, height, has_alpha ? 32 : 24); #endif data = heif_image_get_plane (h_image, heif_channel_interleaved, &stride); buffer = pika_drawable_get_buffer (drawable); 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_with_space (encoding, space); gegl_buffer_get (buffer, GEGL_RECTANGLE (0, 0, width, height), 1.0, format, data, stride, GEGL_ABYSS_NONE); g_object_unref (buffer); } pika_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 (h_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 (lossless) { quality = 100; } heif_encoder_set_lossy_quality (encoder, quality); heif_encoder_set_lossless (encoder, lossless); /* heif_encoder_set_logging_level (encoder, logging_level); */ #if LIBHEIF_HAVE_VERSION(1,8,0) #if LIBHEIF_HAVE_VERSION(1,10,0) if (lossless && pixel_format != HEIFPLUGIN_EXPORT_FORMAT_RGB) { /* disable subsampling for lossless */ pixel_format = HEIFPLUGIN_EXPORT_FORMAT_YUV444; } switch (pixel_format) { case HEIFPLUGIN_EXPORT_FORMAT_RGB: /* same as HEIFPLUGIN_EXPORT_FORMAT_YUV444 */ case HEIFPLUGIN_EXPORT_FORMAT_YUV444: parameter_value = "444"; break; case HEIFPLUGIN_EXPORT_FORMAT_YUV422: parameter_value = "422"; break; default: /* HEIFPLUGIN_EXPORT_FORMAT_YUV420 */ parameter_value = "420"; break; } err = heif_encoder_set_parameter_string (encoder, "chroma", parameter_value); if (err.code != 0) { g_printerr ("Failed to set chroma %s for %s encoder: %s", parameter_value, encoder_name, err.message); } #endif if (compression == heif_compression_HEVC) { switch (encoder_speed) { case HEIFPLUGIN_ENCODER_SPEED_SLOW: parameter_value = "veryslow"; break; case HEIFPLUGIN_ENCODER_SPEED_FASTER: parameter_value = "faster"; break; default: /* HEIFPLUGIN_ENCODER_SPEED_BALANCED */ parameter_value = "medium"; break; } err = heif_encoder_set_parameter_string (encoder, "preset", parameter_value); if (err.code != 0) { g_printerr ("Failed to set preset %s for %s encoder: %s", parameter_value, encoder_name, err.message); } } else if (compression == heif_compression_AV1) { int parameter_number; parameter_number = pika_get_num_processors(); parameter_number = CLAMP(parameter_number, 1, 16); err = heif_encoder_set_parameter_integer (encoder, "threads", parameter_number); if (err.code != 0) { g_printerr ("Failed to set threads=%d for %s encoder: %s", parameter_number, encoder_name, err.message); } if (g_strcmp0 (encoder_name, "aom") == 0) /* AOMedia AV1 encoder */ { switch (encoder_speed) { case HEIFPLUGIN_ENCODER_SPEED_SLOW: parameter_number = 1; break; case HEIFPLUGIN_ENCODER_SPEED_FASTER: parameter_number = 6; #if LIBHEIF_HAVE_VERSION(1,10,0) err = heif_encoder_set_parameter_boolean (encoder, "realtime", 1); if (err.code != 0) { g_printerr ("Failed to set realtime=1 for %s encoder: %s", encoder_name, err.message); } #endif break; default: /* HEIFPLUGIN_ENCODER_SPEED_BALANCED */ parameter_number = 5; break; } err = heif_encoder_set_parameter_integer (encoder, "speed", parameter_number); if (err.code != 0) { g_printerr ("Failed to set speed=%d for %s encoder: %s", parameter_number, encoder_name, err.message); } } else if (g_strcmp0 (encoder_name, "rav1e") == 0) /* Rav1e encoder */ { switch (encoder_speed) { case HEIFPLUGIN_ENCODER_SPEED_SLOW: parameter_number = 6; break; case HEIFPLUGIN_ENCODER_SPEED_FASTER: parameter_number = 10; break; default: /* HEIFPLUGIN_ENCODER_SPEED_BALANCED */ parameter_number = 8; break; } err = heif_encoder_set_parameter_integer (encoder, "speed", parameter_number); if (err.code != 0) { g_printerr ("Failed to set speed=%d for %s encoder: %s", parameter_number, encoder_name, err.message); } } else { g_printerr ("Parameters not set, unsupported AV1 encoder: %s", encoder_name); } } #endif #if LIBHEIF_HAVE_VERSION(1,10,0) if (pixel_format == HEIFPLUGIN_EXPORT_FORMAT_RGB) { encoder_options = heif_encoding_options_alloc (); encoder_options->save_two_colr_boxes_when_ICC_and_nclx_available = 1; } #endif err = heif_context_encode_image (context, h_image, encoder, encoder_options, &handle); if (encoder_options) { heif_encoding_options_free (encoder_options); } 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 (h_image); heif_context_free (context); return FALSE; } if (metadata && (save_exif || save_xmp)) { PikaMetadata *filtered_metadata; PikaMetadataSaveFlags metadata_flags = 0; if (save_exif) { metadata_flags |= PIKA_METADATA_SAVE_EXIF; } if (save_xmp) { metadata_flags |= PIKA_METADATA_SAVE_XMP; } filtered_metadata = pika_image_metadata_save_filter (image, "image/heif", metadata, metadata_flags, NULL, error); if(! filtered_metadata) { if (error && *error) { g_printerr ("%s: error filtering metadata: %s", G_STRFUNC, (*error)->message); g_clear_error (error); } } else { GExiv2Metadata *filtered_g2metadata = GEXIV2_METADATA (filtered_metadata); /* EXIF metadata */ if (save_exif && gexiv2_metadata_has_exif (filtered_g2metadata)) { GBytes *raw_exif_data; raw_exif_data = gexiv2_metadata_get_exif_data (filtered_g2metadata, GEXIV2_BYTE_ORDER_LITTLE, error); if (raw_exif_data) { gsize exif_size = 0; gconstpointer exif_buffer = g_bytes_get_data (raw_exif_data, &exif_size); if (exif_size >= 4) { err = heif_context_add_exif_metadata (context, handle, exif_buffer, exif_size); if (err.code != 0) { g_printerr ("Failed to save EXIF metadata: %s", err.message); } } g_bytes_unref (raw_exif_data); } else { if (error && *error) { g_printerr ("%s: error preparing EXIF metadata: %s", G_STRFUNC, (*error)->message); g_clear_error (error); } } } /* XMP metadata */ if (save_xmp && gexiv2_metadata_has_xmp (filtered_g2metadata)) { gchar *xmp_packet; xmp_packet = gexiv2_metadata_try_generate_xmp_packet (filtered_g2metadata, GEXIV2_USE_COMPACT_FORMAT | GEXIV2_OMIT_ALL_FORMATTING, 0, NULL); if (xmp_packet) { int xmp_size = strlen (xmp_packet); if (xmp_size > 0) { heif_context_add_XMP_metadata (context, handle, xmp_packet, xmp_size); } g_free (xmp_packet); } } g_object_unref (filtered_metadata); } } heif_image_handle_release (handle); pika_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 (h_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 (h_image); heif_context_free (context); return FALSE; } g_object_unref (output); heif_encoder_release (encoder); heif_image_release (h_image); heif_context_free (context); pika_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) { pika_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) { pika_message (err.message); continue; } } else { err = heif_context_get_image_handle (heif, IDs[i], &thumbnail_handle); if (err.code) { pika_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) { pika_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) { pika_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 = pika_dialog_new (_("Load HEIF Image"), PLUG_IN_BINARY, NULL, 0, pika_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 = pika_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 = (pika_dialog_run (PIKA_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 */ gboolean save_dialog (PikaProcedure *procedure, GObject *config, PikaImage *image) { GtkWidget *dialog; GtkWidget *quality_scale; #if LIBHEIF_HAVE_VERSION(1, 10, 0) GtkListStore *store_pixelformats; #endif #if LIBHEIF_HAVE_VERSION(1, 8, 0) GtkListStore *store_bitdepths; GtkListStore *store_speeds; #endif gboolean run; dialog = pika_save_procedure_dialog_new (PIKA_SAVE_PROCEDURE (procedure), PIKA_PROCEDURE_CONFIG (config), image); pika_procedure_dialog_get_widget (PIKA_PROCEDURE_DIALOG (dialog), "lossless", GTK_TYPE_CHECK_BUTTON); quality_scale = pika_procedure_dialog_get_widget (PIKA_PROCEDURE_DIALOG (dialog), "quality", PIKA_TYPE_SCALE_ENTRY); g_object_bind_property (config, "lossless", quality_scale, "sensitive", G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); #if LIBHEIF_HAVE_VERSION(1, 10, 0) store_pixelformats = pika_int_store_new (_("RGB"), HEIFPLUGIN_EXPORT_FORMAT_RGB, _("YUV444"), HEIFPLUGIN_EXPORT_FORMAT_YUV444, _("YUV420"), HEIFPLUGIN_EXPORT_FORMAT_YUV420, NULL); pika_procedure_dialog_get_int_combo (PIKA_PROCEDURE_DIALOG (dialog), "pixel-format", PIKA_INT_STORE (store_pixelformats)); #endif #if LIBHEIF_HAVE_VERSION(1, 8, 0) store_bitdepths = pika_int_store_new (_("8 bit/channel"), 8, _("10 bit/channel"), 10, _("12 bit/channel"), 12, NULL); pika_procedure_dialog_get_int_combo (PIKA_PROCEDURE_DIALOG (dialog), "save-bit-depth", PIKA_INT_STORE (store_bitdepths)); store_speeds = pika_int_store_new (_("Slow"), HEIFPLUGIN_ENCODER_SPEED_SLOW, _("Balanced"), HEIFPLUGIN_ENCODER_SPEED_BALANCED, _("Fast"), HEIFPLUGIN_ENCODER_SPEED_FASTER, NULL); pika_procedure_dialog_get_int_combo (PIKA_PROCEDURE_DIALOG (dialog), "encoder-speed", PIKA_INT_STORE (store_speeds)); #endif pika_procedure_dialog_fill (PIKA_PROCEDURE_DIALOG (dialog), "lossless", "quality", #if LIBHEIF_HAVE_VERSION(1, 10, 0) "pixel-format", #endif #if LIBHEIF_HAVE_VERSION(1, 8, 0) "save-bit-depth", "encoder-speed", #endif #if LIBHEIF_HAVE_VERSION(1, 4, 0) "save-color-profile", #endif "save-exif", "save-xmp", NULL); run = pika_procedure_dialog_run (PIKA_PROCEDURE_DIALOG (dialog)); gtk_widget_destroy (dialog); return run; }