Commit ba149f17 authored by Michael Natterer's avatar Michael Natterer 😴

plug-ins: add HEIF loading/saving plug-in written by Dirk Farin

Thanks a lot to Dirk for contributing this, added him to AUTHORS.

Import the code from https://github.com/strukturag/heif-gimp-plugin.git
as of today. Merged the files into a single-file plug-in. Changed
the code a lot to match our coding style, but only formatting,
no logic changes.

Still uses deprecated GimpDrawable API and no GIO, but I wanted to do
actual code changes separately from the initial import. Also disabled
metadata support because updating that to GimpMetadata was too much
for the initial import.
parent 07c81abf
......@@ -76,6 +76,7 @@ The following people have contributed code to GIMP:
Ell
Morton Eriksen
Larry Ewing
Dirk Farin
Pedro Alonso Ferrer
Nick Fetchak
Piotr Filiciak
......
......@@ -84,6 +84,7 @@
<contributor role="author" last-active="2.0">Morton Eriksen</contributor>
<contributor role="author" last-active="2.0">Larry Ewing</contributor>
<contributor role="documenter" last-active="2.6">Alessandro Falappa</contributor>
<contributor role="author" last-active="2.10">Dirk Farin</contributor>
<contributor role="author" last-active="2.4">Pedro Alonso Ferrer</contributor>
<contributor role="author" last-active="1.2">Nick Fetchak</contributor>
<contributor role="author" last-active="2.4">Piotr Filiciak</contributor>
......
......@@ -81,6 +81,7 @@ m4_define([intltool_required_version], [0.40.1])
m4_define([perl_required_version], [5.10.0])
m4_define([python2_required_version], [2.5.0])
m4_define([webp_required_version], [0.6.0])
m4_define([libheif_required_version], [1.1.0])
# Current test considers only 2 version numbers. If we update the recommended
# version of gettext with more version numbers, please update the tests.
......@@ -167,6 +168,7 @@ INTLTOOL_REQUIRED_VERSION=intltool_required_version
PERL_REQUIRED_VERSION=perl_required_version
PYTHON2_REQUIRED_VERSION=python2_required_version
WEBP_REQUIRED_VERSION=webp_required_version
LIBHEIF_REQUIRED_VERSION=libheif_required_version
XGETTEXT_RECOMMENDED_VERSION=xgettext_recommended_version
AC_SUBST(GLIB_REQUIRED_VERSION)
AC_SUBST(GDK_PIXBUF_REQUIRED_VERSION)
......@@ -197,6 +199,7 @@ AC_SUBST(INTLTOOL_REQUIRED_VERSION)
AC_SUBST(PERL_REQUIRED_VERSION)
AC_SUBST(PYTHON2_REQUIRED_VERSION)
AC_SUBST(WEBP_REQUIRED_VERSION)
AC_SUBST(LIBHEIF_REQUIRED_VERSION)
AC_SUBST(XGETTEXT_RECOMMENDED_VERSION)
# The symbol GIMP_UNSTABLE is defined above for substitution in
......@@ -1664,6 +1667,28 @@ fi
AM_CONDITIONAL(HAVE_WEBP, test "x$have_webp" = xyes)
###################
# Check for libheif
###################
AC_ARG_WITH(libheif, [ --without-libheif build without libheif support])
have_libheif=no
if test "x$with_libheif" != xno; then
have_libheif=yes
PKG_CHECK_MODULES(LIBHEIF, libheif >= libheif_required_version,
FILE_HEIF='file-heif$(EXEEXT)',
[have_libheif="no (libheif not found)"])
fi
if test "x$have_libheif" = xyes; then
MIME_TYPES="$MIME_TYPES;image/heif;image/heic"
fi
AC_SUBST(FILE_HEIF)
AM_CONDITIONAL(HAVE_LIBHEIF, test "x$have_libheif" = xyes)
######################
# Check for libmypaint
######################
......@@ -2762,6 +2787,7 @@ Optional Plug-Ins:
MNG: $have_libmng
OpenEXR: $have_openexr
WebP: $have_webp
Heif: $have_libheif
PDF (export): $have_cairo_pdf
Print: $enable_print
Python 2: $enable_python
......
......@@ -78,6 +78,8 @@
/file-glob.exe
/file-header
/file-header.exe
/file-heif
/file-heif.exe
/file-html-table
/file-html-table.exe
/file-jp2-load
......
......@@ -87,6 +87,7 @@ libexec_PROGRAMS = \
file-gih \
file-glob \
file-header \
$(FILE_HEIF) \
file-html-table \
$(FILE_JP2_LOAD) \
$(FILE_MNG) \
......@@ -144,6 +145,7 @@ libexec_PROGRAMS = \
EXTRA_PROGRAMS = \
file-aa \
file-heif \
file-jp2-load \
file-mng \
file-pdf-save \
......@@ -821,6 +823,27 @@ file_header_LDADD = \
$(INTLLIBS) \
$(file_header_RC)
file_heif_CFLAGS = $(LIBHEIF_CFLAGS)
file_heif_SOURCES = \
file-heif.c
file_heif_LDADD = \
$(libgimpui) \
$(libgimpwidgets) \
$(libgimpmodule) \
$(libgimp) \
$(libgimpmath) \
$(libgimpconfig) \
$(libgimpcolor) \
$(libgimpbase) \
$(GTK_LIBS) \
$(GEGL_LIBS) \
$(LIBHEIF_LIBS) \
$(RT_LIBS) \
$(INTLLIBS) \
$(file_heif_RC)
file_html_table_SOURCES = \
file-html-table.c
......
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <libheif/heif.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 PLUG_IN_BINARY "file-heif"
typedef struct _SaveParams SaveParams;
struct _SaveParams
{
gint quality;
gboolean lossless;
};
/* local function prototypes */
static void query (void);
static void run (const gchar *name,
gint nparams,
const GimpParam *param,
gint *nreturn_vals,
GimpParam **return_vals);
static gint32 load_image (const gchar *filename,
gboolean interactive,
GError **error);
static gboolean save_image (const gchar *filename,
gint32 image_ID,
gint32 drawable_ID,
const SaveParams *params,
GError **error);
static gboolean load_dialog (struct heif_context *heif,
uint32_t *selected_image);
static gboolean save_dialog (SaveParams *params);
GimpPlugInInfo PLUG_IN_INFO =
{
NULL, /* init_proc */
NULL, /* quit_proc */
query, /* query_proc */
run, /* run_proc */
};
MAIN ()
static void
query (void)
{
static const GimpParamDef load_args[] =
{
{ GIMP_PDB_INT32, "run-mode", "The run mode { RUN-NONINTERACTIVE (1) }" },
{ GIMP_PDB_STRING, "filename", "The name of the file to load" },
{ GIMP_PDB_STRING, "raw-filename", "The name entered" }
};
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-NONINTERACTIVE (1) }" },
{ GIMP_PDB_IMAGE, "image", "Input image" },
{ GIMP_PDB_DRAWABLE, "drawable", "Drawable to export" },
{ GIMP_PDB_STRING, "filename", "The name of the file to export the image in" },
{ GIMP_PDB_STRING, "raw-filename", "The name of the file to export the image in" },
{ GIMP_PDB_INT32, "quality", "Quality factor (range: 0-100. 0 = worst, 100 = best)" },
{ GIMP_PDB_INT32, "lossless", "Use lossless compression (0 = lossy, 1 = lossless)" }
};
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, "heic,heif", "");
gimp_register_file_handler_mime (LOAD_PROC, "image/heif");
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");
}
#define LOAD_HEIF_ERROR -1
#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 ();
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)
{
const gchar *filename;
gboolean interactive;
if (n_params != 3)
status = GIMP_PDB_CALLING_ERROR;
filename = param[1].data.d_string;
interactive = (run_mode == GIMP_RUN_INTERACTIVE);
if (status == GIMP_PDB_SUCCESS)
{
gint32 image_ID;
image_ID = load_image (filename, interactive, &error);
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 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;
params.lossless = FALSE;
params.quality = 50;
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;
return;
}
break;
default:
break;
}
switch (run_mode)
{
case GIMP_RUN_INTERACTIVE:
gimp_get_data (SAVE_PROC, &params);
if (! save_dialog (&params))
status = GIMP_PDB_CANCEL;
break;
case GIMP_RUN_WITH_LAST_VALS:
gimp_get_data (SAVE_PROC, &params);
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)
{
if (save_image (param[3].data.d_string, image_ID, drawable_ID,
&params,
&error))
{
gimp_set_data (SAVE_PROC, &params, sizeof (params));
}
else
{
status = GIMP_PDB_EXECUTION_ERROR;
}
}
}
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;
}
gint32
load_image (const gchar *filename,
gboolean interactive,
GError **error)
{
struct heif_context *ctx;
struct heif_error err;
struct heif_image_handle *handle = NULL;
struct heif_image *img = 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;
GimpDrawable *drawable;
GimpPixelRgn rgn_out;
const guint8 *data;
gint stride;
gint bpp;
ctx = heif_context_alloc ();
err = heif_context_read_from_file (ctx, filename, 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);
return -1;
}
/* 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);
err = heif_decode_image (handle,
&img,
heif_colorspace_RGB,
has_alpha ? heif_chroma_interleaved_32bit :
heif_chroma_interleaved_24bit,
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;
}
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)
*/
image_ID = gimp_image_new (width, height, GIMP_RGB);
gimp_image_set_filename (image_ID, filename);
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);
drawable = gimp_drawable_get (layer_ID);
gimp_pixel_rgn_init (&rgn_out,
drawable,
0,0,
width, height,
TRUE, TRUE);
data = heif_image_get_plane_readonly (img, heif_channel_interleaved,
&stride);
bpp = heif_image_get_bits_per_pixel (img, heif_channel_interleaved) / 8;
if (stride == width * bpp)
{
/* we can transfer the whole image at once */
gimp_pixel_rgn_set_rect (&rgn_out,
data,
0, 0, width, height);
}
else
{
gint y;
for (y = 0; y < height; y++)
{
/* stride has some padding, we have to send the image line by line */
gimp_pixel_rgn_set_row (&rgn_out,
data + y * stride,
0, y, width);
}
}
if (FALSE)
{
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)
{
size_t data_size;
uint8_t *data;
const gint heif_exif_skip = 4;
data_size = heif_image_handle_get_metadata_size (handle,
metadata_id);
data = g_alloca (data_size);
err = heif_image_handle_get_metadata (handle, metadata_id, data);
gimp_image_attach_new_parasite (image_ID,
"exif-data",
0,
data_size - heif_exif_skip,
data + heif_exif_skip);
}
}
gimp_drawable_flush (drawable);
gimp_drawable_merge_shadow (drawable->drawable_id, TRUE);
gimp_drawable_update (drawable->drawable_id,
0, 0, width, height);
gimp_drawable_detach (drawable);
heif_image_handle_release (handle);
heif_context_free (ctx);
heif_image_release (img);
return image_ID;
}
static gboolean
save_image (const gchar *filename,
gint32 image_ID,
gint32 drawable_ID,
const SaveParams *params,
GError **error)
{
struct heif_image *image = NULL;
struct heif_context *context = heif_context_alloc ();
struct heif_encoder *encoder;
struct heif_image_handle *handle;
struct heif_error err;
guint8 *data;
gint stride;
GimpPixelRgn rgn_in;
GimpDrawable *drawable;
gint width;
gint height;
gboolean has_alpha;
gint y;
width = gimp_drawable_width (drawable_ID);
height = gimp_drawable_height (drawable_ID);
has_alpha = gimp_drawable_has_alpha (drawable_ID);
err = heif_image_create (width, height,
heif_colorspace_RGB,
has_alpha ?
heif_chroma_interleaved_32bit :
heif_chroma_interleaved_24bit,
&image);
heif_image_add_plane (image, heif_channel_interleaved,
width, height, has_alpha ? 32 : 24);
data = heif_image_get_plane (image, heif_channel_interleaved, &stride);
drawable = gimp_drawable_get (drawable_ID);
gimp_pixel_rgn_init (&rgn_in, drawable,
0, 0, width, height, FALSE, FALSE);
for (y = 0; y < height; y++)
{
gimp_pixel_rgn_get_row (&rgn_in,
data + y * stride, 0, y, width);
}
gimp_drawable_detach (drawable);
/* encode to HEIF file */
context = heif_context_alloc ();
err = heif_context_get_encoder_for_format (context,
heif_compression_HEVC,
&encoder);
heif_encoder_set_lossy_quality (encoder, params->quality);
heif_encoder_set_lossless (encoder, params->lossless);
/* heif_encoder_set_logging_level (encoder, logging_level); */
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);
return FALSE;
}
heif_image_handle_release (handle);
err = heif_context_write_to_file (context, filename);
if (err.code != 0)
{
g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
_("Writing HEIF image failed: %s"),
err.message);
return FALSE;
}
heif_context_free (context);
heif_image_release (image);
heif_encoder_release (encoder);
return TRUE;
}
/* the dialogs */
#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);