913 lines
27 KiB
C
913 lines
27 KiB
C
/* PIKA - Photo and Image Kooker Application
|
|
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
|
|
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
|
|
*
|
|
* Original copyright, applying to most contents (license remains unchanged):
|
|
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
|
|
*
|
|
* file-webp - WebP file format plug-in for the PIKA
|
|
* 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 <libpika/pika.h>
|
|
#include <libpika/pikaui.h>
|
|
|
|
#include <webp/encode.h>
|
|
#include <webp/mux.h>
|
|
|
|
#include "file-webp-save.h"
|
|
|
|
#include "libpika/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);
|
|
|
|
static void webp_decide_output (PikaImage *image,
|
|
GObject *config,
|
|
PikaColorProfile **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 pika_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 (GFile *file,
|
|
PikaImage *image,
|
|
PikaDrawable *drawable,
|
|
GObject *config,
|
|
GError **error)
|
|
{
|
|
gboolean status = FALSE;
|
|
FILE *outfile = NULL;
|
|
WebPConfig webp_config = { 0, };
|
|
WebPPicture picture = { 0, };
|
|
guchar *buffer = NULL;
|
|
gint w, h;
|
|
gboolean has_alpha;
|
|
const gchar *encoding;
|
|
const Babl *format;
|
|
const Babl *space = NULL;
|
|
gint bpp;
|
|
PikaColorProfile *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;
|
|
WebPPreset preset;
|
|
gboolean lossless;
|
|
gdouble quality;
|
|
gdouble alpha_quality;
|
|
gboolean use_sharp_yuv;
|
|
|
|
g_object_get (config,
|
|
"preset", &preset,
|
|
"lossless", &lossless,
|
|
"quality", &quality,
|
|
"alpha-quality", &alpha_quality,
|
|
"use-sharp-yuv", &use_sharp_yuv,
|
|
NULL);
|
|
|
|
webp_decide_output (image, config, &profile, &out_linear);
|
|
if (profile)
|
|
{
|
|
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 still output the error.
|
|
*/
|
|
g_printerr ("%s: error getting the profile space: %s",
|
|
G_STRFUNC, (*error)->message);
|
|
g_clear_error (error);
|
|
}
|
|
|
|
}
|
|
if (! space)
|
|
space = pika_drawable_get_format (drawable);
|
|
|
|
/* 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 */
|
|
pika_progress_init_printf (_("Saving '%s'"),
|
|
pika_file_get_utf8_name (file));
|
|
|
|
/* Attempt to open the output file */
|
|
outfile = g_fopen (g_file_peek_path (file), "w+b");
|
|
|
|
if (! outfile)
|
|
{
|
|
g_set_error (error, G_FILE_ERROR,
|
|
g_file_error_from_errno (errno),
|
|
_("Unable to open '%s' for writing: %s"),
|
|
pika_file_get_utf8_name (file),
|
|
g_strerror (errno));
|
|
break;
|
|
}
|
|
|
|
/* Obtain the drawable type */
|
|
has_alpha = pika_drawable_has_alpha (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);
|
|
bpp = babl_format_get_bytes_per_pixel (format);
|
|
|
|
/* Retrieve the buffer for the layer */
|
|
geglbuffer = pika_drawable_get_buffer (drawable);
|
|
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 (&webp_config, preset, quality);
|
|
|
|
webp_config.lossless = lossless;
|
|
webp_config.method = 6; /* better quality */
|
|
webp_config.alpha_quality = alpha_quality;
|
|
webp_config.use_sharp_yuv = use_sharp_yuv ? 1 : 0;
|
|
|
|
/* 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 (&webp_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 = pika_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 (PikaLayer *layer)
|
|
{
|
|
gchar *layer_name;
|
|
gint delay_ms;
|
|
|
|
layer_name = pika_item_get_name (PIKA_ITEM (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 gboolean
|
|
get_layer_needs_combine (PikaLayer *layer)
|
|
{
|
|
gchar *layer_name;
|
|
gboolean needs_combine;
|
|
|
|
layer_name = pika_item_get_name (PIKA_ITEM (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 (source, "output", over, "aux");
|
|
gegl_node_process (target);
|
|
g_object_unref (graph);
|
|
|
|
return buffer;
|
|
}
|
|
|
|
gboolean
|
|
save_animation (GFile *file,
|
|
PikaImage *image,
|
|
gint n_drawables,
|
|
PikaDrawable **drawables,
|
|
GObject *config,
|
|
GError **error)
|
|
{
|
|
GList *layers;
|
|
gint32 n_layers;
|
|
gboolean status = TRUE;
|
|
FILE *outfile = NULL;
|
|
guchar *buffer = NULL;
|
|
gint buffer_size = 0;
|
|
gint w, h;
|
|
gint bpp;
|
|
gboolean has_alpha;
|
|
const gchar *encoding;
|
|
const Babl *format;
|
|
const Babl *space = NULL;
|
|
PikaColorProfile *profile = NULL;
|
|
WebPAnimEncoderOptions enc_options;
|
|
WebPData webp_data;
|
|
int frame_timestamp = 0;
|
|
WebPAnimEncoder *enc = NULL;
|
|
GeglBuffer *prev_frame = NULL;
|
|
gboolean out_linear = FALSE;
|
|
WebPPreset preset;
|
|
gboolean lossless;
|
|
gboolean animation;
|
|
gboolean loop;
|
|
gboolean minimize_size;
|
|
gint keyframe_distance;
|
|
gdouble quality;
|
|
gdouble alpha_quality;
|
|
gint default_delay;
|
|
gboolean force_delay;
|
|
gboolean use_sharp_yuv;
|
|
|
|
g_return_val_if_fail (n_drawables > 0, FALSE);
|
|
|
|
g_object_get (config,
|
|
"preset", &preset,
|
|
"lossless", &lossless,
|
|
"animation", &animation,
|
|
"animation-loop", &loop,
|
|
"minimize-size", &minimize_size,
|
|
"keyframe-distance", &keyframe_distance,
|
|
"quality", &quality,
|
|
"alpha-quality", &alpha_quality,
|
|
"default-delay", &default_delay,
|
|
"force-delay", &force_delay,
|
|
"use-sharp-yuv", &use_sharp_yuv,
|
|
NULL);
|
|
|
|
layers = pika_image_list_layers (image);
|
|
|
|
if (! layers)
|
|
return FALSE;
|
|
|
|
layers = g_list_reverse (layers);
|
|
n_layers = g_list_length (layers);
|
|
|
|
webp_decide_output (image, config, &profile, &out_linear);
|
|
if (profile)
|
|
{
|
|
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 still output the error.
|
|
*/
|
|
g_printerr ("%s: error getting the profile space: %s",
|
|
G_STRFUNC, (*error)->message);
|
|
g_clear_error (error);
|
|
}
|
|
|
|
}
|
|
|
|
if (! space)
|
|
space = pika_drawable_get_format (drawables[0]);
|
|
|
|
pika_image_undo_freeze (image);
|
|
|
|
WebPDataInit (&webp_data);
|
|
|
|
do
|
|
{
|
|
GList *list;
|
|
gint i;
|
|
|
|
/* Begin displaying export progress */
|
|
pika_progress_init_printf (_("Saving '%s'"),
|
|
pika_file_get_utf8_name (file));
|
|
|
|
/* Attempt to open the output file */
|
|
outfile = g_fopen (g_file_peek_path (file), "wb");
|
|
|
|
if (! outfile)
|
|
{
|
|
g_set_error (error, G_FILE_ERROR,
|
|
g_file_error_from_errno (errno),
|
|
_("Unable to open '%s' for writing: %s"),
|
|
pika_file_get_utf8_name (file),
|
|
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 (! loop)
|
|
enc_options.anim_params.loop_count = 1;
|
|
|
|
enc_options.allow_mixed = lossless ? 0 : 1;
|
|
enc_options.minimize_size = minimize_size ? 1 : 0;
|
|
if (! minimize_size)
|
|
{
|
|
enc_options.kmax = keyframe_distance;
|
|
/* explicitly force minimum key-frame distance too, for good measure */
|
|
enc_options.kmin = keyframe_distance - 1;
|
|
}
|
|
|
|
for (list = layers, i = 0;
|
|
list;
|
|
list = g_list_next (list), i++)
|
|
{
|
|
GeglBuffer *geglbuffer;
|
|
GeglBuffer *current_frame;
|
|
GeglRectangle extent;
|
|
WebPConfig webp_config;
|
|
WebPPicture picture;
|
|
WebPMemoryWriter mw = { 0 };
|
|
PikaDrawable *drawable = list->data;
|
|
gint delay;
|
|
gboolean needs_combine;
|
|
|
|
delay = get_layer_delay (PIKA_LAYER (drawable));
|
|
needs_combine = get_layer_needs_combine (PIKA_LAYER (drawable));
|
|
|
|
/* Obtain the drawable type */
|
|
has_alpha = pika_drawable_has_alpha (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);
|
|
bpp = babl_format_get_bytes_per_pixel (format);
|
|
|
|
/* fix layers to avoid offset errors */
|
|
pika_layer_resize_to_image_size (PIKA_LAYER (drawable));
|
|
|
|
/* Retrieve the buffer for the layer */
|
|
geglbuffer = pika_drawable_get_buffer (drawable);
|
|
extent = *gegl_buffer_get_extent (geglbuffer);
|
|
w = extent.width;
|
|
h = extent.height;
|
|
|
|
if (i == 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 (&webp_config, preset, quality);
|
|
|
|
webp_config.lossless = lossless;
|
|
webp_config.method = 6; /* better quality */
|
|
webp_config.alpha_quality = alpha_quality;
|
|
webp_config.exact = 1;
|
|
webp_config.use_sharp_yuv = use_sharp_yuv ? 1 : 0;
|
|
|
|
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 (i == 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,
|
|
&webp_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;
|
|
|
|
pika_progress_update ((i + 1.0) / n_layers);
|
|
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 = pika_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);
|
|
|
|
g_list_free (layers);
|
|
|
|
return status;
|
|
}
|
|
|
|
static void
|
|
webp_decide_output (PikaImage *image,
|
|
GObject *config,
|
|
PikaColorProfile **profile,
|
|
gboolean *out_linear)
|
|
{
|
|
gboolean save_profile;
|
|
|
|
g_return_if_fail (profile && *profile == NULL);
|
|
|
|
g_object_get (config,
|
|
"save-color-profile", &save_profile,
|
|
NULL);
|
|
|
|
*out_linear = FALSE;
|
|
|
|
if (save_profile)
|
|
{
|
|
*profile = pika_image_get_color_profile (image);
|
|
|
|
/* If a profile is explicitly set, follow its TRC, whatever the
|
|
* storage format.
|
|
*/
|
|
if (*profile && pika_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 = 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|