Add wuffs_gif__quirk_delay_num_decoded_frames
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index ce57f3e..28854b1 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -3299,6 +3299,12 @@
     wuffs_gif__decoder_workbuf_len_max_incl_worst_case  //
         WUFFS_BASE__POTENTIALLY_UNUSED = 1;
 
+#define WUFFS_GIF__QUIRK_DELAY_NUM_DECODED_FRAMES 1041635328
+
+static const uint32_t                          //
+    wuffs_gif__quirk_delay_num_decoded_frames  //
+        WUFFS_BASE__POTENTIALLY_UNUSED = 1041635328;
+
 #define WUFFS_GIF__QUIRK_IGNORE_TOO_MUCH_PIXEL_DATA 1041635329
 
 static const uint32_t                            //
@@ -3435,10 +3441,12 @@
     uint32_t f_metadata_fourcc_value;
     uint64_t f_metadata_chunk_length_value;
     uint64_t f_metadata_io_position;
+    bool f_quirk_enabled_delay_num_decoded_frames;
     bool f_quirk_enabled_ignore_too_much_pixel_data;
     bool f_quirk_enabled_image_bounds_are_strict;
     bool f_quirk_enabled_background_is_opaque;
     bool f_quirk_enabled_reject_empty_palette;
+    bool f_delayed_num_decoded_frames;
     bool f_end_of_data;
     bool f_restarted;
     bool f_previous_lzw_decode_ended_abruptly;
@@ -8731,7 +8739,9 @@
     return wuffs_base__make_empty_struct();
   }
 
