Add base.image_decoder
diff --git a/lang/builtin/builtin.go b/lang/builtin/builtin.go
index ccad8ca..3fbcb14 100644
--- a/lang/builtin/builtin.go
+++ b/lang/builtin/builtin.go
@@ -346,11 +346,13 @@
 
 var Interfaces = []string{
 	"hasher_u32",
+	"image_decoder",
 	"io_transformer",
 }
 
 var InterfacesMap = map[string]bool{
 	"hasher_u32":     true,
+	"image_decoder":  true,
 	"io_transformer": true,
 }
 
@@ -359,6 +361,24 @@
 
 	"hasher_u32.update_u32!(x: slice u8) u32",
 
+	// ---- image_decoder
+
+	"image_decoder.ack_metadata_chunk?(src: io_reader)",
+	"image_decoder.decode_frame?(" +
+		"dst: ptr pixel_buffer, src: io_reader, workbuf: slice u8," +
+		"opts: nptr decode_frame_options)",
+	"image_decoder.decode_frame_config?(dst: nptr frame_config, src: io_reader)",
+	"image_decoder.decode_image_config?(dst: nptr image_config, src: io_reader)",
+	"image_decoder.frame_dirty_rect() rect_ie_u32",
+	"image_decoder.metadata_chunk_length() u64",
+	"image_decoder.metadata_fourcc() u32",
+	"image_decoder.num_animation_loops() u32",
+	"image_decoder.num_decoded_frame_configs() u64",
+	"image_decoder.num_decoded_frames() u64",
+	"image_decoder.restart_frame!(index: u64, io_position: u64) status",
+	"image_decoder.set_report_metadata!(fourcc: u32, report: bool)",
+	"image_decoder.workbuf_len() range_ii_u64",
+
 	// ---- io_transformer
 
 	"io_transformer.transform_io?(dst: io_writer, src: io_reader, workbuf: slice u8)",
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index ed4c560..ead174b 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -2604,6 +2604,190 @@
 
 // --------
 
