std/thumbhash: add QUIRK_JUST_RAW_THUMBHASH
diff --git a/doc/note/quirks.md b/doc/note/quirks.md
index 006631b..8ebdf18 100644
--- a/doc/note/quirks.md
+++ b/doc/note/quirks.md
@@ -81,5 +81,6 @@
 - [JSON decoder quirks](/std/json/decode_quirks.wuffs)
 - [LZMA decoder quirks](/std/lzma/decode_quirks.wuffs)
 - [LZW decoder quirks](/std/lzw/decode_quirks.wuffs)
+- [TH decoder quirks](/std/thumbhash/decode_quirks.wuffs)
 - [XZ decoder quirks](/std/xz/decode_quirks.wuffs)
 - [ZLIB decoder quirks](/std/zlib/decode_quirks.wuffs)
diff --git a/doc/std/image-decoders.md b/doc/std/image-decoders.md
index 8b44eba..4683baf 100644
--- a/doc/std/image-decoders.md
+++ b/doc/std/image-decoders.md
@@ -158,6 +158,8 @@
 ## [Quirks](/doc/note/quirks.md)
 
 - [GIF decoder quirks](/std/gif/decode_quirks.wuffs)
+- [JPEG decoder quirks](/std/jpeg/decode_quirks.wuffs)
+- [TH decoder quirks](/std/thumbhash/decode_quirks.wuffs)
 
 
 ## Related Documentation
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index b31ec0e..fd0f26f 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -14107,6 +14107,8 @@
 
 // ---------------- Public Consts
 
+#define WUFFS_THUMBHASH__QUIRK_JUST_RAW_THUMBHASH 1712283648u
+
 #define WUFFS_THUMBHASH__DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE 0u
 
 // ---------------- Struct Declarations
@@ -14281,6 +14283,7 @@
     uint64_t f_p_dc;
     uint64_t f_q_dc;
     uint64_t f_a_dc;
+    bool f_quirk_just_raw_thumbhash;
     uint8_t f_l_scale;
     uint8_t f_p_scale;
     uint8_t f_q_scale;
@@ -73313,6 +73316,8 @@
 
 // ---------------- Private Consts
 
+#define WUFFS_THUMBHASH__QUIRKS_BASE 1712283648u
+
 static const uint8_t
 WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[8] WUFFS_BASE__POTENTIALLY_UNUSED = {
   0u, 14u, 18u, 19u, 23u, 26u, 27u, 32u,
@@ -73855,6 +73860,9 @@
     return 0;
   }
 
+  if ((a_key == 1712283648u) && self->private_impl.f_quirk_just_raw_thumbhash) {
+    return 1u;
+  }
   return 0u;
 }
 
@@ -73876,6 +73884,10 @@
         : wuffs_base__error__initialize_not_called);
   }
 
+  if (a_key == 1712283648u) {
+    self->private_impl.f_quirk_just_raw_thumbhash = (a_value > 0u);
+    return wuffs_base__make_status(NULL);
+  }
   return wuffs_base__make_status(wuffs_base__error__unsupported_option);
 }
 
@@ -73977,38 +73989,40 @@
       status = wuffs_base__make_status(wuffs_base__error__bad_call_sequence);
       goto exit;
     }