-  if (a_quirk == 1041635329) {
+  if (a_quirk == 1041635328) {
+    self->private_impl.f_quirk_enabled_delay_num_decoded_frames = a_enabled;
+  } else if (a_quirk == 1041635329) {
     self->private_impl.f_quirk_enabled_ignore_too_much_pixel_data = a_enabled;
   } else if (a_quirk == 1041635330) {
     self->private_impl.f_quirk_enabled_image_bounds_are_strict = a_enabled;
@@ -9106,6 +9116,7 @@
   if (self->private_impl.f_call_sequence == 0) {
     return wuffs_base__error__bad_call_sequence;
   }
+  self->private_impl.f_delayed_num_decoded_frames = false;
   self->private_impl.f_end_of_data = false;
   self->private_impl.f_restarted = true;
   self->private_impl.f_frame_config_io_position = a_io_position;
@@ -9300,8 +9311,12 @@
     if (status) {
       goto suspend;
     }
-    wuffs_base__u64__sat_add_indirect(
-        &self->private_impl.f_num_decoded_frames_value, 1);
+    if (self->private_impl.f_quirk_enabled_delay_num_decoded_frames) {
+      self->private_impl.f_delayed_num_decoded_frames = true;
+    } else {
+      wuffs_base__u64__sat_add_indirect(
+          &self->private_impl.f_num_decoded_frames_value, 1);
+    }
     wuffs_gif__decoder__reset_gc(self);
 
     goto ok;
@@ -9491,6 +9506,11 @@
           goto suspend;
         }
       } else if (v_block_type == 44) {
+        if (self->private_impl.f_delayed_num_decoded_frames) {
+          self->private_impl.f_delayed_num_decoded_frames = false;
+          wuffs_base__u64__sat_add_indirect(
+              &self->private_impl.f_num_decoded_frames_value, 1);
+        }
         if (a_src.private_impl.buf) {
           a_src.private_impl.buf->meta.ri =
               ((size_t)(iop_a_src - a_src.private_impl.buf->data.ptr));
@@ -9506,6 +9526,11 @@
         }
         goto label_0_break;
       } else if (v_block_type == 59) {
+        if (self->private_impl.f_delayed_num_decoded_frames) {
+          self->private_impl.f_delayed_num_decoded_frames = false;
+          wuffs_base__u64__sat_add_indirect(
+              &self->private_impl.f_num_decoded_frames_value, 1);
+        }
         self->private_impl.f_end_of_data = true;
         goto label_0_break;
       } else {
diff --git a/std/gif/decode_gif.wuffs b/std/gif/decode_gif.wuffs
index d37643d..902fb1c 100644
--- a/std/gif/decode_gif.wuffs
+++ b/std/gif/decode_gif.wuffs
@@ -38,6 +38,22 @@
 
 // The base38 encoding of "gif " is 0xF8586.
 
+// When this quirk is enabled, when skipping over frames, the number of frames
+// visited isn't incremented when the last byte of the N'th frame is seen.
+// Instead, it is incremented when the first byte of the N+1'th frame's header
+// is seen. There may be zero or more GIF extensions between the N'th frame's
+// payload and the N+1'th frame's header.
+//
+// For a well-formed GIF, this won't have much effect. For a malformed GIF,
+// this can affect the number of valid frames, if there is an error detected in
+// the extensions between one frame's payload and the next frame's header.
+//
+// Some other GIF decoders don't register the N'th frame as complete until they
+// see the N+1'th frame's header (or the end-of-animation terminator), so that
+// e.g. the API for visiting the N'th frame can also return whether it's the
+// final frame. Enabling this quirk allows for matching that behavior.
+pub const quirk_delay_num_decoded_frames base.u32 = (0xF8586 << 10) | 0
+
 // When this quirk is enabled, silently ignore e.g. a frame that reports a
 // width and height of 6 pixels each, followed by 50 pixel values. In that
 // case, we process the first 36 pixel values and discard the excess 14.
@@ -135,11 +151,13 @@
 	metadata_chunk_length_value base.u64,
 	metadata_io_position        base.u64,
 
+	quirk_enabled_delay_num_decoded_frames   base.bool,
 	quirk_enabled_ignore_too_much_pixel_data base.bool,
 	quirk_enabled_image_bounds_are_strict    base.bool,
 	quirk_enabled_background_is_opaque       base.bool,
 	quirk_enabled_reject_empty_palette       base.bool,
 
+	delayed_num_decoded_frames         base.bool,
 	end_of_data                        base.bool,
 	restarted                          base.bool,
 	previous_lzw_decode_ended_abruptly base.bool,
@@ -194,7 +212,9 @@
 )
 
 pub func decoder.set_quirk_enabled!(quirk base.u32, enabled base.bool) {
-	if args.quirk == quirk_ignore_too_much_pixel_data {
+	if args.quirk == quirk_delay_num_decoded_frames {
+		this.quirk_enabled_delay_num_decoded_frames = args.enabled
+	} else if args.quirk == quirk_ignore_too_much_pixel_data {
 		this.quirk_enabled_ignore_too_much_pixel_data = args.enabled
 	} else if args.quirk == quirk_image_bounds_are_strict {
 		this.quirk_enabled_image_bounds_are_strict = args.enabled
@@ -336,6 +356,7 @@
 	if this.call_sequence == 0 {
 		return base."#bad call sequence"
 	}
+	this.delayed_num_decoded_frames = false
 	this.end_of_data = false
 	this.restarted = true
 	this.frame_config_io_position = args.io_position
@@ -409,7 +430,11 @@
 	// Skip the blocks of LZW-compressed data.
 	this.skip_blocks?(src:args.src)
 
-	this.num_decoded_frames_value ~sat+= 1
+	if this.quirk_enabled_delay_num_decoded_frames {
+		this.delayed_num_decoded_frames = true
+	} else {
+		this.num_decoded_frames_value ~sat+= 1
+	}
 	this.reset_gc!()
 }
 
@@ -454,9 +479,17 @@
 		if block_type == 0x21 {  // The spec calls 0x21 the "Extension Introducer".
 			this.decode_extension?(src:args.src)
 		} else if block_type == 0x2C {  // The spec calls 0x2C the "Image Separator".
+			if this.delayed_num_decoded_frames {
+				this.delayed_num_decoded_frames = false
+				this.num_decoded_frames_value ~sat+= 1
+			}
 			this.decode_id_part0?(src:args.src)
 			break
 		} else if block_type == 0x3B {  // The spec calls 0x3B the "Trailer".
+			if this.delayed_num_decoded_frames {
+				this.delayed_num_decoded_frames = false
+				this.num_decoded_frames_value ~sat+= 1
+			}
 			this.end_of_data = true
 			break
 		} else {
diff --git a/test/c/std/gif.c b/test/c/std/gif.c
index d90f04b..7457045 100644
--- a/test/c/std/gif.c
+++ b/test/c/std/gif.c
@@ -724,6 +724,60 @@
       want_frame_config_bounds);
 }
 
+const char* test_wuffs_gif_decode_delay_num_frames_decoded() {
+  CHECK_FOCUS(__func__);
+  wuffs_base__io_buffer src = ((wuffs_base__io_buffer){
+      .data = global_src_slice,
+  });
+  const char* status = read_file(&src, "test/data/animated-red-blue.gif");
+  if (status) {
+    return status;
+  }
+  if (src.meta.wi < 1) {
+    return "src file is too short";
+  }
+
+  // A GIF image should end with the 0x3B Trailer byte.
+  if (src.data.ptr[src.meta.wi - 1] != 0x3B) {
+    RETURN_FAIL("final byte: got 0x%02X, want 0x%02X",
+                src.data.ptr[src.meta.wi - 1], 0x3B);
+  }
+  // Replace that final byte with something invalid: neither 0x21 (Extension
+  // Introducer), 0x2C (Image Separator) or 0x3B (Trailer).
+  src.data.ptr[src.meta.wi - 1] = 0x99;
+
+  int q;
+  for (q = 0; q < 2; q++) {
+    src.meta.ri = 0;
+
+    wuffs_gif__decoder dec;
+    status = wuffs_gif__decoder__initialize(
+        &dec, sizeof dec, WUFFS_VERSION,
+        WUFFS_INITIALIZE__LEAVE_INTERNAL_BUFFERS_UNINITIALIZED);
+    if (status) {
+      RETURN_FAIL("q=%d: initialize: \"%s\"", q, status);
+    }
+    wuffs_gif__decoder__set_quirk_enabled(
+        &dec, wuffs_gif__quirk_delay_num_decoded_frames, q);
+
+    while (true) {
+      status = wuffs_gif__decoder__decode_frame_config(
+          &dec, NULL, wuffs_base__io_buffer__reader(&src));
+      if (status) {
+        break;
+      }
+    }
+
+    uint64_t got = wuffs_gif__decoder__num_decoded_frames(&dec);
+    uint64_t want = q ? 3 : 4;
+    if (got != want) {
+      RETURN_FAIL("q=%d: num_decoded_frames: got %" PRIu64 ", want %" PRIu64, q,
+                  got, want);
+    }
+  }
+  return NULL;
+}
+
 const char* test_wuffs_gif_decode_empty_palette() {
   CHECK_FOCUS(__func__);
   wuffs_base__io_buffer src = ((wuffs_base__io_buffer){
@@ -2141,6 +2195,7 @@
     test_wuffs_gif_decode_animated_small,                    //
     test_wuffs_gif_decode_background_color,                  //
     test_wuffs_gif_decode_bgra_nonpremul,                    //
+    test_wuffs_gif_decode_delay_num_frames_decoded,          //
     test_wuffs_gif_decode_empty_palette,                     //
     test_wuffs_gif_decode_first_frame_is_opaque,             //
     test_wuffs_gif_decode_frame_out_of_bounds,               //