+extern const char* wuffs_base__image_decoder__vtable_name;
+
+typedef struct {
+  wuffs_base__status (*ack_metadata_chunk)(void* self,
+                                           wuffs_base__io_buffer* a_src);
+  wuffs_base__status (*decode_frame)(void* self,
+                                     wuffs_base__pixel_buffer* a_dst,
+                                     wuffs_base__io_buffer* a_src,
+                                     wuffs_base__slice_u8 a_workbuf,
+                                     wuffs_base__decode_frame_options* a_opts);
+  wuffs_base__status (*decode_frame_config)(void* self,
+                                            wuffs_base__frame_config* a_dst,
+                                            wuffs_base__io_buffer* a_src);
+  wuffs_base__status (*decode_image_config)(void* self,
+                                            wuffs_base__image_config* a_dst,
+                                            wuffs_base__io_buffer* a_src);
+  wuffs_base__rect_ie_u32 (*frame_dirty_rect)(const void* self);
+  uint64_t (*metadata_chunk_length)(const void* self);
+  uint32_t (*metadata_fourcc)(const void* self);
+  uint32_t (*num_animation_loops)(const void* self);
+  uint64_t (*num_decoded_frame_configs)(const void* self);
+  uint64_t (*num_decoded_frames)(const void* self);
+  wuffs_base__status (*restart_frame)(void* self,
+                                      uint64_t a_index,
+                                      uint64_t a_io_position);
+  wuffs_base__empty_struct (*set_report_metadata)(void* self,
+                                                  uint32_t a_fourcc,
+                                                  bool a_report);
+  wuffs_base__range_ii_u64 (*workbuf_len)(const void* self);
+} wuffs_base__image_decoder__func_ptrs;
+
+typedef struct wuffs_base__image_decoder__struct wuffs_base__image_decoder;
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__ack_metadata_chunk(wuffs_base__image_decoder* self,
+                                              wuffs_base__io_buffer* a_src);
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__decode_frame(
+    wuffs_base__image_decoder* self,
+    wuffs_base__pixel_buffer* a_dst,
+    wuffs_base__io_buffer* a_src,
+    wuffs_base__slice_u8 a_workbuf,
+    wuffs_base__decode_frame_options* a_opts);
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__decode_frame_config(wuffs_base__image_decoder* self,
+                                               wuffs_base__frame_config* a_dst,
+                                               wuffs_base__io_buffer* a_src);
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__decode_image_config(wuffs_base__image_decoder* self,
+                                               wuffs_base__image_config* a_dst,
+                                               wuffs_base__io_buffer* a_src);
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__rect_ie_u32  //
+wuffs_base__image_decoder__frame_dirty_rect(
+    const wuffs_base__image_decoder* self);
+
+WUFFS_BASE__MAYBE_STATIC uint64_t  //
+wuffs_base__image_decoder__metadata_chunk_length(
+    const wuffs_base__image_decoder* self);
+
+WUFFS_BASE__MAYBE_STATIC uint32_t  //
+wuffs_base__image_decoder__metadata_fourcc(
+    const wuffs_base__image_decoder* self);
+
+WUFFS_BASE__MAYBE_STATIC uint32_t  //
+wuffs_base__image_decoder__num_animation_loops(
+    const wuffs_base__image_decoder* self);
+
+WUFFS_BASE__MAYBE_STATIC uint64_t  //
+wuffs_base__image_decoder__num_decoded_frame_configs(
+    const wuffs_base__image_decoder* self);
+
+WUFFS_BASE__MAYBE_STATIC uint64_t  //
+wuffs_base__image_decoder__num_decoded_frames(
+    const wuffs_base__image_decoder* self);
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__restart_frame(wuffs_base__image_decoder* self,
+                                         uint64_t a_index,
+                                         uint64_t a_io_position);
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__empty_struct  //
+wuffs_base__image_decoder__set_report_metadata(wuffs_base__image_decoder* self,
+                                               uint32_t a_fourcc,
+                                               bool a_report);
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__range_ii_u64  //
+wuffs_base__image_decoder__workbuf_len(const wuffs_base__image_decoder* self);
+
+#if defined(__cplusplus) || defined(WUFFS_IMPLEMENTATION)
+
+struct wuffs_base__image_decoder__struct {
+  struct {
+    uint32_t magic;
+    uint32_t active_coroutine;
+    wuffs_base__vtable first_vtable;
+  } private_impl;
+
+#ifdef __cplusplus
+
+  inline wuffs_base__status  //
+  ack_metadata_chunk(wuffs_base__io_buffer* a_src) {
+    return wuffs_base__image_decoder__ack_metadata_chunk(this, a_src);
+  }
+
+  inline wuffs_base__status  //
+  decode_frame(wuffs_base__pixel_buffer* a_dst,
+               wuffs_base__io_buffer* a_src,
+               wuffs_base__slice_u8 a_workbuf,
+               wuffs_base__decode_frame_options* a_opts) {
+    return wuffs_base__image_decoder__decode_frame(this, a_dst, a_src,
+                                                   a_workbuf, a_opts);
+  }
+
+  inline wuffs_base__status  //
+  decode_frame_config(wuffs_base__frame_config* a_dst,
+                      wuffs_base__io_buffer* a_src) {
+    return wuffs_base__image_decoder__decode_frame_config(this, a_dst, a_src);
+  }
+
+  inline wuffs_base__status  //
+  decode_image_config(wuffs_base__image_config* a_dst,
+                      wuffs_base__io_buffer* a_src) {
+    return wuffs_base__image_decoder__decode_image_config(this, a_dst, a_src);
+  }
+
+  inline wuffs_base__rect_ie_u32  //
+  frame_dirty_rect() const {
+    return wuffs_base__image_decoder__frame_dirty_rect(this);
+  }
+
+  inline uint64_t  //
+  metadata_chunk_length() const {
+    return wuffs_base__image_decoder__metadata_chunk_length(this);
+  }
+
+  inline uint32_t  //
+  metadata_fourcc() const {
+    return wuffs_base__image_decoder__metadata_fourcc(this);
+  }
+
+  inline uint32_t  //
+  num_animation_loops() const {
+    return wuffs_base__image_decoder__num_animation_loops(this);
+  }
+
+  inline uint64_t  //
+  num_decoded_frame_configs() const {
+    return wuffs_base__image_decoder__num_decoded_frame_configs(this);
+  }
+
+  inline uint64_t  //
+  num_decoded_frames() const {
+    return wuffs_base__image_decoder__num_decoded_frames(this);
+  }
+
+  inline wuffs_base__status  //
+  restart_frame(uint64_t a_index, uint64_t a_io_position) {
+    return wuffs_base__image_decoder__restart_frame(this, a_index,
+                                                    a_io_position);
+  }
+
+  inline wuffs_base__empty_struct  //
+  set_report_metadata(uint32_t a_fourcc, bool a_report) {
+    return wuffs_base__image_decoder__set_report_metadata(this, a_fourcc,
+                                                          a_report);
+  }
+
+  inline wuffs_base__range_ii_u64  //
+  workbuf_len() const {
+    return wuffs_base__image_decoder__workbuf_len(this);
+  }
+
+#endif  // __cplusplus
+
+};  // struct wuffs_base__image_decoder__struct
+
+#endif  // defined(__cplusplus) || defined(WUFFS_IMPLEMENTATION)
+
+// --------
+
 extern const char* wuffs_base__io_transformer__vtable_name;
 
 typedef struct {
@@ -3372,6 +3556,12 @@
 
 // ---------------- Upcasts
 
+static inline wuffs_base__image_decoder*  //
+wuffs_gif__decoder__upcast_as__wuffs_base__image_decoder(
+    wuffs_gif__decoder* p) {
+  return (wuffs_base__image_decoder*)p;
+}
+
 // ---------------- Public Function Prototypes
 
 WUFFS_BASE__MAYBE_STATIC wuffs_base__empty_struct  //
@@ -3451,6 +3641,7 @@
   struct {
     uint32_t magic;
     uint32_t active_coroutine;
+    wuffs_base__vtable vtable_for__wuffs_base__image_decoder;
     wuffs_base__vtable null_vtable;
 
     uint32_t f_width;
@@ -3606,6 +3797,11 @@
                                           initialize_flags);
   }
 
