aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2021-12-12 23:22:20 +0100
committerPřemysl Eric Janouch <p@janouch.name>2021-12-12 23:35:31 +0100
commit6c7d431e35ca100a733ed720f0431cd7822509e5 (patch)
tree09765bebf7e3da8986f6956cec8e6dd0076393fb
parent006e547deba1e952d9de1a52d511edf9e6a8f07c (diff)
downloadfiv-6c7d431e35ca100a733ed720f0431cd7822509e5.tar.gz
fiv-6c7d431e35ca100a733ed720f0431cd7822509e5.tar.xz
fiv-6c7d431e35ca100a733ed720f0431cd7822509e5.zip
Finish WebP support with animations
-rw-r--r--fastiv-io.c177
-rw-r--r--fastiv-view.c2
2 files changed, 143 insertions, 36 deletions
diff --git a/fastiv-io.c b/fastiv-io.c
index 67eeb96..1dc04be 100644
--- a/fastiv-io.c
+++ b/fastiv-io.c
@@ -86,6 +86,9 @@ const char *fastiv_io_supported_media_types[] = {
#ifdef HAVE_XCURSOR
"image/x-xcursor",
#endif // HAVE_XCURSOR
+#ifdef HAVE_LIBWEBP
+ "image/webp",
+#endif // HAVE_LIBWEBP
#ifdef HAVE_LIBHEIF
"image/heic",
"image/heif",
@@ -1117,86 +1120,188 @@ open_xcursor(const gchar *data, gsize len, GError **error)
#ifdef HAVE_LIBWEBP //---------------------------------------------------------
static cairo_surface_t *
-open_libwebp(const gchar *data, gsize len, const gchar *path, GError **error)
+load_libwebp_nonanimated(
+ WebPDecoderConfig *config, const WebPData *wd, GError **error)
{
- // It is wholly zero-initialized by libwebp.
- WebPDecoderConfig config = {};
- if (!WebPInitDecoderConfig(&config)) {
- set_error(error, "libwebp version mismatch");
+ cairo_surface_t *surface = cairo_image_surface_create(
+ config->input.has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24,
+ config->input.width, config->input.height);
+ cairo_status_t surface_status = cairo_surface_status(surface);
+ if (surface_status != CAIRO_STATUS_SUCCESS) {
+ set_error(error, cairo_status_to_string(surface_status));
+ cairo_surface_destroy(surface);
return NULL;
}
- // TODO(p): Differentiate between a bad WebP, and not a WebP.
+ config->options.use_threads = true;
+
+ config->output.width = config->input.width;
+ config->output.height = config->input.height;
+ config->output.is_external_memory = true;
+ config->output.u.RGBA.rgba = cairo_image_surface_get_data(surface);
+ config->output.u.RGBA.stride = cairo_image_surface_get_stride(surface);
+ config->output.u.RGBA.size =
+ config->output.u.RGBA.stride * cairo_image_surface_get_height(surface);
+ if (G_BYTE_ORDER == G_LITTLE_ENDIAN)
+ config->output.colorspace = MODE_bgrA;
+ else
+ config->output.colorspace = MODE_Argb;
+
VP8StatusCode err = 0;
- if ((err = WebPGetFeatures((const uint8_t *) data, len, &config.input))) {
+ if ((err = WebPDecode(wd->bytes, wd->size, config))) {
set_error(error, "WebP decoding error");
+ cairo_surface_destroy(surface);
return NULL;
}
- // TODO(p): Support animations through WebPAnimDecoder (has_animation).
- cairo_surface_t *result = NULL;
+ cairo_surface_mark_dirty(surface);
+ return surface;
+}
+static cairo_surface_t *
+load_libwebp_frame(WebPAnimDecoder *dec, const WebPAnimInfo *info,
+ int *last_timestamp, GError **error)
+{
+ uint8_t *buf = NULL;
+ int timestamp = 0;
+ if (!WebPAnimDecoderGetNext(dec, &buf, &timestamp)) {
+ set_error(error, "WebP decoding error");
+ return NULL;
+ }
+
+ bool is_opaque = (info->bgcolor & 0xFF) == 0xFF;
+ uint64_t area = info->canvas_width * info->canvas_height;
cairo_surface_t *surface = cairo_image_surface_create(
- config.input.has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24,
- config.input.width, config.input.height);
+ is_opaque ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32,
+ info->canvas_width, info->canvas_height);
+
cairo_status_t surface_status = cairo_surface_status(surface);
if (surface_status != CAIRO_STATUS_SUCCESS) {
set_error(error, cairo_status_to_string(surface_status));
- goto fail;
+ cairo_surface_destroy(surface);
+ return NULL;
+ }
+
+ uint32_t *dst = (uint32_t *) cairo_image_surface_get_data(surface);
+ if (G_BYTE_ORDER == G_LITTLE_ENDIAN) {
+ memcpy(dst, buf, area * sizeof *dst);
+ } else {
+ uint32_t *src = (uint32_t *) buf;
+ for (uint64_t i = 0; i < area; i++)
+ *dst++ = GUINT32_FROM_LE(*src++);
}
- config.options.use_threads = true;
+ cairo_surface_mark_dirty(surface);
- config.output.width = config.input.width;
- config.output.height = config.input.height;
- config.output.is_external_memory = true;
- config.output.u.RGBA.rgba = cairo_image_surface_get_data(surface);
- config.output.u.RGBA.stride = cairo_image_surface_get_stride(surface);
- config.output.u.RGBA.size =
- config.output.u.RGBA.stride * cairo_image_surface_get_height(surface);
- if (G_BYTE_ORDER == G_LITTLE_ENDIAN)
- config.output.colorspace = MODE_bgrA;
- else
- config.output.colorspace = MODE_Argb;
+ // This API is confusing and awkward.
+ cairo_surface_set_user_data(surface, &fastiv_io_key_frame_duration,
+ (void *) (intptr_t) (timestamp - *last_timestamp), NULL);
+ *last_timestamp = timestamp;
+ return surface;
+}
- if ((err = WebPDecode((const uint8_t *) data, len, &config))) {
+static cairo_surface_t *
+load_libwebp_animated(const WebPData *wd, GError **error)
+{
+ WebPAnimDecoderOptions options = {};
+ WebPAnimDecoderOptionsInit(&options);
+ options.use_threads = true;
+ options.color_mode = MODE_bgrA;
+
+ WebPAnimInfo info = {};
+ WebPAnimDecoder *dec = WebPAnimDecoderNew(wd, &options);
+ WebPAnimDecoderGetInfo(dec, &info);
+
+ int last_timestamp = 0;
+ cairo_surface_t *frames = NULL, *frames_tail = NULL;
+ while (WebPAnimDecoderHasMoreFrames(dec)) {
+ cairo_surface_t *surface =
+ load_libwebp_frame(dec, &info, &last_timestamp, error);
+ if (!surface) {
+ g_clear_pointer(&frames, cairo_surface_destroy);
+ goto fail;
+ }
+
+ if (frames_tail)
+ cairo_surface_set_user_data(frames_tail,
+ &fastiv_io_key_frame_next, surface,
+ (cairo_destroy_func_t) cairo_surface_destroy);
+ else
+ frames = surface;
+
+ cairo_surface_set_user_data(surface,
+ &fastiv_io_key_frame_previous, frames_tail, NULL);
+ frames_tail = surface;
+ }
+
+ if (frames) {
+ cairo_surface_set_user_data(frames, &fastiv_io_key_frame_previous,
+ frames_tail, NULL);
+ } else {
+ set_error(error, "the animation has no frames");
+ g_clear_pointer(&frames, cairo_surface_destroy);
+ }
+
+fail:
+ WebPAnimDecoderDelete(dec);
+ return frames;
+}
+
+static cairo_surface_t *
+open_libwebp(const gchar *data, gsize len, const gchar *path, GError **error)
+{
+ // It is wholly zero-initialized by libwebp.
+ WebPDecoderConfig config = {};
+ if (!WebPInitDecoderConfig(&config)) {
+ set_error(error, "libwebp version mismatch");
+ return NULL;
+ }
+
+ // TODO(p): Differentiate between a bad WebP, and not a WebP.
+ VP8StatusCode err = 0;
+ WebPData wd = {.bytes = (const uint8_t *) data, .size = len};
+ if ((err = WebPGetFeatures(wd.bytes, wd.size, &config.input))) {
set_error(error, "WebP decoding error");
- goto fail;
+ return NULL;
}
+ cairo_surface_t *result = config.input.has_animation
+ ? load_libwebp_animated(&wd, error)
+ : load_libwebp_nonanimated(&config, &wd, error);
+ if (!result)
+ goto fail;
+
// Of course everything has to use a different abstraction.
- WebPData wd = {.bytes = (const uint8_t *) data, .size = len};
WebPDemuxer *demux = WebPDemux(&wd);
if (!demux) {
g_warning("%s: %s", path, "demux failure");
- goto fail_demux;
+ goto fail;
}
uint32_t flags = WebPDemuxGetI(demux, WEBP_FF_FORMAT_FLAGS);
WebPChunkIterator chunk_iter;
if ((flags & ICCP_FLAG) &&
WebPDemuxGetChunk(demux, "ICCP", 1, &chunk_iter)) {
- cairo_surface_set_user_data(surface, &fastiv_io_key_icc,
+ cairo_surface_set_user_data(result, &fastiv_io_key_icc,
g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size),
(cairo_destroy_func_t) g_bytes_unref);
}
if ((flags & EXIF_FLAG) &&
WebPDemuxGetChunk(demux, "EXIF", 1, &chunk_iter)) {
- cairo_surface_set_user_data(surface, &fastiv_io_key_exif,
+ cairo_surface_set_user_data(result, &fastiv_io_key_exif,
g_bytes_new(chunk_iter.chunk.bytes, chunk_iter.chunk.size),
(cairo_destroy_func_t) g_bytes_unref);
}
+ if (flags & ANIMATION_FLAG) {
+ cairo_surface_set_user_data(result, &fastiv_io_key_loops,
+ (void *) (uintptr_t) WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT),
+ NULL);
+ }
WebPDemuxReleaseChunkIterator(&chunk_iter);
WebPDemuxDelete(demux);
-fail_demux:
- result = surface;
-
fail:
- if (!result)
- cairo_surface_destroy(surface);
-
WebPFreeDecBuffer(&config.output);
return result;
}
diff --git a/fastiv-view.c b/fastiv-view.c
index 3f5ae73..db11ae1 100644
--- a/fastiv-view.c
+++ b/fastiv-view.c
@@ -473,6 +473,8 @@ advance_animation(FastivView *self, GdkFrameClock *clock)
return FALSE;
// Do not busy loop. GIF timings are given in hundredths of a second.
+ // Note that browsers seem to do [< 10] => 100:
+ // https://bugs.webkit.org/show_bug.cgi?id=36082
if (duration == 0)
duration = gdk_frame_timings_get_refresh_interval(
gdk_frame_clock_get_current_timings(clock)) / 1000;