-    {
-      WUFFS_BASE__COROUTINE_SUSPENSION_POINT(1);
-      uint32_t t_0;
-      if (WUFFS_BASE__LIKELY(io2_a_src - iop_a_src >= 3)) {
-        t_0 = ((uint32_t)(wuffs_base__peek_u24le__no_bounds_check(iop_a_src)));
-        iop_a_src += 3;
-      } else {
-        self->private_data.s_do_decode_image_config.scratch = 0;
-        WUFFS_BASE__COROUTINE_SUSPENSION_POINT(2);
-        while (true) {
-          if (WUFFS_BASE__UNLIKELY(iop_a_src == io2_a_src)) {
-            status = wuffs_base__make_status(wuffs_base__suspension__short_read);
-            goto suspend;
+    if ( ! self->private_impl.f_quirk_just_raw_thumbhash) {
+      {
+        WUFFS_BASE__COROUTINE_SUSPENSION_POINT(1);
+        uint32_t t_0;
+        if (WUFFS_BASE__LIKELY(io2_a_src - iop_a_src >= 3)) {
+          t_0 = ((uint32_t)(wuffs_base__peek_u24le__no_bounds_check(iop_a_src)));
+          iop_a_src += 3;
+        } else {
+          self->private_data.s_do_decode_image_config.scratch = 0;
+          WUFFS_BASE__COROUTINE_SUSPENSION_POINT(2);
+          while (true) {
+            if (WUFFS_BASE__UNLIKELY(iop_a_src == io2_a_src)) {
+              status = wuffs_base__make_status(wuffs_base__suspension__short_read);
+              goto suspend;
+            }
+            uint64_t* scratch = &self->private_data.s_do_decode_image_config.scratch;
+            uint32_t num_bits_0 = ((uint32_t)(*scratch >> 56));
+            *scratch <<= 8;
+            *scratch >>= 8;
+            *scratch |= ((uint64_t)(*iop_a_src++)) << num_bits_0;
+            if (num_bits_0 == 16) {
+              t_0 = ((uint32_t)(*scratch));
+              break;
+            }
+            num_bits_0 += 8u;
+            *scratch |= ((uint64_t)(num_bits_0)) << 56;
           }
-          uint64_t* scratch = &self->private_data.s_do_decode_image_config.scratch;
-          uint32_t num_bits_0 = ((uint32_t)(*scratch >> 56));
-          *scratch <<= 8;
-          *scratch >>= 8;
-          *scratch |= ((uint64_t)(*iop_a_src++)) << num_bits_0;
-          if (num_bits_0 == 16) {
-            t_0 = ((uint32_t)(*scratch));
-            break;
-          }
-          num_bits_0 += 8u;
-          *scratch |= ((uint64_t)(num_bits_0)) << 56;
         }
+        v_c32 = t_0;
       }
-      v_c32 = t_0;
-    }
-    if (v_c32 != 16694979u) {
-      status = wuffs_base__make_status(wuffs_thumbhash__error__bad_header);
-      goto exit;
+      if (v_c32 != 16694979u) {
+        status = wuffs_base__make_status(wuffs_thumbhash__error__bad_header);
+        goto exit;
+      }
     }
     {
       WUFFS_BASE__COROUTINE_SUSPENSION_POINT(3);
@@ -74093,7 +74107,6 @@
     }
     self->private_impl.f_frame_config_io_position = 8u;
     if (self->private_impl.f_has_alpha != 0u) {
-      self->private_impl.f_frame_config_io_position = 9u;
       {
         WUFFS_BASE__COROUTINE_SUSPENSION_POINT(7);
         if (WUFFS_BASE__UNLIKELY(iop_a_src == io2_a_src)) {
@@ -74105,6 +74118,17 @@
       }
       self->private_impl.f_a_dc = (((uint64_t)(((v_c32 >> 0u) & 15u))) << 42u);
       self->private_impl.f_a_scale = ((uint8_t)(((v_c32 >> 4u) & 15u)));
+      self->private_impl.f_frame_config_io_position = 9u;
+    }
+    if (self->private_impl.f_quirk_just_raw_thumbhash) {
+#if defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+#endif
+      self->private_impl.f_frame_config_io_position -= 3u;
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
     }
     self->private_impl.f_pixfmt = 2415954056u;
     if (self->private_impl.f_has_alpha != 0u) {
diff --git a/std/thumbhash/decode_quirks.wuffs b/std/thumbhash/decode_quirks.wuffs
new file mode 100644
index 0000000..f5f4287
--- /dev/null
+++ b/std/thumbhash/decode_quirks.wuffs
@@ -0,0 +1,26 @@
+// Copyright 2024 The Wuffs Authors.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+//
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+// --------
+
+// Quirks are discussed in (/doc/note/quirks.md).
+//
+// The base38 encoding of "th.." is 0x19_83D8. Left shifting by 10 gives
+// 0x660F_6000.
+pri const QUIRKS_BASE : base.u32 = 0x660F_6000
+
+// --------
+
+// When this quirk is enabled, the decoder speaks just raw thumbhash, without
+// the "\xC3\xBE\xFE" magic identifier to start the file.
+//
+// Enabling this quirk matches the behavior of the original JavaScript
+// reference implementation at https://evanw.github.io/thumbhash/
+pub const QUIRK_JUST_RAW_THUMBHASH : base.u32 = 0x660F_6000 | 0x00
diff --git a/std/thumbhash/decode_thumbhash.wuffs b/std/thumbhash/decode_thumbhash.wuffs
index 7556ef6..487ea1c 100644
--- a/std/thumbhash/decode_thumbhash.wuffs
+++ b/std/thumbhash/decode_thumbhash.wuffs
@@ -71,6 +71,8 @@
         q_dc : base.u64,  // Fixed-point denominator is (LDENOM << 28).
         a_dc : base.u64,  // Fixed-point denominator is (ADENOM << 28).
 
+        quirk_just_raw_thumbhash : base.bool,
+
         l_scale : base.u8[..= 31],
         p_scale : base.u8[..= 63],
         q_scale : base.u8[..= 63],
@@ -185,10 +187,17 @@
 )
 
 pub func decoder.get_quirk(key: base.u32) base.u64 {
+    if (args.key == QUIRK_JUST_RAW_THUMBHASH) and this.quirk_just_raw_thumbhash {
+        return 1
+    }
     return 0
 }
 
 pub func decoder.set_quirk!(key: base.u32, value: base.u64) base.status {
+    if args.key == QUIRK_JUST_RAW_THUMBHASH {
+        this.quirk_just_raw_thumbhash = args.value > 0
+        return ok
+    }
     return base."#unsupported option"
 }
 
@@ -212,10 +221,11 @@
         return base."#bad call sequence"
     }
 
-    // TODO: implement QUIRK_JUST_RAW_THUMBHASH.
-    c32 = args.src.read_u24le_as_u32?()
-    if c32 <> '\xC3\xBE\xFE'le {
-        return "#bad header"
+    if not this.quirk_just_raw_thumbhash {
+        c32 = args.src.read_u24le_as_u32?()
+        if c32 <> '\xC3\xBE\xFE'le {
+            return "#bad header"
+        }
     }
 
     c32 = args.src.read_u24le_as_u32?()
@@ -247,11 +257,17 @@
     }
 
     this.frame_config_io_position = 8
+    assert this.frame_config_io_position >= 8
     if this.has_alpha <> 0 {
-        this.frame_config_io_position = 9
         c32 = args.src.read_u8_as_u32?()
         this.a_dc = (((c32 >> 0x00) & 15) as base.u64) << 42
         this.a_scale = ((c32 >> 0x04) & 15) as base.u8
+        this.frame_config_io_position = 9
+        assert this.frame_config_io_position >= 8
+    }
+
+    if this.quirk_just_raw_thumbhash {
+        this.frame_config_io_position -= 3
     }
 
     this.pixfmt = base.PIXEL_FORMAT__BGRX
diff --git a/test/c/std/thumbhash.c b/test/c/std/thumbhash.c
index 9715dfc..908bce3 100644
--- a/test/c/std/thumbhash.c
+++ b/test/c/std/thumbhash.c
@@ -108,8 +108,7 @@
 }
 
 const char*  //
-test_wuffs_thumbhash_decode_frame_config() {
-  CHECK_FOCUS(__func__);
+do_test_wuffs_thumbhash_decode_frame_config(bool raw) {
   wuffs_thumbhash__decoder dec;
   CHECK_STATUS("initialize",
                wuffs_thumbhash__decoder__initialize(
@@ -122,9 +121,25 @@
   });
   CHECK_STRING(read_file(
       &src, "test/data/artificial-thumbhash/3OcRJYB4d3h_iIeHeEh3eIhw-j3A.th"));
+
+  if (raw) {
+    if ((src.meta.wi <= src.meta.ri) || ((src.meta.wi - src.meta.ri) < 3)) {
+      return "could not skip 3 bytes";
+    }
+    src.meta.ri += 3;  // The number of bytes in "\xC3\xBE\xFE".
+    wuffs_thumbhash__decoder__set_quirk(
+        &dec, WUFFS_THUMBHASH__QUIRK_JUST_RAW_THUMBHASH, 1);
+  }
+
   CHECK_STATUS("decode_frame_config #0",
                wuffs_thumbhash__decoder__decode_frame_config(&dec, &fc, &src));
 
+  uint32_t have = wuffs_base__frame_config__height(&fc);
+  uint32_t want = 23;
+  if (have != want) {
+    RETURN_FAIL("height: have %u, want %u", have, want);
+  }
+
   wuffs_base__status status =
       wuffs_thumbhash__decoder__decode_frame_config(&dec, &fc, &src);
   if (status.repr != wuffs_base__note__end_of_data) {
@@ -134,6 +149,18 @@
   return NULL;
 }
 
+const char*  //
+test_wuffs_thumbhash_decode_frame_config_cooked() {
+  CHECK_FOCUS(__func__);
+  return do_test_wuffs_thumbhash_decode_frame_config(false);
+}
+
+const char*  //
+test_wuffs_thumbhash_decode_frame_config_raw() {
+  CHECK_FOCUS(__func__);
+  return do_test_wuffs_thumbhash_decode_frame_config(true);
+}
+
 // ---------------- Mimic Tests
 
 #ifdef WUFFS_MIMIC
@@ -158,7 +185,8 @@
 
 proc g_tests[] = {
 
-    test_wuffs_thumbhash_decode_frame_config,
+    test_wuffs_thumbhash_decode_frame_config_cooked,
+    test_wuffs_thumbhash_decode_frame_config_raw,
     test_wuffs_thumbhash_decode_interface,
     test_wuffs_thumbhash_decode_truncated_input,