+  inline wuffs_base__image_decoder*  //
+  upcast_as__wuffs_base__image_decoder() {
+    return (wuffs_base__image_decoder*)this;
+  }
+
   inline wuffs_base__empty_struct  //
   set_quirk_enabled(uint32_t a_quirk, bool a_enabled) {
     return wuffs_gif__decoder__set_quirk_enabled(this, a_quirk, a_enabled);
@@ -4841,6 +5037,378 @@
 
 // --------
 
+const char* wuffs_base__image_decoder__vtable_name =
+    "{vtable}wuffs_base__image_decoder";
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__ack_metadata_chunk(wuffs_base__image_decoder* self,
+                                              wuffs_base__io_buffer* a_src) {
+  if (!self) {
+    return wuffs_base__make_status(wuffs_base__error__bad_receiver);
+  }
+  if (self->private_impl.magic != WUFFS_BASE__MAGIC) {
+    return wuffs_base__make_status(
+        (self->private_impl.magic == WUFFS_BASE__DISABLED)
+            ? wuffs_base__error__disabled_by_previous_error
+            : wuffs_base__error__initialize_not_called);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->ack_metadata_chunk)(self, a_src);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__make_status(wuffs_base__error__bad_vtable);
+}
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__decode_frame(
+    wuffs_base__image_decoder* self,
+    wuffs_base__pixel_buffer* a_dst,
+    wuffs_base__io_buffer* a_src,
+    wuffs_base__slice_u8 a_workbuf,
+    wuffs_base__decode_frame_options* a_opts) {
+  if (!self) {
+    return wuffs_base__make_status(wuffs_base__error__bad_receiver);
+  }
+  if (self->private_impl.magic != WUFFS_BASE__MAGIC) {
+    return wuffs_base__make_status(
+        (self->private_impl.magic == WUFFS_BASE__DISABLED)
+            ? wuffs_base__error__disabled_by_previous_error
+            : wuffs_base__error__initialize_not_called);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->decode_frame)(self, a_dst, a_src, a_workbuf, a_opts);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__make_status(wuffs_base__error__bad_vtable);
+}
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__decode_frame_config(wuffs_base__image_decoder* self,
+                                               wuffs_base__frame_config* a_dst,
+                                               wuffs_base__io_buffer* a_src) {
+  if (!self) {
+    return wuffs_base__make_status(wuffs_base__error__bad_receiver);
+  }
+  if (self->private_impl.magic != WUFFS_BASE__MAGIC) {
+    return wuffs_base__make_status(
+        (self->private_impl.magic == WUFFS_BASE__DISABLED)
+            ? wuffs_base__error__disabled_by_previous_error
+            : wuffs_base__error__initialize_not_called);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->decode_frame_config)(self, a_dst, a_src);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__make_status(wuffs_base__error__bad_vtable);
+}
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__decode_image_config(wuffs_base__image_decoder* self,
+                                               wuffs_base__image_config* a_dst,
+                                               wuffs_base__io_buffer* a_src) {
+  if (!self) {
+    return wuffs_base__make_status(wuffs_base__error__bad_receiver);
+  }
+  if (self->private_impl.magic != WUFFS_BASE__MAGIC) {
+    return wuffs_base__make_status(
+        (self->private_impl.magic == WUFFS_BASE__DISABLED)
+            ? wuffs_base__error__disabled_by_previous_error
+            : wuffs_base__error__initialize_not_called);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->decode_image_config)(self, a_dst, a_src);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__make_status(wuffs_base__error__bad_vtable);
+}
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__rect_ie_u32  //
+wuffs_base__image_decoder__frame_dirty_rect(
+    const wuffs_base__image_decoder* self) {
+  if (!self) {
+    return wuffs_base__utility__make_rect_ie_u32(0, 0, 0, 0);
+  }
+  if ((self->private_impl.magic != WUFFS_BASE__MAGIC) &&
+      (self->private_impl.magic != WUFFS_BASE__DISABLED)) {
+    return wuffs_base__utility__make_rect_ie_u32(0, 0, 0, 0);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->frame_dirty_rect)(self);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__utility__make_rect_ie_u32(0, 0, 0, 0);
+}
+
+WUFFS_BASE__MAYBE_STATIC uint64_t  //
+wuffs_base__image_decoder__metadata_chunk_length(
+    const wuffs_base__image_decoder* self) {
+  if (!self) {
+    return 0;
+  }
+  if ((self->private_impl.magic != WUFFS_BASE__MAGIC) &&
+      (self->private_impl.magic != WUFFS_BASE__DISABLED)) {
+    return 0;
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->metadata_chunk_length)(self);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return 0;
+}
+
+WUFFS_BASE__MAYBE_STATIC uint32_t  //
+wuffs_base__image_decoder__metadata_fourcc(
+    const wuffs_base__image_decoder* self) {
+  if (!self) {
+    return 0;
+  }
+  if ((self->private_impl.magic != WUFFS_BASE__MAGIC) &&
+      (self->private_impl.magic != WUFFS_BASE__DISABLED)) {
+    return 0;
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->metadata_fourcc)(self);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return 0;
+}
+
+WUFFS_BASE__MAYBE_STATIC uint32_t  //
+wuffs_base__image_decoder__num_animation_loops(
+    const wuffs_base__image_decoder* self) {
+  if (!self) {
+    return 0;
+  }
+  if ((self->private_impl.magic != WUFFS_BASE__MAGIC) &&
+      (self->private_impl.magic != WUFFS_BASE__DISABLED)) {
+    return 0;
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->num_animation_loops)(self);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return 0;
+}
+
+WUFFS_BASE__MAYBE_STATIC uint64_t  //
+wuffs_base__image_decoder__num_decoded_frame_configs(
+    const wuffs_base__image_decoder* self) {
+  if (!self) {
+    return 0;
+  }
+  if ((self->private_impl.magic != WUFFS_BASE__MAGIC) &&
+      (self->private_impl.magic != WUFFS_BASE__DISABLED)) {
+    return 0;
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->num_decoded_frame_configs)(self);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return 0;
+}
+
+WUFFS_BASE__MAYBE_STATIC uint64_t  //
+wuffs_base__image_decoder__num_decoded_frames(
+    const wuffs_base__image_decoder* self) {
+  if (!self) {
+    return 0;
+  }
+  if ((self->private_impl.magic != WUFFS_BASE__MAGIC) &&
+      (self->private_impl.magic != WUFFS_BASE__DISABLED)) {
+    return 0;
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->num_decoded_frames)(self);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return 0;
+}
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__status  //
+wuffs_base__image_decoder__restart_frame(wuffs_base__image_decoder* self,
+                                         uint64_t a_index,
+                                         uint64_t a_io_position) {
+  if (!self) {
+    return wuffs_base__make_status(wuffs_base__error__bad_receiver);
+  }
+  if (self->private_impl.magic != WUFFS_BASE__MAGIC) {
+    return wuffs_base__make_status(
+        (self->private_impl.magic == WUFFS_BASE__DISABLED)
+            ? wuffs_base__error__disabled_by_previous_error
+            : wuffs_base__error__initialize_not_called);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->restart_frame)(self, a_index, a_io_position);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__make_status(wuffs_base__error__bad_vtable);
+}
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__empty_struct  //
+wuffs_base__image_decoder__set_report_metadata(wuffs_base__image_decoder* self,
+                                               uint32_t a_fourcc,
+                                               bool a_report) {
+  if (!self) {
+    return wuffs_base__make_empty_struct();
+  }
+  if (self->private_impl.magic != WUFFS_BASE__MAGIC) {
+    return wuffs_base__make_empty_struct();
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->set_report_metadata)(self, a_fourcc, a_report);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__make_empty_struct();
+}
+
+WUFFS_BASE__MAYBE_STATIC wuffs_base__range_ii_u64  //
+wuffs_base__image_decoder__workbuf_len(const wuffs_base__image_decoder* self) {
+  if (!self) {
+    return wuffs_base__utility__make_range_ii_u64(0, 0);
+  }
+  if ((self->private_impl.magic != WUFFS_BASE__MAGIC) &&
+      (self->private_impl.magic != WUFFS_BASE__DISABLED)) {
+    return wuffs_base__utility__make_range_ii_u64(0, 0);
+  }
+
+  const wuffs_base__vtable* v = &self->private_impl.first_vtable;
+  int i;
+  for (i = 0; i < 63; i++) {
+    if (v->vtable_name == wuffs_base__image_decoder__vtable_name) {
+      const wuffs_base__image_decoder__func_ptrs* func_ptrs =
+          (const wuffs_base__image_decoder__func_ptrs*)(v->function_pointers);
+      return (*func_ptrs->workbuf_len)(self);
+    } else if (v->vtable_name == NULL) {
+      break;
+    }
+    v++;
+  }
+
+  return wuffs_base__utility__make_range_ii_u64(0, 0);
+}
+
+// --------
+
 const char* wuffs_base__io_transformer__vtable_name =
     "{vtable}wuffs_base__io_transformer";
 
@@ -8926,6 +9494,40 @@
 
 // ---------------- VTables
 
+const wuffs_base__image_decoder__func_ptrs
+    wuffs_gif__decoder__func_ptrs_for__wuffs_base__image_decoder = {
+        (wuffs_base__status(*)(void*, wuffs_base__io_buffer*))(
+            &wuffs_gif__decoder__ack_metadata_chunk),
+        (wuffs_base__status(*)(void*,
+                               wuffs_base__pixel_buffer*,
+                               wuffs_base__io_buffer*,
+                               wuffs_base__slice_u8,
+                               wuffs_base__decode_frame_options*))(
+            &wuffs_gif__decoder__decode_frame),
+        (wuffs_base__status(*)(void*,
+                               wuffs_base__frame_config*,
+                               wuffs_base__io_buffer*))(
+            &wuffs_gif__decoder__decode_frame_config),
+        (wuffs_base__status(*)(void*,
+                               wuffs_base__image_config*,
+                               wuffs_base__io_buffer*))(
+            &wuffs_gif__decoder__decode_image_config),
+        (wuffs_base__rect_ie_u32(*)(const void*))(
+            &wuffs_gif__decoder__frame_dirty_rect),
+        (uint64_t(*)(const void*))(&wuffs_gif__decoder__metadata_chunk_length),
+        (uint32_t(*)(const void*))(&wuffs_gif__decoder__metadata_fourcc),
+        (uint32_t(*)(const void*))(&wuffs_gif__decoder__num_animation_loops),
+        (uint64_t(*)(const void*))(
+            &wuffs_gif__decoder__num_decoded_frame_configs),
+        (uint64_t(*)(const void*))(&wuffs_gif__decoder__num_decoded_frames),
+        (wuffs_base__status(*)(void*, uint64_t, uint64_t))(
+            &wuffs_gif__decoder__restart_frame),
+        (wuffs_base__empty_struct(*)(void*, uint32_t, bool))(
+            &wuffs_gif__decoder__set_report_metadata),
+        (wuffs_base__range_ii_u64(*)(const void*))(
+            &wuffs_gif__decoder__workbuf_len),
+};
+
 // ---------------- Initializer Implementations
 
 wuffs_base__status WUFFS_BASE__WARN_UNUSED_RESULT  //
@@ -8977,6 +9579,10 @@
     }
   }
   self->private_impl.magic = WUFFS_BASE__MAGIC;
