diff options
Diffstat (limited to 'plug-ins/file-webp/file-webp-save.c')
-rw-r--r-- | plug-ins/file-webp/file-webp-save.c | 903 |
1 files changed, 903 insertions, 0 deletions
diff --git a/plug-ins/file-webp/file-webp-save.c b/plug-ins/file-webp/file-webp-save.c new file mode 100644 index 0000000..51dffd9 --- /dev/null +++ b/plug-ins/file-webp/file-webp-save.c @@ -0,0 +1,903 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * file-webp - WebP file format plug-in for the GIMP + * Copyright (C) 2015 Nathan Osman + * Copyright (C) 2016 Ben Touchette + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <errno.h> +#include <glib/gstdio.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> + +#include <gegl.h> + +#include <libgimp/gimp.h> +#include <libgimp/gimpui.h> + +#include <webp/encode.h> +#include <webp/mux.h> + +#include "file-webp-save.h" + +#include "libgimp/stdplugins-intl.h" + + +int webp_anim_file_writer (FILE *outfile, + const uint8_t *data, + size_t data_size); +int webp_file_writer (const uint8_t *data, + size_t data_size, + const WebPPicture *picture); +int webp_file_progress (int percent, + const WebPPicture *picture); +gchar * webp_error_string (WebPEncodingError error_code); + +gboolean save_layer (const gchar *filename, + gint32 nLayers, + gint32 image_ID, + gint32 drawable_ID, + WebPSaveParams *params, + GError **error); + +gboolean save_animation (const gchar *filename, + gint32 nLayers, + gint32 *allLayers, + gint32 image_ID, + gint32 drawable_ID, + WebPSaveParams *params, + GError **error); + +static void webp_decide_output (gint32 image_ID, + WebPSaveParams *params, + GimpColorProfile **profile, + gboolean *out_linear); + +int +webp_anim_file_writer (FILE *outfile, + const uint8_t *data, + size_t data_size) +{ + int ok = 0; + + if (data == NULL) + return 0; + + ok = (fwrite (data, data_size, 1, outfile) == 1); + + return ok; +} + +int +webp_file_writer (const uint8_t *data, + size_t data_size, + const WebPPicture *picture) +{ + FILE *outfile; + + /* Obtain the FILE* and write the data to the file */ + outfile = (FILE *) picture->custom_ptr; + + return fwrite (data, sizeof (uint8_t), data_size, outfile) == data_size; +} + +int +webp_file_progress (int percent, + const WebPPicture *picture) +{ + return gimp_progress_update (percent / 100.0); +} + +gchar * +webp_error_string (WebPEncodingError error_code) +{ + switch (error_code) + { + case VP8_ENC_ERROR_OUT_OF_MEMORY: + return g_strdup (_("out of memory")); + case VP8_ENC_ERROR_BITSTREAM_OUT_OF_MEMORY: + return g_strdup (_("not enough memory to flush bits")); + case VP8_ENC_ERROR_NULL_PARAMETER: + return g_strdup (_("NULL parameter")); + case VP8_ENC_ERROR_INVALID_CONFIGURATION: + return g_strdup (_("invalid configuration")); + case VP8_ENC_ERROR_BAD_DIMENSION: + /* TRANSLATORS: widthxheight with UTF-8 encoded multiply sign. */ + return g_strdup_printf (_("bad image dimensions (maximum: %d\xc3\x97%d)"), + WEBP_MAX_DIMENSION, WEBP_MAX_DIMENSION); + case VP8_ENC_ERROR_PARTITION0_OVERFLOW: + return g_strdup (_("partition is bigger than 512K")); + case VP8_ENC_ERROR_PARTITION_OVERFLOW: + return g_strdup (_("partition is bigger than 16M")); + case VP8_ENC_ERROR_BAD_WRITE: + return g_strdup (_("unable to flush bytes")); + case VP8_ENC_ERROR_FILE_TOO_BIG: + return g_strdup (_("file is larger than 4GiB")); + case VP8_ENC_ERROR_USER_ABORT: + return g_strdup (_("user aborted encoding")); + case VP8_ENC_ERROR_LAST: + return g_strdup (_("list terminator")); + default: + return g_strdup (_("unknown error")); + } +} + +gboolean +save_layer (const gchar *filename, + gint32 nLayers, + gint32 image_ID, + gint32 drawable_ID, + WebPSaveParams *params, + GError **error) +{ + gboolean status = FALSE; + FILE *outfile = NULL; + WebPConfig config = {0}; + WebPPicture picture = {0}; + guchar *buffer = NULL; + gint w, h; + gboolean has_alpha; + const Babl *format; + gint bpp; + GimpColorProfile *profile = NULL; + GeglBuffer *geglbuffer = NULL; + GeglRectangle extent; + gchar *indata; + gsize indatalen; + struct stat stsz; + int fd_outfile; + WebPData chunk; + gboolean out_linear = FALSE; + int res; + + webp_decide_output (image_ID, params, &profile, &out_linear); + + /* The do...while() loop is a neat little trick that makes it easier + * to jump to error handling code while still ensuring proper + * cleanup + */ + + do + { + /* Begin displaying export progress */ + gimp_progress_init_printf (_("Saving '%s'"), + gimp_filename_to_utf8(filename)); + + /* Attempt to open the output file */ + if ((outfile = g_fopen (filename, "w+b")) == NULL) + { + g_set_error (error, G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Unable to open '%s' for writing: %s"), + gimp_filename_to_utf8 (filename), + g_strerror (errno)); + break; + } + + /* Obtain the drawable type */ + has_alpha = gimp_drawable_has_alpha (drawable_ID); + + if (has_alpha) + { + if (out_linear) + format = babl_format ("RGBA u8"); + else + format = babl_format ("R'G'B'A u8"); + } + else + { + if (out_linear) + format = babl_format ("RGB u8"); + else + format = babl_format ("R'G'B' u8"); + } + + bpp = babl_format_get_bytes_per_pixel (format); + + /* Retrieve the buffer for the layer */ + geglbuffer = gimp_drawable_get_buffer (drawable_ID); + extent = *gegl_buffer_get_extent (geglbuffer); + w = extent.width; + h = extent.height; + + /* Initialize the WebP configuration with a preset and fill in the + * remaining values */ + WebPConfigPreset (&config, params->preset, params->quality); + + config.lossless = params->lossless; + config.method = 6; /* better quality */ + config.alpha_quality = params->alpha_quality; + + /* Prepare the WebP structure */ + WebPPictureInit (&picture); + picture.use_argb = 1; + picture.width = w; + picture.height = h; + picture.writer = webp_file_writer; + picture.custom_ptr = outfile; + picture.progress_hook = webp_file_progress; + + /* Attempt to allocate a buffer of the appropriate size */ + buffer = g_try_malloc (w * h * bpp); + if (! buffer) + break; + + /* Read the region into the buffer */ + gegl_buffer_get (geglbuffer, &extent, 1.0, format, buffer, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + /* Use the appropriate function to import the data from the buffer */ + if (! has_alpha) + { + status = WebPPictureImportRGB (&picture, buffer, w * bpp); + } + else + { + status = WebPPictureImportRGBA (&picture, buffer, w * bpp); + } + + g_free (buffer); + if (! status) + { + g_printerr ("%s: memory error in WebPPictureImportRGB(A)().", + G_STRFUNC); + break; + } + + /* Perform the actual encode */ + if (! WebPEncode (&config, &picture)) + { + gchar *error_str = webp_error_string (picture.error_code); + + g_printerr ("WebP error: '%s'", error_str); + g_set_error (error, G_FILE_ERROR, + picture.error_code, + _("WebP error: '%s'"), + error_str); + g_free (error_str); + status = FALSE; + break; + } + + /* The cleanup stuff still needs to run but indicate that everything + * completed successfully + */ + status = TRUE; + + } + while (0); + + /* Flush the drawable and detach */ + if (geglbuffer) + { + g_object_unref (geglbuffer); + } + + fflush (outfile); + fd_outfile = fileno (outfile); + fstat (fd_outfile, &stsz); + indatalen = stsz.st_size; + if (indatalen > 0) + { + indata = (gchar*) g_malloc (indatalen); + rewind (outfile); + res = fread (indata, 1, indatalen, outfile); + if (res > 0) + { + WebPMux *mux; + WebPData wp_data; + + wp_data.bytes = (uint8_t*) indata; + wp_data.size = indatalen; + mux = WebPMuxCreate (&wp_data, 1); + + if (mux) + { + /* Save ICC data */ + if (profile) + { + const guint8 *icc_data; + gsize icc_data_size; + + icc_data = gimp_color_profile_get_icc_profile (profile, + &icc_data_size); + chunk.bytes = icc_data; + chunk.size = icc_data_size; + WebPMuxSetChunk(mux, "ICCP", &chunk, 1); + + WebPMuxAssemble (mux, &wp_data); + rewind (outfile); + webp_anim_file_writer (outfile, wp_data.bytes, wp_data.size); + } + + WebPMuxDelete (mux); + } + else + { + g_printerr ("ERROR: Cannot create mux. Can't save features update.\n"); + } + + WebPDataClear (&wp_data); + } + else + { + g_printerr ("ERROR: No data read for features. Can't save features update.\n"); + } + } + else + { + g_printerr ("ERROR: No data for features. Can't save features update.\n"); + } + + /* Free any resources */ + if (outfile) + fclose (outfile); + + WebPPictureFree (&picture); + g_clear_object (&profile); + + return status; +} + +static gint +parse_ms_tag (const gchar *str) +{ + gint sum = 0; + gint offset = 0; + gint length; + + length = strlen (str); + + find_another_bra: + + while ((offset < length) && (str[offset] != '(')) + offset++; + + if (offset >= length) + return -1; + + if (! g_ascii_isdigit (str[++offset])) + goto find_another_bra; + + do + { + sum *= 10; + sum += str[offset] - '0'; + offset++; + } + while ((offset < length) && (g_ascii_isdigit (str[offset]))); + + if (length - offset <= 2) + return -3; + + if ((g_ascii_toupper (str[offset]) != 'M') || + (g_ascii_toupper (str[offset + 1]) != 'S')) + return -4; + + return sum; +} + +static gint +get_layer_delay (gint32 layer) +{ + gchar *layer_name; + gint delay_ms; + + layer_name = gimp_item_get_name (layer); + delay_ms = parse_ms_tag (layer_name); + g_free (layer_name); + + return delay_ms; +} + +static gboolean +parse_combine (const char* str) +{ + gint offset = 0; + gint length = strlen (str); + + while ((offset + 9) <= length) + { + if (strncmp (&str[offset], "(combine)", 9) == 0) + return TRUE; + + if (strncmp (&str[offset], "(replace)", 9) == 0) + return FALSE; + + offset++; + } + + return FALSE; +} + +static gint +get_layer_needs_combine (gint32 layer) +{ + gchar *layer_name; + gboolean needs_combine; + + layer_name = gimp_item_get_name (layer); + needs_combine = parse_combine (layer_name); + g_free (layer_name); + + return needs_combine; +} + +static GeglBuffer* +combine_buffers (GeglBuffer *layer_buffer, + GeglBuffer *prev_frame_buffer) +{ + GeglBuffer *buffer; + GeglNode *graph; + GeglNode *source; + GeglNode *backdrop; + GeglNode *over; + GeglNode *target; + + graph = gegl_node_new (); + buffer = gegl_buffer_new (gegl_buffer_get_extent (prev_frame_buffer), + gegl_buffer_get_format (prev_frame_buffer)); + + source = gegl_node_new_child (graph, + "operation", "gegl:buffer-source", + "buffer", layer_buffer, + NULL); + backdrop = gegl_node_new_child (graph, + "operation", "gegl:buffer-source", + "buffer", prev_frame_buffer, + NULL); + + over = gegl_node_new_child (graph, + "operation", "gegl:over", + NULL); + target = gegl_node_new_child (graph, + "operation", "gegl:write-buffer", + "buffer", buffer, + NULL); + gegl_node_link_many (backdrop, over, target, NULL); + gegl_node_connect_to (source, "output", + over, "aux"); + gegl_node_process (target); + g_object_unref (graph); + + return buffer; +} + +gboolean +save_animation (const gchar *filename, + gint32 nLayers, + gint32 *allLayers, + gint32 image_ID, + gint32 drawable_ID, + WebPSaveParams *params, + GError **error) +{ + gboolean status = TRUE; + FILE *outfile = NULL; + guchar *buffer = NULL; + gint buffer_size = 0; + gint w, h; + gint bpp; + gboolean has_alpha; + const Babl *format; + GimpColorProfile *profile = NULL; + WebPAnimEncoderOptions enc_options; + WebPData webp_data; + int frame_timestamp = 0; + WebPAnimEncoder *enc = NULL; + GeglBuffer *prev_frame = NULL; + gboolean out_linear = FALSE; + + if (nLayers < 1) + return FALSE; + + webp_decide_output (image_ID, params, &profile, &out_linear); + + gimp_image_undo_freeze (image_ID); + + WebPDataInit (&webp_data); + + do + { + gint loop; + gint default_delay = params->delay; + gboolean force_delay = params->force_delay; + + /* Begin displaying export progress */ + gimp_progress_init_printf (_("Saving '%s'"), + gimp_filename_to_utf8 (filename)); + + /* Attempt to open the output file */ + if ((outfile = g_fopen (filename, "wb")) == NULL) + { + g_set_error (error, G_FILE_ERROR, + g_file_error_from_errno (errno), + _("Unable to open '%s' for writing: %s"), + gimp_filename_to_utf8 (filename), + g_strerror (errno)); + status = FALSE; + break; + } + + if (! WebPAnimEncoderOptionsInit (&enc_options)) + { + g_printerr ("ERROR: version mismatch\n"); + status = FALSE; + break; + } + + enc_options.anim_params.loop_count = 0; + if (! params->loop) + enc_options.anim_params.loop_count = 1; + + enc_options.allow_mixed = params->lossless ? 0 : 1; + enc_options.minimize_size = params->minimize_size ? 1 : 0; + if (! params->minimize_size) + { + enc_options.kmax = params->kf_distance; + /* explicitly force minimum key-frame distance too, for good measure */ + enc_options.kmin = params->kf_distance - 1; + } + + for (loop = 0; loop < nLayers; loop++) + { + GeglBuffer *geglbuffer; + GeglBuffer *current_frame; + GeglRectangle extent; + WebPConfig config; + WebPPicture picture; + WebPMemoryWriter mw = { 0 }; + gint32 drawable = allLayers[nLayers - 1 - loop]; + gint delay = get_layer_delay (drawable); + gboolean needs_combine = get_layer_needs_combine (drawable); + + /* Obtain the drawable type */ + has_alpha = gimp_drawable_has_alpha (drawable); + + if (has_alpha) + { + if (out_linear) + format = babl_format ("RGBA u8"); + else + format = babl_format ("R'G'B'A u8"); + } + else + { + if (out_linear) + format = babl_format ("RGB u8"); + else + format = babl_format ("R'G'B' u8"); + } + + bpp = babl_format_get_bytes_per_pixel (format); + + /* fix layers to avoid offset errors */ + gimp_layer_resize_to_image_size (drawable); + + /* Retrieve the buffer for the layer */ + geglbuffer = gimp_drawable_get_buffer (drawable); + extent = *gegl_buffer_get_extent (geglbuffer); + w = extent.width; + h = extent.height; + + if (loop == 0) + { + enc = WebPAnimEncoderNew (w, h, &enc_options); + if (! enc) + { + g_printerr ("ERROR: enc == null\n"); + status = FALSE; + break; + } + } + + /* Attempt to allocate a buffer of the appropriate size */ + if (! buffer || buffer_size < w * h * bpp) + { + buffer = g_try_realloc (buffer, w * h * bpp); + + if (! buffer) + { + g_printerr ("Buffer error: 'buffer null'\n"); + status = FALSE; + break; + } + else + { + buffer_size = w * h * bpp; + } + } + + WebPConfigPreset (&config, params->preset, params->quality); + + config.lossless = params->lossless; + config.method = 6; /* better quality */ + config.alpha_quality = params->alpha_quality; + config.exact = 1; + + WebPMemoryWriterInit (&mw); + + /* Prepare the WebP structure */ + WebPPictureInit (&picture); + picture.use_argb = 1; + picture.argb_stride = w * bpp; + picture.width = w; + picture.height = h; + picture.custom_ptr = &mw; + picture.writer = WebPMemoryWrite; + + if (loop == 0 || ! needs_combine) + { + g_clear_object (&prev_frame); + current_frame = geglbuffer; + } + else + { + current_frame = combine_buffers (geglbuffer, prev_frame); + + /* release resources. */ + g_object_unref (geglbuffer); + g_clear_object (&prev_frame); + } + prev_frame = current_frame; + + /* Read the region into the buffer */ + gegl_buffer_get (current_frame, &extent, 1.0, format, buffer, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + /* Use the appropriate function to import the data from the buffer */ + if (! has_alpha) + { + status = WebPPictureImportRGB (&picture, buffer, w * bpp); + } + else + { + status = WebPPictureImportRGBA (&picture, buffer, w * bpp); + } + + if (! status) + { + g_printerr ("%s: memory error in WebPPictureImportRGB(A)().", + G_STRFUNC); + } + /* Perform the actual encode */ + else if (! WebPAnimEncoderAdd (enc, &picture, frame_timestamp, &config)) + { + gchar *error_str = webp_error_string (picture.error_code); + g_printerr ("ERROR[%d]: line %d: %s\n", + picture.error_code, __LINE__, + error_str); + g_free (error_str); + status = FALSE; + } + + WebPMemoryWriterClear (&mw); + WebPPictureFree (&picture); + + if (status == FALSE) + break; + + gimp_progress_update ((loop + 1.0) / nLayers); + frame_timestamp += (delay <= 0 || force_delay) ? default_delay : delay; + } + g_free (buffer); + + if (status == FALSE) + break; + + WebPAnimEncoderAdd (enc, NULL, frame_timestamp, NULL); + + if (! WebPAnimEncoderAssemble (enc, &webp_data)) + { + g_printerr ("ERROR: %s\n", + WebPAnimEncoderGetError (enc)); + status = FALSE; + break; + } + + /* Create a mux object if profile is present */ + if (profile) + { + WebPMux *mux; + WebPData chunk; + const guint8 *icc_data; + gsize icc_data_size; + + mux = WebPMuxCreate (&webp_data, 1); + if (mux == NULL) + { + g_printerr ("ERROR: could not extract muxing object\n"); + status = FALSE; + break; + } + + /* Save ICC data */ + icc_data = gimp_color_profile_get_icc_profile (profile, &icc_data_size); + chunk.bytes = icc_data; + chunk.size = icc_data_size; + WebPMuxSetChunk (mux, "ICCP", &chunk, 1); + + WebPDataClear (&webp_data); + if (WebPMuxAssemble (mux, &webp_data) != WEBP_MUX_OK) + { + g_printerr ("ERROR: could not assemble final bytestream\n"); + status = FALSE; + break; + } + } + + webp_anim_file_writer (outfile, webp_data.bytes, webp_data.size); + } + while (0); + + /* Free any resources */ + WebPDataClear (&webp_data); + WebPAnimEncoderDelete (enc); + g_clear_object (&profile); + + if (prev_frame != NULL) + { + g_object_unref (prev_frame); + } + + if (outfile) + fclose (outfile); + + return status; +} + + +gboolean +save_image (const gchar *filename, + gint32 image_ID, + gint32 drawable_ID, + GimpMetadata *metadata, + GimpMetadataSaveFlags metadata_flags, + WebPSaveParams *params, + GError **error) +{ + GFile *file; + gboolean status = FALSE; + gint32 *layers; + gint nlayers; + + layers = gimp_image_get_layers (image_ID, &nlayers); + + if (nlayers == 0) + { + g_free (layers); + return FALSE; + } + + g_printerr ("Saving WebP file %s\n", filename); + + if (params->animation) + { + status = save_animation (filename, + nlayers, layers, image_ID, drawable_ID, params, + error); + } + else + { + status = save_layer (filename, + nlayers, image_ID, drawable_ID, params, error); + } + + g_free (layers); + + if (metadata) + { + gimp_metadata_set_bits_per_sample (metadata, 8); + + if (params->exif) + metadata_flags |= GIMP_METADATA_SAVE_EXIF; + else + metadata_flags &= ~GIMP_METADATA_SAVE_EXIF; + + /* WebP doesn't support iptc natively and + sets it via xmp */ + if (params->xmp) + { + metadata_flags |= GIMP_METADATA_SAVE_XMP; + if (params->iptc) + metadata_flags |= GIMP_METADATA_SAVE_IPTC; + else + metadata_flags &= ~GIMP_METADATA_SAVE_IPTC; + } + else + { + metadata_flags &= ~GIMP_METADATA_SAVE_XMP; + metadata_flags &= ~GIMP_METADATA_SAVE_IPTC; + } + + if (params->profile) + metadata_flags |= GIMP_METADATA_SAVE_COLOR_PROFILE; + else + metadata_flags &= ~GIMP_METADATA_SAVE_COLOR_PROFILE; + + if (params->thumbnail) + metadata_flags |= GIMP_METADATA_SAVE_THUMBNAIL; + else + metadata_flags &= ~GIMP_METADATA_SAVE_THUMBNAIL; + + file = g_file_new_for_path (filename); + gimp_image_metadata_save_finish (image_ID, + "image/webp", + metadata, metadata_flags, + file, NULL); + g_object_unref (file); + } + + /* Return the status */ + return status; +} + +static void +webp_decide_output (gint32 image_ID, + WebPSaveParams *params, + GimpColorProfile **profile, + gboolean *out_linear) +{ + g_return_if_fail (profile && *profile == NULL); + + *out_linear = FALSE; + if (params->profile) + { + *profile = gimp_image_get_color_profile (image_ID); + + /* If a profile is explicitly set, follow its TRC, whatever the + * storage format. + */ + if (*profile && gimp_color_profile_is_linear (*profile)) + *out_linear = TRUE; + + /* When no profile was explicitly set, since WebP is apparently + * 8-bit max, we export it as sRGB to avoid shadow posterization + * (we don't care about storage TRC). + * We do an exception for 8-bit linear work image to avoid + * conversion loss while the precision is the same. + */ + if (! *profile) + { + /* There is always an effective profile. */ + *profile = gimp_image_get_effective_color_profile (image_ID); + + if (gimp_color_profile_is_linear (*profile)) + { + if (gimp_image_get_precision (image_ID) != GIMP_PRECISION_U8_LINEAR) + { + /* If stored data was linear, let's convert the profile. */ + GimpColorProfile *saved_profile; + + saved_profile = gimp_color_profile_new_srgb_trc_from_color_profile (*profile); + g_clear_object (profile); + *profile = saved_profile; + } + else + { + /* Keep linear profile as-is for 8-bit linear image. */ + *out_linear = TRUE; + } + } + } + } +} |