diff --git a/plug-ins/common/Makefile.am b/plug-ins/common/Makefile.am index aa7dc474a3..d5035e2766 100644 --- a/plug-ins/common/Makefile.am +++ b/plug-ins/common/Makefile.am @@ -774,10 +774,12 @@ file_heif_LDADD = \ $(libgimpcolor) \ $(libgimpbase) \ $(GTK_LIBS) \ + $(GEXIV2_LIBS) \ $(GEGL_LIBS) \ $(LIBHEIF_LIBS) \ $(RT_LIBS) \ $(INTLLIBS) \ + $(LCMS_LIBS) \ $(file_heif_RC) file_html_table_SOURCES = \ diff --git a/plug-ins/common/file-heif.c b/plug-ins/common/file-heif.c index e17dcba607..45de152287 100644 --- a/plug-ins/common/file-heif.c +++ b/plug-ins/common/file-heif.c @@ -19,6 +19,8 @@ #include "config.h" #include +#include +#include #include #include @@ -28,6 +30,7 @@ #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" @@ -68,20 +71,33 @@ static GimpValueArray * heif_save (GimpProcedure *procedure, const GimpValueArray *args, gpointer run_data); +#if LIBHEIF_HAVE_VERSION(1,8,0) +static GimpValueArray * heif_av1_save (GimpProcedure *procedure, + GimpRunMode run_mode, + GimpImage *image, + gint n_drawables, + GimpDrawable **drawables, + GFile *file, + const GimpValueArray *args, + gpointer run_data); +#endif + static GimpImage * load_image (GFile *file, gboolean interactive, GimpPDBStatusType *status, GError **error); -static gboolean save_image (GFile *file, - GimpImage *image, - GimpDrawable *drawable, - GObject *config, - GError **error); +static gboolean save_image (GFile *file, + GimpImage *image, + GimpDrawable *drawable, + GObject *config, + GError **error, + enum heif_compression_format compression); static gboolean load_dialog (struct heif_context *heif, uint32_t *selected_image); static gboolean save_dialog (GimpProcedure *procedure, - GObject *config); + GObject *config, + GimpImage *image); G_DEFINE_TYPE (Heif, heif, GIMP_TYPE_PLUG_IN) @@ -110,13 +126,15 @@ heif_query_procedures (GimpPlugIn *plug_in) list = g_list_append (list, g_strdup (LOAD_PROC)); list = g_list_append (list, g_strdup (SAVE_PROC)); - +#if LIBHEIF_HAVE_VERSION(1,8,0) + list = g_list_append (list, g_strdup (SAVE_PROC_AV1)); +#endif return list; } static GimpProcedure * heif_create_procedure (GimpPlugIn *plug_in, - const gchar *name) + const gchar *name) { GimpProcedure *procedure = NULL; @@ -143,9 +161,17 @@ heif_create_procedure (GimpPlugIn *plug_in, gimp_file_procedure_set_handles_remote (GIMP_FILE_PROCEDURE (procedure), TRUE); gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure), - "image/heif"); + "image/heif" +#if LIBHEIF_HAVE_VERSION(1,8,0) + ",image/avif" +#endif + ); gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure), - "heif,heic"); + "heif,heic" +#if LIBHEIF_HAVE_VERSION(1,8,0) + ",avif" +#endif + ); /* HEIF is an ISOBMFF format whose "brand" (the value after "ftyp") * can be of various values. @@ -156,7 +182,11 @@ heif_create_procedure (GimpPlugIn *plug_in, "4,string,ftyphevc,4,string,ftypheim," "4,string,ftypheis,4,string,ftyphevm," "4,string,ftyphevs,4,string,ftypmif1," - "4,string,ftypmsf1"); + "4,string,ftypmsf1" +#if LIBHEIF_HAVE_VERSION(1,8,0) + ",4,string,ftypavif" +#endif + ); } else if (! strcmp (name, SAVE_PROC)) { @@ -202,17 +232,74 @@ heif_create_procedure (GimpPlugIn *plug_in, "Save the image's color profile", gimp_export_color_profile (), G_PARAM_READWRITE); - } + GIMP_PROC_ARG_INT (procedure, "save-bit-depth", + "Bit depth", + "Bit depth of exported image", + 8, 12, 8, + G_PARAM_READWRITE); + } +#if LIBHEIF_HAVE_VERSION(1,8,0) + else if (! strcmp (name, SAVE_PROC_AV1)) + { + procedure = gimp_save_procedure_new (plug_in, name, + GIMP_PDB_PROC_TYPE_PLUGIN, + heif_av1_save, NULL, NULL); + + gimp_procedure_set_image_types (procedure, "RGB*"); + + gimp_procedure_set_menu_label (procedure, "HEIF/AVIF"); + + gimp_procedure_set_documentation (procedure, + "Exports AVIF images", + "Save image in AV1 Image File Format (AVIF)", + name); + gimp_procedure_set_attribution (procedure, + "Daniel Novomesky ", + "Daniel Novomesky ", + "2020"); + + gimp_file_procedure_set_handles_remote (GIMP_FILE_PROCEDURE (procedure), + TRUE); + gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure), + "image/avif"); + gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure), + "avif"); + + GIMP_PROC_ARG_INT (procedure, "quality", + "Quality", + "Quality factor (0 = worst, 100 = best)", + 0, 100, 50, + G_PARAM_READWRITE); + + GIMP_PROC_ARG_BOOLEAN (procedure, "lossless", + "Lossless", + "Use lossless compression", + FALSE, + G_PARAM_READWRITE); + + GIMP_PROC_AUX_ARG_BOOLEAN (procedure, "save-color-profile", + "Save color profile", + "Save the image's color profile", + gimp_export_color_profile (), + G_PARAM_READWRITE); + + GIMP_PROC_ARG_INT (procedure, "save-bit-depth", + "Bit depth", + "Bit depth of exported image", + 8, 12, 8, + G_PARAM_READWRITE); + } +#endif return procedure; } static GimpValueArray * heif_load (GimpProcedure *procedure, - GimpRunMode run_mode, - GFile *file, - const GimpValueArray *args, - gpointer run_data) + GimpRunMode run_mode, + GFile *file, + const GimpValueArray *args, + gpointer run_data) { GimpValueArray *return_vals; GimpPDBStatusType status = GIMP_PDB_SUCCESS; @@ -244,13 +331,13 @@ heif_load (GimpProcedure *procedure, static GimpValueArray * heif_save (GimpProcedure *procedure, - GimpRunMode run_mode, - GimpImage *image, - gint n_drawables, - GimpDrawable **drawables, - GFile *file, - const GimpValueArray *args, - gpointer run_data) + GimpRunMode run_mode, + GimpImage *image, + gint n_drawables, + GimpDrawable **drawables, + GFile *file, + const GimpValueArray *args, + gpointer run_data) { GimpProcedureConfig *config; GimpPDBStatusType status = GIMP_PDB_SUCCESS; @@ -296,14 +383,14 @@ heif_save (GimpProcedure *procedure, if (run_mode == GIMP_RUN_INTERACTIVE) { - if (! save_dialog (procedure, G_OBJECT (config))) + if (! save_dialog (procedure, G_OBJECT (config), image)) status = GIMP_PDB_CANCEL; } if (status == GIMP_PDB_SUCCESS) { if (! save_image (file, image, drawables[0], G_OBJECT (config), - &error)) + &error, heif_compression_HEVC)) { status = GIMP_PDB_EXECUTION_ERROR; } @@ -321,6 +408,87 @@ heif_save (GimpProcedure *procedure, return gimp_procedure_new_return_values (procedure, status, error); } +#if LIBHEIF_HAVE_VERSION(1,8,0) +static GimpValueArray * +heif_av1_save (GimpProcedure *procedure, + GimpRunMode run_mode, + GimpImage *image, + gint n_drawables, + GimpDrawable **drawables, + GFile *file, + const GimpValueArray *args, + gpointer run_data) +{ + GimpProcedureConfig *config; + GimpPDBStatusType status = GIMP_PDB_SUCCESS; + GimpExportReturn export = GIMP_EXPORT_CANCEL; + GError *error = NULL; + + INIT_I18N (); + gegl_init (NULL, NULL); + + config = gimp_procedure_create_config (procedure); + gimp_procedure_config_begin_export (config, image, run_mode, + args, "image/avif"); + + switch (run_mode) + { + case GIMP_RUN_INTERACTIVE: + case GIMP_RUN_WITH_LAST_VALS: + gimp_ui_init (PLUG_IN_BINARY); + + export = gimp_export_image (&image, &n_drawables, &drawables, "AVIF", + GIMP_EXPORT_CAN_HANDLE_RGB | + GIMP_EXPORT_CAN_HANDLE_ALPHA); + + if (export == GIMP_EXPORT_CANCEL) + return gimp_procedure_new_return_values (procedure, + GIMP_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 gimp_procedure_new_return_values (procedure, + GIMP_PDB_CALLING_ERROR, + error); + } + + if (run_mode == GIMP_RUN_INTERACTIVE) + { + if (! save_dialog (procedure, G_OBJECT (config), image)) + status = GIMP_PDB_CANCEL; + } + + if (status == GIMP_PDB_SUCCESS) + { + if (! save_image (file, image, drawables[0], G_OBJECT (config), + &error, heif_compression_AV1)) + { + status = GIMP_PDB_EXECUTION_ERROR; + } + } + + gimp_procedure_config_end_export (config, image, file, status); + g_object_unref (config); + + if (export == GIMP_EXPORT_EXPORT) + { + gimp_image_delete (image); + g_free (drawables); + } + + return gimp_procedure_new_return_values (procedure, status, error); +} +#endif + static goffset get_file_size (GFile *file, GError **error) @@ -342,6 +510,180 @@ get_file_size (GFile *file, 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 + GimpImage * load_image (GFile *file, gboolean interactive, @@ -369,10 +711,17 @@ load_image (GFile *file, const Babl *format; const guint8 *data; gint stride; + gint bit_depth; + enum heif_chroma chroma; + GimpPrecision precision; + gboolean load_linear; + const char *encoding; gimp_progress_init_printf (_("Opening '%s'"), g_file_get_parse_name (file)); + *status = GIMP_PDB_EXECUTION_ERROR; + file_size = get_file_size (file, error); if (file_size <= 0) return NULL; @@ -395,6 +744,14 @@ load_image (GFile *file, 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 NULL; + } err = heif_context_read_from_memory (ctx, file_buffer, file_size, NULL); if (err.code) @@ -483,24 +840,68 @@ load_image (GFile *file, has_alpha = heif_image_handle_has_alpha_channel (handle); + 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; + } + + if (bit_depth == 8) + { + if (has_alpha) + { + chroma = heif_chroma_interleaved_RGBA; + } + else + { + chroma = heif_chroma_interleaved_RGB; + } + } + else /* high bit depth */ + { +#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 + } + err = heif_decode_image (handle, &img, heif_colorspace_RGB, - has_alpha ? heif_chroma_interleaved_RGBA : - heif_chroma_interleaved_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); + heif_image_handle_release (handle); + heif_context_free (ctx); - return NULL; + return NULL; } -#ifdef HAVE_LIBHEIF_1_4_0 +#if LIBHEIF_HAVE_VERSION(1,4,0) switch (heif_image_handle_get_color_profile_type (handle)) { case heif_color_profile_type_not_present: @@ -510,34 +911,49 @@ load_image (GFile *file, /* I am unsure, but it looks like both these types represent an * ICC color profile. XXX */ - { - void *profile_data; - size_t profile_size; + { + 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); + 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); + if (err.code) + g_warning ("%s: ICC 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); - } + 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: - /* heif_color_profile_type_nclx (what is that?) and any future - * profile type which we don't support in GIMP (yet). - */ g_warning ("%s: unknown color profile type has been discarded.", G_STRFUNC); break; } -#endif /* HAVE_LIBHEIF_1_4_0 */ +#endif /* LIBHEIF_HAVE_VERSION(1,4,0) */ gimp_progress_update (0.75); @@ -548,11 +964,60 @@ load_image (GFile *file, * (converting it to RGB) */ - image = gimp_image_new (width, height, GIMP_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_NON_LINEAR; + encoding = has_alpha ? "R'G'B'A u8" : "R'G'B' u8"; + } + else + { + precision = GIMP_PRECISION_U16_NON_LINEAR; + encoding = has_alpha ? "R'G'B'A u16" : "R'G'B' u16"; + } + } + + image = gimp_image_new_with_precision (width, height, GIMP_RGB, precision); gimp_image_set_file (image, file); if (profile) - gimp_image_set_color_profile (image, profile); + { + if (gimp_color_profile_is_rgb (profile)) + { + gimp_image_set_color_profile (image, 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 = gimp_layer_new (image, _("image content"), @@ -565,19 +1030,86 @@ load_image (GFile *file, buffer = gimp_drawable_get_buffer (GIMP_DRAWABLE (layer)); - if (has_alpha) - format = babl_format_with_space ("R'G'B'A u8", - gegl_buffer_get_format (buffer)); - else - format = babl_format_with_space ("R'G'B' u8", - gegl_buffer_get_format (buffer)); - data = heif_image_get_plane_readonly (img, heif_channel_interleaved, &stride); - gegl_buffer_set (buffer, - GEGL_RECTANGLE (0, 0, width, height), - 0, format, data, 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); @@ -609,37 +1141,78 @@ load_image (GFile *file, n_metadata = heif_image_handle_get_list_of_metadata_block_IDs (handle, - "XMP", + "mime", &metadata_id, 1); if (n_metadata > 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) + if (g_strcmp0 ( + heif_image_handle_get_metadata_content_type (handle, metadata_id), + "application/rdf+xml") == 0) { - xmp_data = NULL; - xmp_data_size = 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_ALL; + GimpMetadataLoadFlags flags = GIMP_METADATA_LOAD_COMMENT | GIMP_METADATA_LOAD_RESOLUTION; if (exif_data) - gimp_metadata_set_from_exif (metadata, - exif_data, exif_data_size, NULL); + { + 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[2] == tiffHeaderBE[2]) + { + break; + } + if (tiffheader[0] == tiffHeaderLE[0] && tiffheader[1] == tiffHeaderLE[1] && + tiffheader[2] == tiffHeaderLE[2] && tiffheader[2] == tiffHeaderLE[2]) + { + 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) - gimp_metadata_set_from_xmp (metadata, - xmp_data, xmp_data_size, NULL); - - if (profile) - flags &= ~GIMP_METADATA_LOAD_COLORSPACE; + { + 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); + } + } gimp_image_metadata_load_finish (image, "image/heif", metadata, flags, @@ -656,6 +1229,10 @@ load_image (GFile *file, gimp_progress_update (1.0); + if (image) + { + *status = GIMP_PDB_SUCCESS; + } return image; } @@ -670,7 +1247,7 @@ write_callback (struct heif_context *ctx, struct heif_error heif_error; heif_error.code = heif_error_Ok; - heif_error.subcode = heif_error_Ok; + heif_error.subcode = heif_suberror_Unspecified; heif_error.message = ""; if (! g_output_stream_write_all (output, data, size, NULL, NULL, &error)) @@ -683,11 +1260,12 @@ write_callback (struct heif_context *ctx, } static gboolean -save_image (GFile *file, - GimpImage *image, - GimpDrawable *drawable, - GObject *config, - GError **error) +save_image (GFile *file, + GimpImage *image, + GimpDrawable *drawable, + GObject *config, + GError **error, + enum heif_compression_format compression) { struct heif_image *h_image = NULL; struct heif_context *context = heif_context_alloc (); @@ -709,10 +1287,21 @@ save_image (GFile *file, gboolean lossless; gint quality; gboolean save_profile; + gint save_bit_depth = 8; + + if (!context) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "cannot allocate heif_context"); + return NULL; + } g_object_get (config, "lossless", &lossless, "quality", &quality, +#if LIBHEIF_HAVE_VERSION(1,8,0) + "save-bit-depth", &save_bit_depth, +#endif "save-color-profile", &save_profile, NULL); @@ -724,14 +1313,53 @@ save_image (GFile *file, has_alpha = gimp_drawable_has_alpha (drawable); - err = heif_image_create (width, height, - heif_colorspace_RGB, - has_alpha ? - heif_chroma_interleaved_RGBA : - heif_chroma_interleaved_RGB, - &h_image); + 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; + 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; + 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; + } -#ifdef HAVE_LIBHEIF_1_4_0 + 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) { GimpColorProfile *profile = NULL; @@ -780,48 +1408,171 @@ save_image (GFile *file, g_object_unref (profile); } -#endif /* HAVE_LIBHEIF_1_4_0 */ + else + { +#if LIBHEIF_HAVE_VERSION(1,8,0) + /* We save as sRGB */ + struct heif_color_profile_nclx nclx_profile; + + 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 (h_image, &nclx_profile); + + space = babl_space ("sRGB"); + out_linear = FALSE; +#endif + } +#endif /* LIBHEIF_HAVE_VERSION(1,4,0) */ if (! space) space = gimp_drawable_get_format (drawable); - heif_image_add_plane (h_image, heif_channel_interleaved, - width, height, has_alpha ? 32 : 24); - data = heif_image_get_plane (h_image, heif_channel_interleaved, &stride); - - buffer = gimp_drawable_get_buffer (drawable); - - if (has_alpha) + if (save_bit_depth > 8) { - if (out_linear) - encoding = "RGBA u8"; - else - encoding = "R'G'B'A u8"; + 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 = gimp_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 + else /* save_bit_depth == 8 */ { - if (out_linear) - encoding = "RGB u8"; +#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 = gimp_drawable_get_buffer (drawable); + + if (has_alpha) + { + if (out_linear) + encoding = "RGBA u8"; + else + encoding = "R'G'B'A u8"; + } else - encoding = "R'G'B' u8"; + { + 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); } - 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); gimp_progress_update (0.33); /* encode to HEIF file */ - err = heif_context_get_encoder_for_format (context, - heif_compression_HEVC, + compression, &encoder); + if (err.code != 0) + { + g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED, + "Unable to find suitable HEIF encoder"); + heif_image_release (h_image); + heif_context_free (context); + return FALSE; + } + heif_encoder_set_lossy_quality (encoder, quality); heif_encoder_set_lossless (encoder, lossless); /* heif_encoder_set_logging_level (encoder, logging_level); */ @@ -836,6 +1587,9 @@ save_image (GFile *file, 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; } @@ -850,7 +1604,12 @@ save_image (GFile *file, NULL, FALSE, G_FILE_CREATE_NONE, NULL, error)); if (! output) - return FALSE; + { + heif_encoder_release (encoder); + heif_image_release (h_image); + heif_context_free (context); + return FALSE; + } err = heif_context_write (context, &writer, output); @@ -865,15 +1624,18 @@ save_image (GFile *file, 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_context_free (context); - heif_image_release (h_image); - heif_encoder_release (encoder); + heif_image_release (h_image); + heif_context_free (context); gimp_progress_update (1.0); @@ -1233,13 +1995,20 @@ load_dialog (struct heif_context *heif, gboolean save_dialog (GimpProcedure *procedure, - GObject *config) + GObject *config, + GimpImage *image) { GtkWidget *dialog; GtkWidget *main_vbox; GtkWidget *grid; GtkWidget *button; GtkWidget *frame; +#if LIBHEIF_HAVE_VERSION(1,8,0) + GtkWidget *grid2; + GtkListStore *store; + GtkWidget *combo; + gint save_bit_depth = 8; +#endif gboolean run; dialog = gimp_procedure_dialog_new (procedure, @@ -1276,7 +2045,56 @@ save_dialog (GimpProcedure *procedure, 1, 10, 0, FALSE, 0, 0); -#ifdef HAVE_LIBHEIF_1_4_0 +#if LIBHEIF_HAVE_VERSION(1,8,0) + g_object_get (config, + "save-bit-depth", &save_bit_depth, + NULL); + + switch (gimp_image_get_precision (image)) + { + case GIMP_PRECISION_U8_LINEAR: + case GIMP_PRECISION_U8_NON_LINEAR: + case GIMP_PRECISION_U8_PERCEPTUAL: + /* image is 8bit depth */ + if (save_bit_depth > 8) + { + save_bit_depth = 8; + g_object_set (config, + "save-bit-depth", save_bit_depth, + NULL); + } + break; + default: + /* high bit depth */ + if (save_bit_depth < 10) + { + save_bit_depth = 10; + g_object_set (config, + "save-bit-depth", save_bit_depth, + NULL); + } + break; + } + + grid2 = gtk_grid_new (); + gtk_grid_set_column_spacing (GTK_GRID (grid2), 6); + gtk_box_pack_start (GTK_BOX (main_vbox), grid2, FALSE, FALSE, 0); + gtk_widget_show (grid2); + + store = gimp_int_store_new ("8 bit/channel", 8, + "10 bit/channel (HDR)", 10, + "12 bit/channel (HDR)", 12, + NULL); + + combo = gimp_prop_int_combo_box_new (config, "save-bit-depth", + GIMP_INT_STORE (store)); + g_object_unref (store); + gimp_grid_attach_aligned (GTK_GRID (grid2), 0, 1, + "Bit depth:", 0.0, 0.5, + combo, 2); +#endif + +#if LIBHEIF_HAVE_VERSION(1,4,0) button = gimp_prop_check_button_new (config, "save-color-profile", _("Save color _profile")); gtk_box_pack_start (GTK_BOX (main_vbox), button, FALSE, FALSE, 0); diff --git a/plug-ins/common/meson.build b/plug-ins/common/meson.build index 8b41c30478..1e11436d0f 100644 --- a/plug-ins/common/meson.build +++ b/plug-ins/common/meson.build @@ -93,7 +93,7 @@ endif if libheif.found() common_plugins_list += { 'name': 'file-heif', - 'deps': [ gtk3, gegl, libheif, ], + 'deps': [ gtk3, gegl, libheif, gexiv2, lcms, ], } endif