+  self->private_impl.vtable_for__wuffs_base__image_decoder.vtable_name =
+      wuffs_base__image_decoder__vtable_name;
+  self->private_impl.vtable_for__wuffs_base__image_decoder.function_pointers =
+      (const void*)(&wuffs_gif__decoder__func_ptrs_for__wuffs_base__image_decoder);
   return wuffs_base__make_status(NULL);
 }
 
diff --git a/std/gif/decode_gif.wuffs b/std/gif/decode_gif.wuffs
index 2019331..e4606a1 100644
--- a/std/gif/decode_gif.wuffs
+++ b/std/gif/decode_gif.wuffs
@@ -46,7 +46,7 @@
 pri const interlace_delta array[5] base.u8 = [1, 2, 4, 8, 8]
 pri const interlace_count array[5] base.u8 = [0, 1, 2, 4, 8]
 
-pub struct decoder?(
+pub struct decoder? implements base.image_decoder(
 	width  : base.u32,
 	height : base.u32,
 
diff --git a/test/c/std/gif.c b/test/c/std/gif.c
index 4fdd5ce..a3fa11f 100644
--- a/test/c/std/gif.c
+++ b/test/c/std/gif.c
@@ -195,6 +195,18 @@
 
 // ---------------- GIF Tests
 
+const char* test_wuffs_gif_decode_interface() {
+  CHECK_FOCUS(__func__);
+  wuffs_gif__decoder dec;
+  CHECK_STATUS("initialize",
+               wuffs_gif__decoder__initialize(
+                   &dec, sizeof dec, WUFFS_VERSION,
+                   WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED));
+  return do_test__wuffs_base__image_decoder(
+      wuffs_gif__decoder__upcast_as__wuffs_base__image_decoder(&dec),
+      "test/data/bricks-nodither.gif", 0, SIZE_MAX, 160, 120, 0xFF012463);
+}
+
 const char* wuffs_gif_decode(wuffs_base__io_buffer* dst,
                              uint32_t wuffs_initialize_flags,
                              wuffs_base__pixel_format pixfmt,
@@ -2267,6 +2279,7 @@
     test_wuffs_gif_decode_input_is_a_gif_many_medium_reads,  //
     test_wuffs_gif_decode_input_is_a_gif_many_small_reads,   //
     test_wuffs_gif_decode_input_is_a_png,                    //
+    test_wuffs_gif_decode_interface,                         //
     test_wuffs_gif_decode_interlaced_truncated,              //
     test_wuffs_gif_decode_metadata_empty,                    //
     test_wuffs_gif_decode_metadata_full,                     //
diff --git a/test/c/testlib/testlib.c b/test/c/testlib/testlib.c
index ed00ac7..dda75ec 100644
--- a/test/c/testlib/testlib.c
+++ b/test/c/testlib/testlib.c
@@ -784,6 +784,61 @@
   return NULL;
 }
 
+const char* do_test__wuffs_base__image_decoder(
+    wuffs_base__image_decoder* b,
+    const char* src_filename,
+    size_t src_ri,
+    size_t src_wi,
+    uint32_t want_width,
+    uint32_t want_height,
+    wuffs_base__color_u32_argb_premul want_final_pixel) {
+  if ((want_width > 16384) || (want_height > 16384) ||
+      ((want_width * want_height * 4) > BUFFER_SIZE)) {
+    return "want dimensions are too large";
+  }
+
+  wuffs_base__image_config ic = ((wuffs_base__image_config){});
+  wuffs_base__io_buffer src = ((wuffs_base__io_buffer){
+      .data = global_src_slice,
+  });
+  CHECK_STRING(read_file_fragment(&src, src_filename, src_ri, src_wi));
+  CHECK_STATUS("decode_image_config",
+               wuffs_base__image_decoder__decode_image_config(b, &ic, &src));
+
+  uint32_t got_width = wuffs_base__pixel_config__width(&ic.pixcfg);
+  if (got_width != want_width) {
+    RETURN_FAIL("width: got %" PRIu32 ", want %" PRIu32, got_width, want_width);
+  }
+  uint32_t got_height = wuffs_base__pixel_config__height(&ic.pixcfg);
+  if (got_height != want_height) {
+    RETURN_FAIL("height: got %" PRIu32 ", want %" PRIu32, got_height,
+                want_height);
+  }
+  wuffs_base__pixel_config__set(
+      &ic.pixcfg, WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL,
+      WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, want_width, want_height);
+
+  wuffs_base__pixel_buffer pb = ((wuffs_base__pixel_buffer){});
+  CHECK_STATUS("set_from_slice", wuffs_base__pixel_buffer__set_from_slice(
+                                     &pb, &ic.pixcfg, global_pixel_slice));
+  CHECK_STATUS("decode_frame", wuffs_base__image_decoder__decode_frame(
+                                   b, &pb, &src, global_work_slice, NULL));
+
+  uint64_t n = wuffs_base__pixel_config__pixbuf_len(&ic.pixcfg);
+  if (n < 4) {
+    RETURN_FAIL("pixbuf_len too small");
+  } else if (n > BUFFER_SIZE) {
+    RETURN_FAIL("pixbuf_len too large");
+  }
+  wuffs_base__color_u32_argb_premul got_final_pixel =
+      wuffs_base__load_u32le(&global_pixel_array[n - 4]);
+  if (got_final_pixel != want_final_pixel) {
+    RETURN_FAIL("final pixel: got 0x%08" PRIX32 ", want 0x%08" PRIX32,
+                got_final_pixel, want_final_pixel);
+  }
+  return NULL;
+}
+
 const char* do_test__wuffs_base__io_transformer(wuffs_base__io_transformer* b,
                                                 const char* src_filename,
                                                 size_t src_ri,