std/thumbhash: implement do_decode_frame
diff --git a/internal/cgen/base/fundamental-private.h b/internal/cgen/base/fundamental-private.h
index fb72c8a..204e56c 100644
--- a/internal/cgen/base/fundamental-private.h
+++ b/internal/cgen/base/fundamental-private.h
@@ -173,12 +173,24 @@
 
 // ---------------- Numeric Types (Utility)
 
+#define wuffs_base__utility__i64_divide(a, b) \
+  ((uint64_t)(((int64_t)(a)) / ((int64_t)(b))))
+
 #define wuffs_base__utility__sign_extend_convert_u8_u32(a) \
   ((uint32_t)(int32_t)(int8_t)(a))
 
+#define wuffs_base__utility__sign_extend_convert_u8_u64(a) \
+  ((uint64_t)(int64_t)(int8_t)(a))
+
 #define wuffs_base__utility__sign_extend_convert_u16_u32(a) \
   ((uint32_t)(int32_t)(int16_t)(a))
 
+#define wuffs_base__utility__sign_extend_convert_u16_u64(a) \
+  ((uint64_t)(int64_t)(int16_t)(a))
+
+#define wuffs_base__utility__sign_extend_convert_u32_u64(a) \
+  ((uint64_t)(int64_t)(int32_t)(a))
+
 #define wuffs_base__utility__sign_extend_rshift_u32(a, n) \
   ((uint32_t)(((int32_t)(a)) >> (n)))
 
diff --git a/lang/builtin/builtin.go b/lang/builtin/builtin.go
index 7ce3152..976fff2 100644
--- a/lang/builtin/builtin.go
+++ b/lang/builtin/builtin.go
@@ -389,6 +389,7 @@
 	"utility.empty_rect_ii_u32() rect_ii_u32",
 	"utility.empty_rect_ie_u32() rect_ie_u32",
 	"utility.empty_slice_u8() slice u8",
+	"utility.i64_divide(a: u64, b: u64[1 ..=]) u64",
 	"utility.make_bitvec256(e00: u64, e01: u64, e02: u64, e03: u64) bitvec256",
 	"utility.make_optional_u63(has_value: bool, value: u64[..= 0x7FFF_FFFF_FFFF_FFFF]) optional_u63",
 	"utility.make_pixel_format(repr: u32) pixel_format",
@@ -401,7 +402,10 @@
 	"utility.make_rect_ie_u32(" +
 		"min_incl_x: u32, min_incl_y: u32, max_excl_x: u32, max_excl_y: u32) rect_ie_u32",
 	"utility.sign_extend_convert_u8_u32(a: u8) u32",
+	"utility.sign_extend_convert_u8_u64(a: u8) u64",
 	"utility.sign_extend_convert_u16_u32(a: u16) u32",
+	"utility.sign_extend_convert_u16_u64(a: u16) u64",
+	"utility.sign_extend_convert_u32_u64(a: u32) u64",
 	"utility.sign_extend_rshift_u32(a: u32, n: u32[..= 31]) u32",
 	"utility.sign_extend_rshift_u64(a: u64, n: u32[..= 63]) u64",
 
diff --git a/release/c/wuffs-unsupported-snapshot.c b/release/c/wuffs-unsupported-snapshot.c
index f4a39bc..b31ec0e 100644
--- a/release/c/wuffs-unsupported-snapshot.c
+++ b/release/c/wuffs-unsupported-snapshot.c
@@ -14273,9 +14273,23 @@
     wuffs_base__vtable null_vtable;
 
     uint32_t f_pixfmt;
-    uint32_t f_width;
-    uint32_t f_height;
+    uint8_t f_w_dimension_code;
+    uint8_t f_h_dimension_code;
     uint8_t f_call_sequence;
+    uint8_t f_frame_config_io_position;
+    uint64_t f_l_dc;
+    uint64_t f_p_dc;
+    uint64_t f_q_dc;
+    uint64_t f_a_dc;
+    uint8_t f_l_scale;
+    uint8_t f_p_scale;
+    uint8_t f_q_scale;
+    uint8_t f_a_scale;
+    uint8_t f_has_alpha;
+    uint8_t f_l_count;
+    uint8_t f_is_landscape;
+    uint32_t f_lx;
+    uint32_t f_ly;
     wuffs_base__pixel_swizzler f_swizzler;
 
     uint32_t p_decode_image_config;
@@ -14284,14 +14298,25 @@
     uint32_t p_do_decode_frame_config;
     uint32_t p_decode_frame;
     uint32_t p_do_decode_frame;
+    uint32_t p_from_src_to_coeffs;
   } private_impl;
 
   struct {
+    uint32_t f_lac[32];
+    uint32_t f_pac[8];
+    uint32_t f_qac[8];
+    uint32_t f_aac[16];
     uint8_t f_pixels[32][128];
 
     struct {
       uint64_t scratch;
     } s_do_decode_image_config;
+    struct {
+      uint32_t v_cy;
+      uint32_t v_cx;
+      uint32_t v_i;
+      bool v_has_bits;
+    } s_from_src_to_coeffs;
   } private_data;
 
 #ifdef __cplusplus
@@ -17014,12 +17039,24 @@
 
 // ---------------- Numeric Types (Utility)
 
+#define wuffs_base__utility__i64_divide(a, b) \
+  ((uint64_t)(((int64_t)(a)) / ((int64_t)(b))))
+
 #define wuffs_base__utility__sign_extend_convert_u8_u32(a) \
   ((uint32_t)(int32_t)(int8_t)(a))
 
+#define wuffs_base__utility__sign_extend_convert_u8_u64(a) \
+  ((uint64_t)(int64_t)(int8_t)(a))
+
 #define wuffs_base__utility__sign_extend_convert_u16_u32(a) \
   ((uint32_t)(int32_t)(int16_t)(a))
 
+#define wuffs_base__utility__sign_extend_convert_u16_u64(a) \
+  ((uint64_t)(int64_t)(int16_t)(a))
+
+#define wuffs_base__utility__sign_extend_convert_u32_u64(a) \
+  ((uint64_t)(int64_t)(int32_t)(a))
+
 #define wuffs_base__utility__sign_extend_rshift_u32(a, n) \
   ((uint32_t)(((int32_t)(a)) >> (n)))
 
@@ -73276,6 +73313,379 @@
 
 // ---------------- Private Consts
 
+static const uint8_t
+WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[8] WUFFS_BASE__POTENTIALLY_UNUSED = {
+  0u, 14u, 18u, 19u, 23u, 26u, 27u, 32u,
+};
+
+static const uint8_t
+WUFFS_THUMBHASH__CUMULATIVE_DIMENSIONS[8] WUFFS_BASE__POTENTIALLY_UNUSED = {
+  0u, 0u, 14u, 32u, 51u, 74u, 100u, 127u,
+};
+
+static const uint8_t
+WUFFS_THUMBHASH__DIMENSION_CODES_FROM_HAS_ALPHA_AND_L_COUNT[2][8] WUFFS_BASE__POTENTIALLY_UNUSED = {
+  {
+    23u, 23u, 23u, 23u, 39u, 71u, 103u, 119u,
+  }, {
+    55u, 55u, 55u, 55u, 87u, 119u, 118u, 116u,
+  },
+};
+
+static const uint16_t
+WUFFS_THUMBHASH__FROM_4_BITS_TO_PLUS_MINUS_1_00[16] WUFFS_BASE__POTENTIALLY_UNUSED = {
+  49152u, 51337u, 53521u, 55706u, 57890u, 60075u, 62259u, 64444u,
+  1092u, 3277u, 5461u, 7646u, 9830u, 12015u, 14199u, 16384u,
+};
+
+static const uint16_t
+WUFFS_THUMBHASH__FROM_4_BITS_TO_PLUS_MINUS_1_25[16] WUFFS_BASE__POTENTIALLY_UNUSED = {
+  45056u, 47787u, 50517u, 53248u, 55979u, 58709u, 61440u, 64171u,
+  1365u, 4096u, 6827u, 9557u, 12288u, 15019u, 17749u, 20480u,
+};
+
+static const uint16_t
+WUFFS_THUMBHASH__COSINES[159][6] WUFFS_BASE__POTENTIALLY_UNUSED = {
+  {
+    16281u, 15973u, 15465u, 14761u, 13873u, 12810u,
+  }, {
+    15465u, 12810u, 8717u, 3646u, 63702u, 58427u,
+  }, {
+    13873u, 7109u, 63702u, 55321u, 50071u, 49563u,
+  }, {
+    11585u, 0u, 53951u, 49152u, 53951u, 0u,
+  }, {
+    8717u, 58427u, 49255u, 55321u, 5411u, 15973u,
+  }, {
+    5411u, 52726u, 51663u, 3646u, 16281u, 7109u,
+  }, {
+    1834u, 49563u, 60125u, 14761u, 8717u, 52726u,
+  }, {
+    63702u, 49563u, 5411u, 14761u, 56819u, 52726u,
+  },
+  {
+    60125u, 52726u, 13873u, 3646u, 49255u, 7109u,
+  }, {
+    56819u, 58427u, 16281u, 55321u, 60125u, 15973u,
+  }, {
+    53951u, 0u, 11585u, 49152u, 11585u, 0u,
+  }, {
+    51663u, 7109u, 1834u, 55321u, 15465u, 49563u,
+  }, {
+    50071u, 12810u, 56819u, 3646u, 1834u, 58427u,
+  }, {
+    49255u, 15973u, 50071u, 14761u, 51663u, 12810u,
+  }, {
+    16322u, 16135u, 15826u, 15396u, 14849u, 14189u,
+  }, {
+    15826u, 14189u, 11585u, 8192u, 4240u, 0u,
+  },
+  {
+    14849u, 10531u, 4240u, 62691u, 56139u, 51347u,
+  }, {
+    13421u, 5604u, 61296u, 52985u, 49214u, 51347u,
+  }, {
+    11585u, 0u, 53951u, 49152u, 53951u, 0u,
+  }, {
+    9397u, 59932u, 49710u, 52985u, 1428u, 14189u,
+  }, {
+    6924u, 55005u, 49710u, 62691u, 13421u, 14189u,
+  }, {
+    4240u, 51347u, 53951u, 8192u, 15826u, 0u,
+  }, {
+    1428u, 49401u, 61296u, 15396u, 6924u, 51347u,
+  }, {
+    64108u, 49401u, 4240u, 15396u, 58612u, 51347u,
+  },
+  {
+    61296u, 51347u, 11585u, 8192u, 49710u, 0u,
+  }, {
+    58612u, 55005u, 15826u, 62691u, 52115u, 14189u,
+  }, {
+    56139u, 59932u, 15826u, 52985u, 64108u, 14189u,
+  }, {
+    53951u, 0u, 11585u, 49152u, 11585u, 0u,
+  }, {
+    52115u, 5604u, 4240u, 52985u, 16322u, 51347u,
+  }, {
+    50687u, 10531u, 61296u, 62691u, 9397u, 51347u,
+  }, {
+    49710u, 14189u, 53951u, 8192u, 61296u, 0u,
+  }, {
+    49214u, 16135u, 49710u, 15396u, 50687u, 14189u,
+  },
+  {
+    16328u, 16161u, 15883u, 15496u, 15004u, 14409u,
+  }, {
+    15883u, 14409u, 12054u, 8961u, 5320u, 1353u,
+  }, {
+    15004u, 11097u, 5320u, 64183u, 57738u, 52607u,
+  }, {
+    13716u, 6581u, 62839u, 54439u, 49653u, 50040u,
+  }, {
+    12054u, 1353u, 55473u, 49375u, 51820u, 61514u,
+  }, {
+    10063u, 61514u, 50532u, 51127u, 62839u, 11097u,
+  }, {
+    7798u, 56575u, 49208u, 58955u, 10063u, 16161u,
+  }, {
+    5320u, 52607u, 51820u, 4022u, 16328u, 6581u,
+  },
+  {
+    2697u, 50040u, 57738u, 12929u, 12054u, 56575u,
+  }, {
+    0u, 49152u, 0u, 16384u, 0u, 49152u,
+  }, {
+    62839u, 50040u, 7798u, 12929u, 53482u, 56575u,
+  }, {
+    60216u, 52607u, 13716u, 4022u, 49208u, 6581u,
+  }, {
+    57738u, 56575u, 16328u, 58955u, 55473u, 16161u,
+  }, {
+    55473u, 61514u, 15004u, 51127u, 2697u, 11097u,
+  }, {
+    53482u, 1353u, 10063u, 49375u, 13716u, 61514u,
+  }, {
+    51820u, 6581u, 2697u, 54439u, 15883u, 50040u,
+  },
+  {
+    50532u, 11097u, 60216u, 64183u, 7798u, 52607u,
+  }, {
+    49653u, 14409u, 53482u, 8961u, 60216u, 1353u,
+  }, {
+    49208u, 16161u, 49653u, 15496u, 50532u, 14409u,
+  }, {
+    16346u, 16231u, 16041u, 15776u, 15438u, 15028u,
+  }, {
+    16041u, 15028u, 13385u, 11183u, 8513u, 5487u,
+  }, {
+    15438u, 12709u, 8513u, 3333u, 63305u, 57998u,
+  }, {
+    14547u, 9448u, 2231u, 60049u, 53562u, 49760u,
+  }, {
+    13385u, 5487u, 61116u, 52827u, 49190u, 51537u,
+  },
+  {
+    11974u, 1118u, 55196u, 49305u, 52151u, 62203u,
+  }, {
+    10340u, 62203u, 50989u, 50508u, 61116u, 9448u,
+  }, {
+    8513u, 57998u, 49190u, 56088u, 6527u, 16231u,
+  }, {
+    6527u, 54353u, 50098u, 64418u, 14547u, 12709u,
+  }, {
+    4420u, 51537u, 53562u, 7538u, 16041u, 1118u,
+  }, {
+    2231u, 49760u, 59009u, 13999u, 10340u, 54353u,
+  }, {
+    0u, 49152u, 0u, 16384u, 0u, 49152u,
+  }, {
+    63305u, 49760u, 6527u, 13999u, 55196u, 54353u,
+  },
+  {
+    61116u, 51537u, 11974u, 7538u, 49495u, 1118u,
+  }, {
+    59009u, 54353u, 15438u, 64418u, 50989u, 12709u,
+  }, {
+    57023u, 57998u, 16346u, 56088u, 59009u, 16231u,
+  }, {
+    55196u, 62203u, 14547u, 50508u, 4420u, 9448u,
+  }, {
+    53562u, 1118u, 10340u, 49305u, 13385u, 62203u,
+  }, {
+    52151u, 5487u, 4420u, 52827u, 16346u, 51537u,
+  }, {
+    50989u, 9448u, 63305u, 60049u, 11974u, 49760u,
+  }, {
+    50098u, 12709u, 57023u, 3333u, 2231u, 57998u,
+  },
+  {
+    49495u, 15028u, 52151u, 11183u, 57023u, 5487u,
+  }, {
+    49190u, 16231u, 49495u, 15776u, 50098u, 15028u,
+  }, {
+    16354u, 16265u, 16116u, 15908u, 15642u, 15319u,
+  }, {
+    16116u, 15319u, 14021u, 12264u, 10104u, 7614u,
+  }, {
+    15642u, 13484u, 10104u, 5810u, 989u, 61615u,
+  }, {
+    14941u, 10865u, 4874u, 63561u, 57060u, 52052u,
+  }, {
+    14021u, 7614u, 64547u, 56229u, 50595u, 49271u,
+  }, {
+    12897u, 3921u, 58812u, 51029u, 49420u, 54671u,
+  },
+  {
+    11585u, 0u, 53951u, 49152u, 53951u, 0u,
+  }, {
+    10104u, 61615u, 50595u, 51029u, 62583u, 10865u,
+  }, {
+    8476u, 57922u, 49182u, 56229u, 6724u, 16265u,
+  }, {
+    6724u, 54671u, 49894u, 63561u, 14021u, 13484u,
+  }, {
+    4874u, 52052u, 52639u, 5810u, 16354u, 3921u,
+  }, {
+    2953u, 50217u, 57060u, 12264u, 12897u, 57922u,
+  }, {
+    989u, 49271u, 62583u, 15908u, 4874u, 50217u,
+  }, {
+    64547u, 49271u, 2953u, 15908u, 60662u, 50217u,
+  },
+  {
+    62583u, 50217u, 8476u, 12264u, 52639u, 57922u,
+  }, {
+    60662u, 52052u, 12897u, 5810u, 49182u, 3921u,
+  }, {
+    58812u, 54671u, 15642u, 63561u, 51515u, 13484u,
+  }, {
+    57060u, 57922u, 16354u, 56229u, 58812u, 16265u,
+  }, {
+    55432u, 61615u, 14941u, 51029u, 2953u, 10865u,
+  }, {
+    53951u, 0u, 11585u, 49152u, 11585u, 0u,
+  }, {
+    52639u, 3921u, 6724u, 51029u, 16116u, 54671u,
+  }, {
+    51515u, 7614u, 989u, 56229u, 14941u, 49271u,
+  },
+  {
+    50595u, 10865u, 60662u, 63561u, 8476u, 52052u,
+  }, {
+    49894u, 13484u, 55432u, 5810u, 64547u, 61615u,
+  }, {
+    49420u, 15319u, 51515u, 12264u, 55432u, 7614u,
+  }, {
+    49182u, 16265u, 49420u, 15908u, 49894u, 15319u,
+  }, {
+    16356u, 16273u, 16135u, 15942u, 15696u, 15396u,
+  }, {
+    16135u, 15396u, 14189u, 12551u, 10531u, 8192u,
+  }, {
+    15696u, 13689u, 10531u, 6489u, 1902u, 62691u,
+  }, {
+    15044u, 11243u, 5604u, 64583u, 58183u, 52985u,
+  },
+  {
+    14189u, 8192u, 0u, 57344u, 51347u, 49152u,
+  }, {
+    13142u, 4699u, 59932u, 51847u, 49180u, 52985u,
+  }, {
+    11917u, 953u, 55005u, 49263u, 52394u, 62691u,
+  }, {
+    10531u, 62691u, 51347u, 50140u, 59932u, 8192u,
+  }, {
+    9003u, 59047u, 49401u, 54293u, 3778u, 15396u,
+  }, {
+    7353u, 55752u, 49401u, 60837u, 11917u, 15396u,
+  }, {
+    5604u, 52985u, 51347u, 2845u, 16135u, 8192u,
+  }, {
+    3778u, 50895u, 55005u, 9784u, 15044u, 62691u,
+  },
+  {
+    1902u, 49594u, 59932u, 14641u, 9003u, 52985u,
+  }, {
+    0u, 49152u, 0u, 16384u, 0u, 49152u,
+  }, {
+    63634u, 49594u, 5604u, 14641u, 56533u, 52985u,
+  }, {
+    61758u, 50895u, 10531u, 9784u, 50492u, 62691u,
+  }, {
+    59932u, 52985u, 14189u, 2845u, 49401u, 8192u,
+  }, {
+    58183u, 55752u, 16135u, 60837u, 53619u, 15396u,
+  }, {
+    56533u, 59047u, 16135u, 54293u, 61758u, 15396u,
+  }, {
+    55005u, 62691u, 14189u, 50140u, 5604u, 8192u,
+  },
+  {
+    53619u, 953u, 10531u, 49263u, 13142u, 62691u,
+  }, {
+    52394u, 4699u, 5604u, 51847u, 16356u, 52985u,
+  }, {
+    51347u, 8192u, 0u, 57344u, 14189u, 49152u,
+  }, {
+    50492u, 11243u, 59932u, 64583u, 7353u, 52985u,
+  }, {
+    49840u, 13689u, 55005u, 6489u, 63634u, 62691u,
+  }, {
+    49401u, 15396u, 51347u, 12551u, 55005u, 8192u,
+  }, {
+    49180u, 16273u, 49401u, 15942u, 49840u, 15396u,
+  }, {
+    16364u, 16305u, 16207u, 16069u, 15893u, 15679u,
+  },
+  {
+    16207u, 15679u, 14811u, 13623u, 12140u, 10394u,
+  }, {
+    15893u, 14449u, 12140u, 9102u, 5520u, 1606u,
+  }, {
+    15426u, 12665u, 8423u, 3196u, 63132u, 57813u,
+  }, {
+    14811u, 10394u, 3981u, 62340u, 55776u, 51087u,
+  }, {
+    14053u, 7723u, 64732u, 56434u, 50725u, 49231u,
+  }, {
+    13160u, 4756u, 60016u, 51913u, 49172u, 52871u,
+  }, {
+    12140u, 1606u, 55776u, 49467u, 51483u, 60780u,
+  }, {
+    11003u, 63930u, 52376u, 49467u, 57113u, 4756u,
+  },
+  {
+    9760u, 60780u, 50110u, 51913u, 64732u, 12665u,
+  }, {
+    8423u, 57813u, 49172u, 56434u, 7005u, 16305u,
+  }, {
+    7005u, 55142u, 49643u, 62340u, 13160u, 14449u,
+  }, {
+    5520u, 52871u, 51483u, 3196u, 16207u, 7723u,
+  }, {
+    3981u, 51087u, 54533u, 9102u, 15426u, 63930u,
+  }, {
+    2404u, 49857u, 58531u, 13623u, 11003u, 55142u,
+  }, {
+    804u, 49231u, 63132u, 16069u, 3981u, 49857u,
+  }, {
+    64732u, 49231u, 2404u, 16069u, 61555u, 49857u,
+  },
+  {
+    63132u, 49857u, 7005u, 13623u, 54533u, 55142u,
+  }, {
+    61555u, 51087u, 11003u, 9102u, 50110u, 63930u,
+  }, {
+    60016u, 52871u, 14053u, 3196u, 49329u, 7723u,
+  }, {
+    58531u, 55142u, 15893u, 62340u, 52376u, 14449u,
+  }, {
+    57113u, 57813u, 16364u, 56434u, 58531u, 16305u,
+  }, {
+    55776u, 60780u, 15426u, 51913u, 804u, 12665u,
+  }, {
+    54533u, 63930u, 13160u, 49467u, 8423u, 4756u,
+  }, {
+    53396u, 1606u, 9760u, 49467u, 14053u, 60780u,
+  },
+  {
+    52376u, 4756u, 5520u, 51913u, 16364u, 52871u,
+  }, {
+    51483u, 7723u, 804u, 56434u, 14811u, 49231u,
+  }, {
+    50725u, 10394u, 61555u, 62340u, 9760u, 51087u,
+  }, {
+    50110u, 12665u, 57113u, 3196u, 2404u, 57813u,
+  }, {
+    49643u, 14449u, 53396u, 9102u, 60016u, 1606u,
+  }, {
+    49329u, 15679u, 50725u, 13623u, 53396u, 10394u,
+  }, {
+    49172u, 16305u, 49329u, 16069u, 49643u, 15679u,
+  },
+};
+
 // ---------------- Private Initializer Prototypes
 
 // ---------------- Private Function Prototypes
@@ -73306,11 +73716,16 @@
 
 WUFFS_BASE__GENERATED_C_CODE
 static wuffs_base__status
-wuffs_thumbhash__decoder__from_src_to_pixels(
+wuffs_thumbhash__decoder__from_src_to_coeffs(
     wuffs_thumbhash__decoder* self,
     wuffs_base__io_buffer* a_src);
 
 WUFFS_BASE__GENERATED_C_CODE
+static wuffs_base__empty_struct
+wuffs_thumbhash__decoder__from_coeffs_to_pixels(
+    wuffs_thumbhash__decoder* self);
+
+WUFFS_BASE__GENERATED_C_CODE
 static wuffs_base__status
 wuffs_thumbhash__decoder__from_pixels_to_dst(
     wuffs_thumbhash__decoder* self,
@@ -73541,6 +73956,7 @@
   wuffs_base__status status = wuffs_base__make_status(NULL);
 
   uint32_t v_c32 = 0;
+  uint8_t v_swap = 0;
 
   const uint8_t* iop_a_src = NULL;
   const uint8_t* io0_a_src WUFFS_BASE__POTENTIALLY_UNUSED = NULL;
@@ -73594,18 +74010,115 @@
       status = wuffs_base__make_status(wuffs_thumbhash__error__bad_header);
       goto exit;
     }
+    {
+      WUFFS_BASE__COROUTINE_SUSPENSION_POINT(3);
+      uint32_t t_1;
+      if (WUFFS_BASE__LIKELY(io2_a_src - iop_a_src >= 3)) {
+        t_1 = ((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(4);
+        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_1 = ((uint32_t)(*scratch >> 56));
+          *scratch <<= 8;
+          *scratch >>= 8;
+          *scratch |= ((uint64_t)(*iop_a_src++)) << num_bits_1;
+          if (num_bits_1 == 16) {
+            t_1 = ((uint32_t)(*scratch));
+            break;
+          }
+          num_bits_1 += 8u;
+          *scratch |= ((uint64_t)(num_bits_1)) << 56;
+        }
+      }
+      v_c32 = t_1;
+    }
+    self->private_impl.f_l_dc = (((uint64_t)(((v_c32 >> 0u) & 63u))) * 136339441844224u);
+    self->private_impl.f_p_dc = ((uint64_t)((((uint64_t)(((v_c32 >> 6u) & 63u))) * 272678883688448u) - 8589384836186112u));
+    self->private_impl.f_q_dc = ((uint64_t)((((uint64_t)(((v_c32 >> 12u) & 63u))) * 272678883688448u) - 8589384836186112u));
+    self->private_impl.f_l_scale = ((uint8_t)(((v_c32 >> 18u) & 31u)));
+    self->private_impl.f_has_alpha = ((uint8_t)(((v_c32 >> 23u) & 1u)));
+    {
+      WUFFS_BASE__COROUTINE_SUSPENSION_POINT(5);
+      uint32_t t_2;
+      if (WUFFS_BASE__LIKELY(io2_a_src - iop_a_src >= 2)) {
+        t_2 = ((uint32_t)(wuffs_base__peek_u16le__no_bounds_check(iop_a_src)));
+        iop_a_src += 2;
+      } else {
+        self->private_data.s_do_decode_image_config.scratch = 0;
+        WUFFS_BASE__COROUTINE_SUSPENSION_POINT(6);
+        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_2 = ((uint32_t)(*scratch >> 56));
+          *scratch <<= 8;
+          *scratch >>= 8;
+          *scratch |= ((uint64_t)(*iop_a_src++)) << num_bits_2;
+          if (num_bits_2 == 8) {
+            t_2 = ((uint32_t)(*scratch));
+            break;
+          }
+          num_bits_2 += 8u;
+          *scratch |= ((uint64_t)(num_bits_2)) << 56;
+        }
+      }
+      v_c32 = t_2;
+    }
+    self->private_impl.f_l_count = ((uint8_t)(((v_c32 >> 0u) & 7u)));
+    self->private_impl.f_p_scale = ((uint8_t)(((v_c32 >> 3u) & 63u)));
+    self->private_impl.f_q_scale = ((uint8_t)(((v_c32 >> 9u) & 63u)));
+    self->private_impl.f_is_landscape = ((uint8_t)(((v_c32 >> 15u) & 1u)));
+    self->private_impl.f_w_dimension_code = ((uint8_t)(((uint8_t)(WUFFS_THUMBHASH__DIMENSION_CODES_FROM_HAS_ALPHA_AND_L_COUNT[self->private_impl.f_has_alpha][self->private_impl.f_l_count] >> 4u)) & 7u));
+    self->private_impl.f_h_dimension_code = ((uint8_t)(((uint8_t)(WUFFS_THUMBHASH__DIMENSION_CODES_FROM_HAS_ALPHA_AND_L_COUNT[self->private_impl.f_has_alpha][self->private_impl.f_l_count] >> 0u)) & 7u));
+    if (self->private_impl.f_is_landscape != 0u) {
+      v_swap = self->private_impl.f_w_dimension_code;
+      self->private_impl.f_w_dimension_code = self->private_impl.f_h_dimension_code;
+      self->private_impl.f_h_dimension_code = v_swap;
+    }
+    if (self->private_impl.f_is_landscape != 0u) {
+      self->private_impl.f_lx = ((uint32_t)(((uint8_t)(7u - ((uint8_t)(2u * self->private_impl.f_has_alpha))))));
+      self->private_impl.f_ly = ((uint32_t)(wuffs_base__u8__max(self->private_impl.f_l_count, 3u)));
+    } else {
+      self->private_impl.f_lx = ((uint32_t)(wuffs_base__u8__max(self->private_impl.f_l_count, 3u)));
+      self->private_impl.f_ly = ((uint32_t)(((uint8_t)(7u - ((uint8_t)(2u * self->private_impl.f_has_alpha))))));
+    }
+    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)) {
+          status = wuffs_base__make_status(wuffs_base__suspension__short_read);
+          goto suspend;
+        }
+        uint32_t t_3 = *iop_a_src++;
+        v_c32 = t_3;
+      }
+      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_pixfmt = 2415954056u;
-    self->private_impl.f_width = 32u;
-    self->private_impl.f_height = 32u;
+    if (self->private_impl.f_has_alpha != 0u) {
+      self->private_impl.f_pixfmt = 2164295816u;
+    }
     if (a_dst != NULL) {
       wuffs_base__image_config__set(
           a_dst,
           self->private_impl.f_pixfmt,
           0u,
-          self->private_impl.f_width,
-          self->private_impl.f_height,
-          3u,
-          (self->private_impl.f_pixfmt == 2415954056u));
+          ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_w_dimension_code])),
+          ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_h_dimension_code])),
+          ((uint64_t)(self->private_impl.f_frame_config_io_position)),
+          (self->private_impl.f_has_alpha == 0u));
     }
     self->private_impl.f_call_sequence = 32u;
 
@@ -73733,7 +74246,7 @@
         goto suspend;
       }
     } else if (self->private_impl.f_call_sequence == 40u) {
-      if (3u != wuffs_base__u64__sat_add((a_src ? a_src->meta.pos : 0), ((uint64_t)(iop_a_src - io0_a_src)))) {
+      if (((uint64_t)(self->private_impl.f_frame_config_io_position)) != wuffs_base__u64__sat_add((a_src ? a_src->meta.pos : 0), ((uint64_t)(iop_a_src - io0_a_src)))) {
         status = wuffs_base__make_status(wuffs_base__error__bad_restart);
         goto exit;
       }
@@ -73751,13 +74264,13 @@
           wuffs_base__utility__make_rect_ie_u32(
           0u,
           0u,
-          self->private_impl.f_width,
-          self->private_impl.f_height),
+          ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_w_dimension_code])),
+          ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_h_dimension_code]))),
           ((wuffs_base__flicks)(0u)),
           0u,
-          3u,
+          ((uint64_t)(self->private_impl.f_frame_config_io_position)),
           0u,
-          (self->private_impl.f_pixfmt == 2415954056u),
+          (self->private_impl.f_has_alpha == 0u),
           false,
           0u);
     }
@@ -73902,10 +74415,11 @@
       goto ok;
     }
     WUFFS_BASE__COROUTINE_SUSPENSION_POINT(2);
-    status = wuffs_thumbhash__decoder__from_src_to_pixels(self, a_src);
+    status = wuffs_thumbhash__decoder__from_src_to_coeffs(self, a_src);
     if (status.repr) {
       goto suspend;
     }
+    wuffs_thumbhash__decoder__from_coeffs_to_pixels(self);
     v_status = wuffs_thumbhash__decoder__from_pixels_to_dst(self, a_dst);
     if ( ! wuffs_base__status__is_ok(&v_status)) {
       status = v_status;
@@ -73933,36 +74447,333 @@
   return status;
 }
 
-// -------- func thumbhash.decoder.from_src_to_pixels
+// -------- func thumbhash.decoder.from_src_to_coeffs
 
 WUFFS_BASE__GENERATED_C_CODE
 static wuffs_base__status
-wuffs_thumbhash__decoder__from_src_to_pixels(
+wuffs_thumbhash__decoder__from_src_to_coeffs(
     wuffs_thumbhash__decoder* self,
     wuffs_base__io_buffer* a_src) {
   wuffs_base__status status = wuffs_base__make_status(NULL);
 
+  uint8_t v_c8 = 0;
+  uint32_t v_cy = 0;
+  uint32_t v_cx = 0;
+  uint32_t v_i = 0;
+  bool v_has_bits = false;
+
+  const uint8_t* iop_a_src = NULL;
+  const uint8_t* io0_a_src WUFFS_BASE__POTENTIALLY_UNUSED = NULL;
+  const uint8_t* io1_a_src WUFFS_BASE__POTENTIALLY_UNUSED = NULL;
+  const uint8_t* io2_a_src WUFFS_BASE__POTENTIALLY_UNUSED = NULL;
+  if (a_src && a_src->data.ptr) {
+    io0_a_src = a_src->data.ptr;
+    io1_a_src = io0_a_src + a_src->meta.ri;
+    iop_a_src = io1_a_src;
+    io2_a_src = io0_a_src + a_src->meta.wi;
+  }
+
+  uint32_t coro_susp_point = self->private_impl.p_from_src_to_coeffs;
+  if (coro_susp_point) {
+    v_cy = self->private_data.s_from_src_to_coeffs.v_cy;
+    v_cx = self->private_data.s_from_src_to_coeffs.v_cx;
+    v_i = self->private_data.s_from_src_to_coeffs.v_i;
+    v_has_bits = self->private_data.s_from_src_to_coeffs.v_has_bits;
+  }
+  switch (coro_susp_point) {
+    WUFFS_BASE__COROUTINE_SUSPENSION_POINT_0;
+
+    v_i = 0u;
+    v_cy = 0u;
+    while (v_cy < self->private_impl.f_ly) {
+      v_cx = 0u;
+      if (v_cy == 0u) {
+        v_cx = 1u;
+      }
+      while (((uint32_t)(v_cx * self->private_impl.f_ly)) < ((uint32_t)(self->private_impl.f_lx * ((uint32_t)(self->private_impl.f_ly - v_cy))))) {
+        if (v_has_bits) {
+          v_has_bits = false;
+#if defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+#endif
+          v_c8 >>= 4u;
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+        } else {
+          v_has_bits = true;
+          {
+            WUFFS_BASE__COROUTINE_SUSPENSION_POINT(1);
+            if (WUFFS_BASE__UNLIKELY(iop_a_src == io2_a_src)) {
+              status = wuffs_base__make_status(wuffs_base__suspension__short_read);
+              goto suspend;
+            }
+            uint8_t t_0 = *iop_a_src++;
+            v_c8 = t_0;
+          }
+        }
+        self->private_data.f_lac[(v_i & 31u)] = ((uint32_t)(((uint32_t)(((uint32_t)(self->private_impl.f_l_scale)) * 126u)) * wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__FROM_4_BITS_TO_PLUS_MINUS_1_00[((uint8_t)(v_c8 & 15u))])));
+        v_i += 1u;
+        v_cx += 1u;
+      }
+      v_cy += 1u;
+    }
+    v_i = 0u;
+    v_cx = 0u;
+    while (v_cx < 5u) {
+      if (v_has_bits) {
+        v_has_bits = false;
+#if defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+#endif
+        v_c8 >>= 4u;
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+      } else {
+        v_has_bits = true;
+        {
+          WUFFS_BASE__COROUTINE_SUSPENSION_POINT(2);
+          if (WUFFS_BASE__UNLIKELY(iop_a_src == io2_a_src)) {
+            status = wuffs_base__make_status(wuffs_base__suspension__short_read);
+            goto suspend;
+          }
+          uint8_t t_1 = *iop_a_src++;
+          v_c8 = t_1;
+        }
+      }
+      self->private_data.f_pac[(v_i & 7u)] = ((uint32_t)(((uint32_t)(((uint32_t)(self->private_impl.f_p_scale)) * 62u)) * wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__FROM_4_BITS_TO_PLUS_MINUS_1_25[((uint8_t)(v_c8 & 15u))])));
+      v_i += 1u;
+      v_cx += 1u;
+    }
+    v_i = 0u;
+    v_cx = 0u;
+    while (v_cx < 5u) {
+      if (v_has_bits) {
+        v_has_bits = false;
+#if defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+#endif
+        v_c8 >>= 4u;
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+      } else {
+        v_has_bits = true;
+        {
+          WUFFS_BASE__COROUTINE_SUSPENSION_POINT(3);
+          if (WUFFS_BASE__UNLIKELY(iop_a_src == io2_a_src)) {
+            status = wuffs_base__make_status(wuffs_base__suspension__short_read);
+            goto suspend;
+          }
+          uint8_t t_2 = *iop_a_src++;
+          v_c8 = t_2;
+        }
+      }
+      self->private_data.f_qac[(v_i & 7u)] = ((uint32_t)(((uint32_t)(((uint32_t)(self->private_impl.f_q_scale)) * 62u)) * wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__FROM_4_BITS_TO_PLUS_MINUS_1_25[((uint8_t)(v_c8 & 15u))])));
+      v_i += 1u;
+      v_cx += 1u;
+    }
+    if (self->private_impl.f_has_alpha == 0u) {
+      status = wuffs_base__make_status(NULL);
+      goto ok;
+    }
+    v_i = 0u;
+    v_cx = 0u;
+    while (v_cx < 14u) {
+      if (v_has_bits) {
+        v_has_bits = false;
+#if defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wconversion"
+#endif
+        v_c8 >>= 4u;
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+      } else {
+        v_has_bits = true;
+        {
+          WUFFS_BASE__COROUTINE_SUSPENSION_POINT(4);
+          if (WUFFS_BASE__UNLIKELY(iop_a_src == io2_a_src)) {
+            status = wuffs_base__make_status(wuffs_base__suspension__short_read);
+            goto suspend;
+          }
+          uint8_t t_3 = *iop_a_src++;
+          v_c8 = t_3;
+        }
+      }
+      self->private_data.f_aac[(v_i & 15u)] = ((uint32_t)(((uint32_t)(((uint32_t)(self->private_impl.f_a_scale)) * 2u)) * wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__FROM_4_BITS_TO_PLUS_MINUS_1_00[((uint8_t)(v_c8 & 15u))])));
+      v_i += 1u;
+      v_cx += 1u;
+    }
+
+    ok:
+    self->private_impl.p_from_src_to_coeffs = 0;
+    goto exit;
+  }
+
+  goto suspend;
+  suspend:
+  self->private_impl.p_from_src_to_coeffs = wuffs_base__status__is_suspension(&status) ? coro_susp_point : 0;
+  self->private_data.s_from_src_to_coeffs.v_cy = v_cy;
+  self->private_data.s_from_src_to_coeffs.v_cx = v_cx;
+  self->private_data.s_from_src_to_coeffs.v_i = v_i;
+  self->private_data.s_from_src_to_coeffs.v_has_bits = v_has_bits;
+
+  goto exit;
+  exit:
+  if (a_src && a_src->data.ptr) {
+    a_src->meta.ri = ((size_t)(iop_a_src - a_src->data.ptr));
+  }
+
+  return status;
+}
+
+// -------- func thumbhash.decoder.from_coeffs_to_pixels
+
+WUFFS_BASE__GENERATED_C_CODE
+static wuffs_base__empty_struct
+wuffs_thumbhash__decoder__from_coeffs_to_pixels(
+    wuffs_thumbhash__decoder* self) {
+  uint32_t v_h = 0;
+  uint32_t v_w = 0;
+  uint32_t v_fy[8] = {0};
+  uint32_t v_fx[8] = {0};
+  uint32_t v_cosines_base_y = 0;
+  uint32_t v_cosines_base_x = 0;
   uint32_t v_y = 0;
   uint32_t v_x = 0;
+  uint32_t v_f = 0;
+  uint64_t v_l = 0;
+  uint64_t v_p = 0;
+  uint64_t v_q = 0;
+  uint64_t v_b = 0;
+  uint64_t v_g = 0;
+  uint64_t v_r = 0;
+  uint64_t v_a = 0;
+  uint32_t v_i = 0;
+  uint32_t v_cy = 0;
+  uint32_t v_cx = 0;
 
+  v_h = ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_h_dimension_code]));
+  v_w = ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_w_dimension_code]));
+  v_fy[0u] = 16384u;
+  v_fx[0u] = 16384u;
+  v_a = 255u;
   v_y = 0u;
-  while (v_y < 32u) {
+  while (v_y < v_h) {
+    v_cosines_base_y = ((uint32_t)(WUFFS_THUMBHASH__CUMULATIVE_DIMENSIONS[self->private_impl.f_h_dimension_code]));
+    v_fy[1u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_y + v_y)][0u]);
+    v_fy[2u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_y + v_y)][1u]);
+    v_fy[3u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_y + v_y)][2u]);
+    v_fy[4u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_y + v_y)][3u]);
+    v_fy[5u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_y + v_y)][4u]);
+    v_fy[6u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_y + v_y)][5u]);
     v_x = 0u;
-    while (v_x < 32u) {
-      self->private_data.f_pixels[v_y][((4u * v_x) + 0u)] = ((uint8_t)((v_x * 8u)));
-      self->private_data.f_pixels[v_y][((4u * v_x) + 1u)] = ((uint8_t)((v_y * 8u)));
-      self->private_data.f_pixels[v_y][((4u * v_x) + 2u)] = 0u;
-      self->private_data.f_pixels[v_y][((4u * v_x) + 3u)] = 255u;
+    while (v_x < v_w) {
+      v_cosines_base_x = ((uint32_t)(WUFFS_THUMBHASH__CUMULATIVE_DIMENSIONS[self->private_impl.f_w_dimension_code]));
+      v_fx[1u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_x + v_x)][0u]);
+      v_fx[2u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_x + v_x)][1u]);
+      v_fx[3u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_x + v_x)][2u]);
+      v_fx[4u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_x + v_x)][3u]);
+      v_fx[5u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_x + v_x)][4u]);
+      v_fx[6u] = wuffs_base__utility__sign_extend_convert_u16_u32(WUFFS_THUMBHASH__COSINES[(v_cosines_base_x + v_x)][5u]);
+      v_l = self->private_impl.f_l_dc;
+      v_i = 0u;
+      v_cy = 0u;
+      while (v_cy < self->private_impl.f_ly) {
+        v_cx = 0u;
+        if (v_cy == 0u) {
+          v_cx = 1u;
+        }
+        while (((uint32_t)(v_cx * self->private_impl.f_ly)) < ((uint32_t)(self->private_impl.f_lx * ((uint32_t)(self->private_impl.f_ly - v_cy))))) {
+          v_f = ((uint32_t)(v_fx[(v_cx & 7u)] * v_fy[(v_cy & 7u)]));
+          v_l += ((uint64_t)(wuffs_base__utility__sign_extend_convert_u32_u64(v_f) * wuffs_base__utility__sign_extend_convert_u32_u64(self->private_data.f_lac[(v_i & 31u)])));
+          v_i += 1u;
+          v_cx += 1u;
+        }
+        v_cy += 1u;
+      }
+      v_p = self->private_impl.f_p_dc;
+      v_q = self->private_impl.f_q_dc;
+      v_i = 0u;
+      v_cy = 0u;
+      while (v_cy < 3u) {
+        v_cx = 0u;
+        if (v_cy == 0u) {
+          v_cx = 1u;
+        }
+        while (v_cx < (3u - v_cy)) {
+          v_f = ((uint32_t)(v_fx[v_cx] * v_fy[v_cy]));
+          v_p += ((uint64_t)(wuffs_base__utility__sign_extend_convert_u32_u64(v_f) * wuffs_base__utility__sign_extend_convert_u32_u64(self->private_data.f_pac[(v_i & 7u)])));
+          v_q += ((uint64_t)(wuffs_base__utility__sign_extend_convert_u32_u64(v_f) * wuffs_base__utility__sign_extend_convert_u32_u64(self->private_data.f_qac[(v_i & 7u)])));
+          v_i += 1u;
+          v_cx += 1u;
+        }
+        v_cy += 1u;
+      }
+      v_b = ((uint64_t)(v_l - wuffs_base__utility__i64_divide(((uint64_t)(2u * v_p)), 3u)));
+      v_r = wuffs_base__utility__sign_extend_rshift_u64(((uint64_t)(((uint64_t)(((uint64_t)(3u * v_l)) + v_q)) - v_b)), 1u);
+      v_g = ((uint64_t)(v_r - v_q));
+      if ((v_b >> 63u) != 0u) {
+        v_b = 0u;
+      } else if (v_b >= 8589384836185950u) {
+        v_b = 255u;
+      } else {
+        v_b /= 33683862102690u;
+      }
+      if ((v_g >> 63u) != 0u) {
+        v_g = 0u;
+      } else if (v_g >= 8589384836185950u) {
+        v_g = 255u;
+      } else {
+        v_g /= 33683862102690u;
+      }
+      if ((v_r >> 63u) != 0u) {
+        v_r = 0u;
+      } else if (v_r >= 8589384836185950u) {
+        v_r = 255u;
+      } else {
+        v_r /= 33683862102690u;
+      }
+      if (self->private_impl.f_has_alpha != 0u) {
+        v_a = self->private_impl.f_a_dc;
+        v_i = 0u;
+        v_cy = 0u;
+        while (v_cy < 5u) {
+          v_cx = 0u;
+          if (v_cy == 0u) {
+            v_cx = 1u;
+          }
+          while (v_cx < (5u - v_cy)) {
+            v_f = ((uint32_t)(v_fx[v_cx] * v_fy[v_cy]));
+            v_a += ((uint64_t)(wuffs_base__utility__sign_extend_convert_u32_u64(v_f) * wuffs_base__utility__sign_extend_convert_u32_u64(self->private_data.f_aac[(v_i & 15u)])));
+            v_i += 1u;
+            v_cx += 1u;
+          }
+          v_cy += 1u;
+        }
+        if ((v_a >> 63u) != 0u) {
+          v_a = 0u;
+        } else if (v_a >= 65970697666500u) {
+          v_a = 255u;
+        } else {
+          v_a /= 258708618300u;
+        }
+      }
+      self->private_data.f_pixels[v_y][((4u * v_x) + 0u)] = ((uint8_t)(v_b));
+      self->private_data.f_pixels[v_y][((4u * v_x) + 1u)] = ((uint8_t)(v_g));
+      self->private_data.f_pixels[v_y][((4u * v_x) + 2u)] = ((uint8_t)(v_r));
+      self->private_data.f_pixels[v_y][((4u * v_x) + 3u)] = ((uint8_t)(v_a));
       v_x += 1u;
     }
     v_y += 1u;
   }
-
-  goto ok;
-  ok:
-  goto exit;
-  exit:
-  return status;
+  return wuffs_base__make_empty_struct();
 }
 
 // -------- func thumbhash.decoder.from_pixels_to_dst
@@ -73972,6 +74783,8 @@
 wuffs_thumbhash__decoder__from_pixels_to_dst(
     wuffs_thumbhash__decoder* self,
     wuffs_base__pixel_buffer* a_dst) {
+  uint32_t v_h = 0;
+  uint32_t v_w = 0;
   wuffs_base__pixel_format v_dst_pixfmt = {0};
   uint32_t v_dst_bits_per_pixel = 0;
   uint32_t v_dst_bytes_per_pixel = 0;
@@ -73981,16 +74794,18 @@
   wuffs_base__slice_u8 v_dst = {0};
   wuffs_base__slice_u8 v_src = {0};
 
+  v_h = ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_h_dimension_code]));
+  v_w = ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_w_dimension_code]));
   v_dst_pixfmt = wuffs_base__pixel_buffer__pixel_format(a_dst);
   v_dst_bits_per_pixel = wuffs_base__pixel_format__bits_per_pixel(&v_dst_pixfmt);
   if ((v_dst_bits_per_pixel & 7u) != 0u) {
     return wuffs_base__make_status(wuffs_base__error__unsupported_option);
   }
   v_dst_bytes_per_pixel = (v_dst_bits_per_pixel / 8u);
-  v_dst_bytes_per_row = ((uint64_t)((self->private_impl.f_width * v_dst_bytes_per_pixel)));
+  v_dst_bytes_per_row = ((uint64_t)((v_w * v_dst_bytes_per_pixel)));
   v_tab = wuffs_base__pixel_buffer__plane(a_dst, 0u);
-  while (v_y < self->private_impl.f_height) {
-    v_src = wuffs_base__make_slice_u8(self->private_data.f_pixels[v_y], (self->private_impl.f_width * 4u));
+  while (v_y < v_h) {
+    v_src = wuffs_base__make_slice_u8(self->private_data.f_pixels[v_y], (v_w * 4u));
     v_dst = wuffs_private_impl__table_u8__row_u32(v_tab, v_y);
     if (v_dst_bytes_per_row < ((uint64_t)(v_dst.len))) {
       v_dst = wuffs_base__slice_u8__subslice_j(v_dst, v_dst_bytes_per_row);
@@ -74018,8 +74833,8 @@
   return wuffs_base__utility__make_rect_ie_u32(
       0u,
       0u,
-      self->private_impl.f_width,
-      self->private_impl.f_height);
+      ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_w_dimension_code])),
+      ((uint32_t)(WUFFS_THUMBHASH__DIMENSIONS_FROM_DIMENSION_CODES[self->private_impl.f_h_dimension_code])));
 }
 
 // -------- func thumbhash.decoder.num_animation_loops
@@ -74100,7 +74915,7 @@
   if (self->private_impl.f_call_sequence < 32u) {
     return wuffs_base__make_status(wuffs_base__error__bad_call_sequence);
   }
-  if ((a_index != 0u) || (a_io_position != 3u)) {
+  if ((a_index != 0u) || (a_io_position != ((uint64_t)(self->private_impl.f_frame_config_io_position)))) {
     return wuffs_base__make_status(wuffs_base__error__bad_argument);
   }
   self->private_impl.f_call_sequence = 40u;
diff --git a/script/print-thumbhash-tables.go b/script/print-thumbhash-tables.go
new file mode 100644
index 0000000..1976334
--- /dev/null
+++ b/script/print-thumbhash-tables.go
@@ -0,0 +1,144 @@
+// 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
+
+//go:build ignore
+// +build ignore
+
+package main
+
+// print-thumbhash-tables.go prints the std/thumbhash tables.
+//
+// Usage: go run print-thumbhash-tables.go
+
+import (
+	"fmt"
+	"math"
+	"os"
+)
+
+func main() {
+	if err := main1(); err != nil {
+		os.Stderr.WriteString(err.Error() + "\n")
+		os.Exit(1)
+	}
+}
+
+func main1() error {
+	printLxLy()
+	fmt.Println()
+	printFrom4Bits(1.00)
+	fmt.Println()
+	printFrom4Bits(1.25)
+	fmt.Println()
+	printCosines()
+	return nil
+}
+
+func printLxLy() {
+	for bits := 0; bits < 16; bits++ {
+		lCount := bits & 7
+		if lCount < 3 {
+			continue
+		} else if lCount == 3 {
+			fmt.Println()
+		}
+		hasAlpha := (bits >> 3) & 1
+		isLandscape := (bits >> 4) & 1
+
+		lx := lCount
+		ly := 7
+		if hasAlpha != 0 {
+			ly = 5
+		}
+		if isLandscape != 0 {
+			lx, ly = ly, lx
+		}
+		ratio := float64(lx) / float64(ly)
+
+		w, h := 32, 32
+		if ratio > 1 {
+			h = int(math.Round(32 / ratio))
+		} else {
+			w = int(math.Round(32 * ratio))
+		}
+
+		lenlac := 0
+		for cy := 0; cy < ly; cy++ {
+			for cx := ifElse(cy, 0, 1); (cx * ly) < (lx * (ly - cy)); cx++ {
+				lenlac++
+			}
+		}
+
+		lenpac := 0
+		for cy := 0; cy < 3; cy++ {
+			for cx := ifElse(cy, 0, 1); (cx * 3) < (3 * (3 - cy)); cx++ {
+				lenpac++
+			}
+		}
+
+		lenaac := 0
+		if hasAlpha != 0 {
+			for cy := 0; cy < 5; cy++ {
+				for cx := ifElse(cy, 0, 1); (cx * 5) < (5 * (5 - cy)); cx++ {
+					lenaac++
+				}
+			}
+		}
+
+		fileSize := 5 + hasAlpha + (lenlac+lenpac+lenpac+lenaac+1)/2
+
+		fmt.Printf("l_count: %d   lx / ly = %d / %d   w = %2d   h = %2d   #lac = %2d   #pac = #qac = %d   #aac = %2d   file_size = 3 + %2d\n",
+			lCount, lx, ly, w, h, lenlac, lenpac, lenaac, fileSize)
+	}
+}
+
+func printFrom4Bits(scale float64) {
+	for i := 0; i < 16; i++ {
+		x := (float64(i)/7.5 - 1) * scale
+		y := (x * (1 << 14))
+		fmt.Printf("0x%04X,  // %s%0.3f\n", 0xFFFF&int(math.Round(y)), plusSign(y), x)
+	}
+}
+
+func printCosines() {
+	widths := []int{
+		0, 14, 18, 19, 23, 26, 27, 32,
+	}
+
+	for _, w := range widths {
+		fmt.Printf("\n// w = %2d\n", w)
+		for x := 0; x < w; x++ {
+			fmt.Printf("[")
+			for cx := 1; cx < 7; cx++ {
+				u := math.Cos(math.Pi * float64(cx) * (float64(x) + 0.5) / float64(w))
+				v := u * (1 << 14)
+				fmt.Printf("0x%04X", 0xFFFF&int(math.Round(v)))
+				if cx < 6 {
+					fmt.Printf(",")
+				}
+			}
+			fmt.Printf("],  // x = %2d\n", x)
+		}
+	}
+}
+
+func ifElse(c int, x int, y int) int {
+	if c != 0 {
+		return x
+	}
+	return y
+}
+
+func plusSign(x float64) string {
+	if x >= 0 {
+		return "+"
+	}
+	return ""
+}
diff --git a/std/thumbhash/decode_thumbhash.wuffs b/std/thumbhash/decode_thumbhash.wuffs
index 72be5fc..7556ef6 100644
--- a/std/thumbhash/decode_thumbhash.wuffs
+++ b/std/thumbhash/decode_thumbhash.wuffs
@@ -33,17 +33,154 @@
 pub const DECODER_WORKBUF_LEN_MAX_INCL_WORST_CASE : base.u64 = 0
 
 pub struct decoder? implements base.image_decoder(
-        pixfmt : base.u32,
-        width  : base.u32[..= 32],
-        height : base.u32[..= 32],
+        pixfmt           : base.u32,
+        w_dimension_code : base.u8[..= 7],
+        h_dimension_code : base.u8[..= 7],
 
         // The call sequence state machine is discussed in
         // (/doc/std/image-decoders-call-sequence.md).
         call_sequence : base.u8,
 
+        frame_config_io_position : base.u8[..= 9],
+
+        // The L, P and Q DC values occupy 6 bits in the file format.
+        // Conceptually, they range from 0 to 1 (for L) or from -1 to +1 (for P
+        // and Q). That's what values are calculated in the original JavaScript
+        // reference implementation, which uses floating point.
+        //
+        // This implementation uses fixed point. The fx and fy cosine tables
+        // and the FROM_4_BITS_TO_PLUS_MINUS_ETC tables are all stored as 2.14
+        // signed fixed point (represented in this code as a base.u16, since
+        // Wuffs as of 2024 only speaks unsigned integers). Multiplying two of
+        // those uses 4.28 (as a base.u32). Multiplying three of those uses
+        // 22.42 (as a base.u64).
+        //
+        // We eventually combine L, P and Q values so we need them to use the
+        // same denominator. l_scale is out of 31 and p_scale and q_scale are
+        // out of 63, so the common denominator is ((63 * 31) << 42) =
+        // 8589_384836_186112 = 0x001E_8400_0000_0000.
+        //
+        // The A channel is not combined with L, P or Q and a_scale is out of
+        // 15, so we can keep the a_dc denominator at (15 << 42).
+        //
+        // Let LDENOM = ((63 * 31) << 14) = 0x01E8_4000.
+        //
+        // Let ADENOM = ( 15       << 14) = 0x0003_C000.
+        l_dc : base.u64,  // Fixed-point denominator is (LDENOM << 28).
+        p_dc : base.u64,  // Fixed-point denominator is (LDENOM << 28).
+        q_dc : base.u64,  // Fixed-point denominator is (LDENOM << 28).
+        a_dc : base.u64,  // Fixed-point denominator is (ADENOM << 28).
+
+        l_scale : base.u8[..= 31],
+        p_scale : base.u8[..= 63],
+        q_scale : base.u8[..= 63],
+        a_scale : base.u8[..= 15],
+
+        // The first three fields determine lx and ly: the exclusive max of the
+        // (cx, cy) pairs indexing two-dimensional AC coefficients. The (0, 0)
+        // pair is the DC coefficient. (lx / ly) is also the image's aspect
+        // ratio, after rounding width and height to a whole number of pixels
+        // such that the longest dimension is 32.
+        //
+        // Let lminor and lmajor alias lx and ly if is_landscape is 0 (false).
+        // If is_landscape is 1 (true) then these are swapped.
+        //
+        // lminor equals max(3, l_count). lmajor equals 7 or 5, depending on
+        // whether has_alpha is 0 or 1. For example, if has_alpha = 0, l_count
+        // = 5 and is_landscape = 1 then lx = 7 and ly = 5, so that width = 32
+        // and height = 23.
+        //
+        // lx and ly then detemine the number of L AC coefficients (the number
+        // of lac elements that are used), ranging in 10 ..= 27. The number for
+        // P and Q is always 5 and the number for A is 0 or 14, depending on
+        // has_alpha. Total file size is an optional 3-byte magic identifier
+        // plus (5 + has_alpha) bytes of header plus 4 bits per AC coefficient.
+        // As a table:
+        //
+        // When has_alpha = 0 and is_landscape = 0:
+        // l_count: 3   lx / ly = 3 / 7   w = 14   h = 32   #lac = 14   #pac = #qac = 5   #aac =  0   file_size = 3 + 17
+        // l_count: 4   lx / ly = 4 / 7   w = 18   h = 32   #lac = 18   #pac = #qac = 5   #aac =  0   file_size = 3 + 19
+        // l_count: 5   lx / ly = 5 / 7   w = 23   h = 32   #lac = 22   #pac = #qac = 5   #aac =  0   file_size = 3 + 21
+        // l_count: 6   lx / ly = 6 / 7   w = 27   h = 32   #lac = 26   #pac = #qac = 5   #aac =  0   file_size = 3 + 23
+        // l_count: 7   lx / ly = 7 / 7   w = 32   h = 32   #lac = 27   #pac = #qac = 5   #aac =  0   file_size = 3 + 24
+        //
+        // When has_alpha = 1 and is_landscape = 0:
+        // l_count: 3   lx / ly = 3 / 5   w = 19   h = 32   #lac = 10   #pac = #qac = 5   #aac = 14   file_size = 3 + 23
+        // l_count: 4   lx / ly = 4 / 5   w = 26   h = 32   #lac = 13   #pac = #qac = 5   #aac = 14   file_size = 3 + 25
+        // l_count: 5   lx / ly = 5 / 5   w = 32   h = 32   #lac = 14   #pac = #qac = 5   #aac = 14   file_size = 3 + 25
+        // l_count: 6   lx / ly = 6 / 5   w = 32   h = 27   #lac = 19   #pac = #qac = 5   #aac = 14   file_size = 3 + 28
+        // l_count: 7   lx / ly = 7 / 5   w = 32   h = 23   #lac = 22   #pac = #qac = 5   #aac = 14   file_size = 3 + 29
+        //
+        // A "(cx * ly) < (lx * (ly - cy))" constraint determines which L AC
+        // coefficients are used. Here are some visualizations, where 'D' is
+        // the DC coefficient. When has_alpha = 0:
+        //  - the       14  '3' coefficients are always used,
+        //  - the (18 - 14) '4' coefficients are also used when (l_count >= 4),
+        //  - the (22 - 18) '5' coefficients are also used when (l_count >= 5),
+        //  - the (26 - 22) '6' coefficients are also used when (l_count >= 6),
+        //  - the (27 - 26) '7' coefficients are also used when (l_count >= 7),
+        //
+        // When has_alpha = 0:
+        // D334567
+        // 333456
+        // 33356
+        // 3346
+        // 335
+        // 34
+        // 3
+        //
+        // When has_alpha = 1:
+        // D334567
+        // 333467
+        // 33467
+        // 336
+        // 36
+        //
+        // The same visualization for P and Q:
+        // D33
+        // 33
+        // 3
+        //
+        // The same visualization for A (when has_alpha = 1):
+        // D3333
+        // 3333
+        // 333
+        // 33
+        // 3
+        has_alpha    : base.u8[..= 1],
+        l_count      : base.u8[..= 7],
+        is_landscape : base.u8[..= 1],
+        lx           : base.u32[..= 7],
+        ly           : base.u32[..= 7],
+
         swizzler : base.pixel_swizzler,
         util     : base.utility,
 ) + (
+        // AC coefficients, doubled (†) when stored here to simplify the later
+        // (fx[cx] * fy[cy]) calculation in from_coeffs_to_pixels.
+        //
+        // Before doubling, these conceptually range from -1 to +1 (for L and
+        // A) or from -1.25 to +1.25 (for P and Q, boosted "to compensate for
+        // quantization" in the original JavaScript reference implementation).
+        //
+        // These AC values will eventually be multiplied by fx[cx] and fy[cy],
+        // both of which are 2.14 signed fixed point, so these fixed point
+        // values use the DC value denominators right-shifted by 28: LDENOM =
+        // ((63 * 31) << 14) for L, P and Q and ADENOM = (15 << 14) for A.
+        //
+        // For example, if lac[10] was 23998464 as a base.u32 here in Wuffs
+        // code, the corresponding undoubled floating point value in JavaScript
+        // code would be (23998464.0 / (2 * LDENOM)) = 0.375.
+        //
+        // Not every element is used, only the first up-to-27 (lac), 5 (pac and
+        // qac) or 14 (aac). But we round up the array sizes up to a power of 2
+        // (and bitwise-and the array indexes) to simplify bounds checking.
+        lac : array[32] base.u32,  // Fixed-point denominator is LDENOM.
+        pac : array[8] base.u32,  //  Fixed-point denominator is LDENOM.
+        qac : array[8] base.u32,  //  Fixed-point denominator is LDENOM.
+        aac : array[16] base.u32,  // Fixed-point denominator is ADENOM.
+
+        // 32 rows, 32 BGRA pixels per row.
         pixels : array[32] array[128] base.u8,
 )
 
@@ -68,7 +205,8 @@
 }
 
 pri func decoder.do_decode_image_config?(dst: nptr base.image_config, src: base.io_reader) {
-    var c32 : base.u32
+    var c32  : base.u32
+    var swap : base.u8[..= 7]
 
     if this.call_sequence <> 0x00 {
         return base."#bad call sequence"
@@ -80,18 +218,55 @@
         return "#bad header"
     }
 
+    c32 = args.src.read_u24le_as_u32?()
+    this.l_dc = (((c32 >> 0x00) & 63) as base.u64) * (31 << 42)
+    this.p_dc = ((((c32 >> 0x06) & 63) as base.u64) * (31 << 43)) ~mod- ((63 * 31) << 42)
+    this.q_dc = ((((c32 >> 0x0C) & 63) as base.u64) * (31 << 43)) ~mod- ((63 * 31) << 42)
+    this.l_scale = ((c32 >> 0x12) & 31) as base.u8
+    this.has_alpha = ((c32 >> 0x17) & 1) as base.u8
+
+    c32 = args.src.read_u16le_as_u32?()
+    this.l_count = ((c32 >> 0x00) & 7) as base.u8
+    this.p_scale = ((c32 >> 0x03) & 63) as base.u8
+    this.q_scale = ((c32 >> 0x09) & 63) as base.u8
+    this.is_landscape = ((c32 >> 0x0F) & 1) as base.u8
+    this.w_dimension_code = (DIMENSION_CODES_FROM_HAS_ALPHA_AND_L_COUNT[this.has_alpha][this.l_count] >> 4) & 7
+    this.h_dimension_code = (DIMENSION_CODES_FROM_HAS_ALPHA_AND_L_COUNT[this.has_alpha][this.l_count] >> 0) & 7
+    if this.is_landscape <> 0 {
+        swap = this.w_dimension_code
+        this.w_dimension_code = this.h_dimension_code
+        this.h_dimension_code = swap
+    }
+
+    if this.is_landscape <> 0 {
+        this.lx = (7 - (2 * this.has_alpha)) as base.u32
+        this.ly = (this.l_count.max(no_less_than: 3)) as base.u32
+    } else {
+        this.lx = (this.l_count.max(no_less_than: 3)) as base.u32
+        this.ly = (7 - (2 * this.has_alpha)) as base.u32
+    }
+
+    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.pixfmt = base.PIXEL_FORMAT__BGRX
-    this.width = 32
-    this.height = 32
+    if this.has_alpha <> 0 {
+        this.pixfmt = base.PIXEL_FORMAT__BGRA_NONPREMUL
+    }
 
     if args.dst <> nullptr {
         args.dst.set!(
                 pixfmt: this.pixfmt,
                 pixsub: 0,
-                width: this.width,
-                height: this.height,
-                first_frame_io_position: 3,
-                first_frame_is_opaque: this.pixfmt == base.PIXEL_FORMAT__BGRX)
+                width: DIMENSIONS_FROM_DIMENSION_CODES[this.w_dimension_code] as base.u32,
+                height: DIMENSIONS_FROM_DIMENSION_CODES[this.h_dimension_code] as base.u32,
+                first_frame_io_position: this.frame_config_io_position as base.u64,
+                first_frame_is_opaque: this.has_alpha == 0)
     }
 
     this.call_sequence = 0x20
@@ -115,7 +290,7 @@
     } else if this.call_sequence < 0x20 {
         this.do_decode_image_config?(dst: nullptr, src: args.src)
     } else if this.call_sequence == 0x28 {
-        if 3 <> args.src.position() {
+        if (this.frame_config_io_position as base.u64) <> args.src.position() {
             return base."#bad restart"
         }
     } else if this.call_sequence == 0x40 {
@@ -129,13 +304,13 @@
         args.dst.set!(bounds: this.util.make_rect_ie_u32(
                 min_incl_x: 0,
                 min_incl_y: 0,
-                max_excl_x: this.width,
-                max_excl_y: this.height),
+                max_excl_x: DIMENSIONS_FROM_DIMENSION_CODES[this.w_dimension_code] as base.u32,
+                max_excl_y: DIMENSIONS_FROM_DIMENSION_CODES[this.h_dimension_code] as base.u32),
                 duration: 0,
                 index: 0,
-                io_position: 3,
+                io_position: this.frame_config_io_position as base.u64,
                 disposal: 0,
-                opaque_within_bounds: this.pixfmt == base.PIXEL_FORMAT__BGRX,
+                opaque_within_bounds: this.has_alpha == 0,
                 overwrite_instead_of_blend: false,
                 background_color: 0x0000_0000)
     }
@@ -176,7 +351,8 @@
         return status
     }
 
-    this.from_src_to_pixels?(src: args.src)
+    this.from_src_to_coeffs?(src: args.src)
+    this.from_coeffs_to_pixels!()
     status = this.from_pixels_to_dst!(dst: args.dst)
     if not status.is_ok() {
         return status
@@ -185,23 +361,304 @@
     this.call_sequence = 0x60
 }
 
-pri func decoder.from_src_to_pixels?(src: base.io_reader) {
+pri func decoder.from_src_to_coeffs?(src: base.io_reader) {
+    var c8       : base.u8
+    var cy       : base.u32
+    var cx       : base.u32
+    var i        : base.u32
+    var has_bits : base.bool
+
+    // Read L AC coefficients.
+    i = 0
+    cy = 0
+    while cy < this.ly {
+        cx = 0
+        if cy == 0 {
+            cx = 1
+        }
+        while (cx ~mod* this.ly) < (this.lx ~mod* (this.ly ~mod- cy)) {
+            if has_bits {
+                has_bits = false
+                c8 >>= 4
+            } else {
+                has_bits = true
+                c8 = args.src.read_u8?()
+            }
+
+            // Multiply by (63 * 2) to double (†) and get to LDENOM.
+            this.lac[i & 31] = ((this.l_scale as base.u32) ~mod* 126) ~mod*
+                    this.util.sign_extend_convert_u16_u32(a: FROM_4_BITS_TO_PLUS_MINUS_1_00[c8 & 15])
+
+            i ~mod+= 1
+            cx ~mod+= 1
+        }
+        cy ~mod+= 1
+    }
+
+    // Read P AC coefficients.
+    i = 0
+    cx = 0
+    while cx < 5 {
+        if has_bits {
+            has_bits = false
+            c8 >>= 4
+        } else {
+            has_bits = true
+            c8 = args.src.read_u8?()
+        }
+
+        // Multiply by (31 * 2) to double (†) and get to LDENOM.
+        this.pac[i & 7] = ((this.p_scale as base.u32) ~mod* 62) ~mod*
+                this.util.sign_extend_convert_u16_u32(a: FROM_4_BITS_TO_PLUS_MINUS_1_25[c8 & 15])
+
+        i ~mod+= 1
+        cx ~mod+= 1
+    }
+
+    // Read Q AC coefficients.
+    i = 0
+    cx = 0
+    while cx < 5 {
+        if has_bits {
+            has_bits = false
+            c8 >>= 4
+        } else {
+            has_bits = true
+            c8 = args.src.read_u8?()
+        }
+
+        // Multiply by (31 * 2) to double (†) and get to LDENOM.
+        this.qac[i & 7] = ((this.q_scale as base.u32) ~mod* 62) ~mod*
+                this.util.sign_extend_convert_u16_u32(a: FROM_4_BITS_TO_PLUS_MINUS_1_25[c8 & 15])
+
+        i ~mod+= 1
+        cx ~mod+= 1
+    }
+
+    // Read A AC coefficients.
+    if this.has_alpha == 0 {
+        return ok
+    }
+    i = 0
+    cx = 0
+    while cx < 14 {
+        if has_bits {
+            has_bits = false
+            c8 >>= 4
+        } else {
+            has_bits = true
+            c8 = args.src.read_u8?()
+        }
+
+        // Multiply by 2 to double (†). We're already at ADENOM.
+        this.aac[i & 15] = ((this.a_scale as base.u32) ~mod* 2) ~mod*
+                this.util.sign_extend_convert_u16_u32(a: FROM_4_BITS_TO_PLUS_MINUS_1_00[c8 & 15])
+
+        i ~mod+= 1
+        cx ~mod+= 1
+    }
+}
+
+pri func decoder.from_coeffs_to_pixels!() {
+    var h : base.u32[..= 32]
+    var w : base.u32[..= 32]
+
+    // fx[7] and fy[7] are unused but we round up the array sizes up to a power
+    // of 2 (and bitwise-and the array indexes) to simplify bounds checking.
+    var fy : array[8] base.u32
+    var fx : array[8] base.u32
+
+    var cosines_base_y : base.u32[..= 127]
+    var cosines_base_x : base.u32[..= 127]
+
     var y : base.u32
     var x : base.u32
 
-    // TODO: actually implement the format. For now, fill in a placeholder
-    // blue-green gradient.
+    var f : base.u32
+    var l : base.u64
+    var p : base.u64
+    var q : base.u64
+    var b : base.u64
+    var g : base.u64
+    var r : base.u64
+    var a : base.u64
+
+    var i  : base.u32
+    var cy : base.u32
+    var cx : base.u32
+
+    h = DIMENSIONS_FROM_DIMENSION_CODES[this.h_dimension_code] as base.u32
+    w = DIMENSIONS_FROM_DIMENSION_CODES[this.w_dimension_code] as base.u32
+    fy[0] = 0x4000  // This is always cos(0), scaled by 0x4000 = (1 << 14).
+    fx[0] = 0x4000  // This is always cos(0), scaled by 0x4000 = (1 << 14).
+    a = 0xFF
 
     y = 0
-    while y < 32 {
+    while y < h {
+        assert y < 32 via "a < b: a < c; c <= b"(c: h)
+
+        cosines_base_y = CUMULATIVE_DIMENSIONS[this.h_dimension_code] as base.u32
+        fy[1] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_y + y][0])
+        fy[2] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_y + y][1])
+        fy[3] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_y + y][2])
+        fy[4] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_y + y][3])
+        fy[5] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_y + y][4])
+        fy[6] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_y + y][5])
+
+        // The original JavaScript reference implementation also multiplied
+        // fy[cy] by 2 but we have already adjusted for that (†).
+
         x = 0
-        while x < 32,
+        while x < w,
                 inv y < 32,
         {
-            this.pixels[y][(4 * x) + 0] = (x * 8) as base.u8
-            this.pixels[y][(4 * x) + 1] = (y * 8) as base.u8
-            this.pixels[y][(4 * x) + 2] = 0x00
-            this.pixels[y][(4 * x) + 3] = 0xFF
+            assert x < 32 via "a < b: a < c; c <= b"(c: w)
+
+            cosines_base_x = CUMULATIVE_DIMENSIONS[this.w_dimension_code] as base.u32
+            fx[1] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_x + x][0])
+            fx[2] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_x + x][1])
+            fx[3] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_x + x][2])
+            fx[4] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_x + x][3])
+            fx[5] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_x + x][4])
+            fx[6] = this.util.sign_extend_convert_u16_u32(a: COSINES[cosines_base_x + x][5])
+
+            // Accumulate L.
+            l = this.l_dc
+            i = 0
+            cy = 0
+            while cy < this.ly,
+                    inv y < 32,
+                    inv x < 32,
+            {
+                cx = 0
+                if cy == 0 {
+                    cx = 1
+                }
+                while (cx ~mod* this.ly) < (this.lx ~mod* (this.ly ~mod- cy)),
+                        inv y < 32,
+                        inv x < 32,
+                {
+                    f = fx[cx & 7] ~mod* fy[cy & 7]
+                    l ~mod+= this.util.sign_extend_convert_u32_u64(a: f) ~mod*
+                            this.util.sign_extend_convert_u32_u64(a: this.lac[i & 31])
+                    i ~mod+= 1
+                    cx ~mod+= 1
+                }
+                cy ~mod+= 1
+            }
+
+            // Accumulate P and Q.
+            p = this.p_dc
+            q = this.q_dc
+            i = 0
+            cy = 0
+            while cy < 3,
+                    inv y < 32,
+                    inv x < 32,
+            {
+                cx = 0
+                if cy == 0 {
+                    cx = 1
+                }
+                while cx < (3 - cy),
+                        inv y < 32,
+                        inv x < 32,
+                        inv cy < 3,
+                {
+                    assert cx < 3 via "a < b: a < c; c <= b"(c: 3 - cy)
+                    f = fx[cx] ~mod* fy[cy]
+                    p ~mod+= this.util.sign_extend_convert_u32_u64(a: f) ~mod*
+                            this.util.sign_extend_convert_u32_u64(a: this.pac[i & 7])
+                    q ~mod+= this.util.sign_extend_convert_u32_u64(a: f) ~mod*
+                            this.util.sign_extend_convert_u32_u64(a: this.qac[i & 7])
+                    i ~mod+= 1
+                    cx ~mod+= 1
+                }
+                cy ~mod+= 1
+            }
+
+            // Convert LPQ to RGB. The code is the i64 equivalents of:
+            //   b = l - ((2 * p) / 3)
+            //   r = (((3 * l) + q) - b) / 2
+            //   g = r - q
+            b = l ~mod- this.util.i64_divide(a: 2 ~mod* p, b: 3)
+            r = this.util.sign_extend_rshift_u64(a: ((3 ~mod* l) ~mod+ q) ~mod- b, n: 1)
+            g = r ~mod- q
+
+            // Rescale from (LDENOM << 28) to 255 and clamp.
+            //
+            // (LDENOM << 28) = 8589_384836_186112 = (33_683862_102690 * 255) + 162
+            //
+            // Rounding down (instead of rounding to nearest) more closely
+            // matches the original JavaScript reference implementation. The
+            // two implementations aren't a 100% match, due to floating point
+            // versus fixed point rounding errors, but they're pretty close.
+            if (b >> 63) <> 0 {
+                b = 0
+            } else if b >= (33_683862_102690 * 255) {
+                b = 255
+            } else {
+                b /= 33_683862_102690
+            }
+            if (g >> 63) <> 0 {
+                g = 0
+            } else if g >= (33_683862_102690 * 255) {
+                g = 255
+            } else {
+                g /= 33_683862_102690
+            }
+            if (r >> 63) <> 0 {
+                r = 0
+            } else if r >= (33_683862_102690 * 255) {
+                r = 255
+            } else {
+                r /= 33_683862_102690
+            }
+
+            // Accumulate, rescale from (ADENOM << 28) and clamp A.
+            if this.has_alpha <> 0 {
+                a = this.a_dc
+                i = 0
+                cy = 0
+                while cy < 5,
+                        inv y < 32,
+                        inv x < 32,
+                {
+                    cx = 0
+                    if cy == 0 {
+                        cx = 1
+                    }
+                    while cx < (5 - cy),
+                            inv y < 32,
+                            inv x < 32,
+                            inv cy < 5,
+                    {
+                        assert cx < 5 via "a < b: a < c; c <= b"(c: 5 - cy)
+                        f = fx[cx] ~mod* fy[cy]
+                        a ~mod+= this.util.sign_extend_convert_u32_u64(a: f) ~mod*
+                                this.util.sign_extend_convert_u32_u64(a: this.aac[i & 15])
+                        i ~mod+= 1
+                        cx ~mod+= 1
+                    }
+                    cy ~mod+= 1
+                }
+
+                // (ADENOM << 28) = 65_970697_666560 = (258708_618300 * 255) + 60
+                if (a >> 63) <> 0 {
+                    a = 0
+                } else if a >= (258708_618300 * 255) {
+                    a = 255
+                } else {
+                    a /= 258708_618300
+                }
+            }
+
+            // Store the BGRA pixel.
+            this.pixels[y][(4 * x) + 0] = (b & 0xFF) as base.u8
+            this.pixels[y][(4 * x) + 1] = (g & 0xFF) as base.u8
+            this.pixels[y][(4 * x) + 2] = (r & 0xFF) as base.u8
+            this.pixels[y][(4 * x) + 3] = (a & 0xFF) as base.u8
+
             x += 1
         }
         y += 1
@@ -209,6 +666,9 @@
 }
 
 pri func decoder.from_pixels_to_dst!(dst: ptr base.pixel_buffer) base.status {
+    var h : base.u32[..= 32]
+    var w : base.u32[..= 32]
+
     var dst_pixfmt          : base.pixel_format
     var dst_bits_per_pixel  : base.u32[..= 256]
     var dst_bytes_per_pixel : base.u32[..= 32]
@@ -218,6 +678,9 @@
     var dst                 : slice base.u8
     var src                 : slice base.u8
 
+    h = DIMENSIONS_FROM_DIMENSION_CODES[this.h_dimension_code] as base.u32
+    w = DIMENSIONS_FROM_DIMENSION_CODES[this.w_dimension_code] as base.u32
+
     // TODO: the dst_pixfmt variable shouldn't be necessary. We should be able
     // to chain the two calls: "args.dst.pixel_format().bits_per_pixel()".
     dst_pixfmt = args.dst.pixel_format()
@@ -226,12 +689,12 @@
         return base."#unsupported option"
     }
     dst_bytes_per_pixel = dst_bits_per_pixel / 8
-    dst_bytes_per_row = (this.width * dst_bytes_per_pixel) as base.u64
+    dst_bytes_per_row = (w * dst_bytes_per_pixel) as base.u64
     tab = args.dst.plane(p: 0)
 
-    while y < this.height {
-        assert y < 32 via "a < b: a < c; c <= b"(c: this.height)
-        src = this.pixels[y][.. this.width * 4]
+    while y < h {
+        assert y < 32 via "a < b: a < c; c <= b"(c: h)
+        src = this.pixels[y][.. w * 4]
 
         dst = tab.row_u32(y: y)
         if dst_bytes_per_row < dst.length() {
@@ -253,8 +716,8 @@
     return this.util.make_rect_ie_u32(
             min_incl_x: 0,
             min_incl_y: 0,
-            max_excl_x: this.width,
-            max_excl_y: this.height)
+            max_excl_x: DIMENSIONS_FROM_DIMENSION_CODES[this.w_dimension_code] as base.u32,
+            max_excl_y: DIMENSIONS_FROM_DIMENSION_CODES[this.h_dimension_code] as base.u32)
 }
 
 pub func decoder.num_animation_loops() base.u32 {
@@ -279,7 +742,7 @@
     if this.call_sequence < 0x20 {
         return base."#bad call sequence"
     }
-    if (args.index <> 0) or (args.io_position <> 3) {
+    if (args.index <> 0) or (args.io_position <> (this.frame_config_io_position as base.u64)) {
         return base."#bad argument"
     }
     this.call_sequence = 0x28
@@ -297,3 +760,248 @@
 pub func decoder.workbuf_len() base.range_ii_u64 {
     return this.util.make_range_ii_u64(min_incl: 0, max_incl: 0)
 }
+
+// DIMENSIONS_FROM_DIMENSION_CODES enumerates all possible widths and heights.
+pri const DIMENSIONS_FROM_DIMENSION_CODES : roarray[8] base.u8[..= 32] = [
+        0, 14, 18, 19, 23, 26, 27, 32,
+]
+
+// CUMULATIVE_DIMENSIONS is the sum-table of DIMENSIONS_FROM_DIMENSION_CODES.
+pri const CUMULATIVE_DIMENSIONS : roarray[8] base.u8[..= 127] = [
+        0, 0, 14, 32, 51, 74, 100, 127,
+]
+
+// DIMENSION_CODES_FROM_HAS_ALPHA_AND_L_COUNT holds the width (high 4 bits) and
+// height (low 4 bits) codes. For example, if has_alpha is 0 and l_count is 5
+// then the 0x47 value means that the image is 23×32 (so the aspect ratio is
+// 0.71875), because:
+//  - the width  is DIMENSIONS_FROM_DIMENSION_CODES[4] = 23
+//  - the height is DIMENSIONS_FROM_DIMENSION_CODES[7] = 32
+//
+// If is_landscape is 1 (true) then the width and height are swapped.
+pri const DIMENSION_CODES_FROM_HAS_ALPHA_AND_L_COUNT : roarray[2] roarray[8] base.u8 = [
+        [0x17, 0x17, 0x17, 0x17, 0x27, 0x47, 0x67, 0x77],
+        [0x37, 0x37, 0x37, 0x37, 0x57, 0x77, 0x76, 0x74],
+]
+
+// FROM_4_BITS_TO_PLUS_MINUS_1_00 converts AC coefficients to 2.14 fixed point.
+pri const FROM_4_BITS_TO_PLUS_MINUS_1_00 : roarray[16] base.u16 = [
+        0xC000,  // -1.000
+        0xC889,  // -0.867
+        0xD111,  // -0.733
+        0xD99A,  // -0.600
+        0xE222,  // -0.467
+        0xEAAB,  // -0.333
+        0xF333,  // -0.200
+        0xFBBC,  // -0.067
+        0x0444,  // +0.067
+        0x0CCD,  // +0.200
+        0x1555,  // +0.333
+        0x1DDE,  // +0.467
+        0x2666,  // +0.600
+        0x2EEF,  // +0.733
+        0x3777,  // +0.867
+        0x4000,  // +1.000
+]
+
+// FROM_4_BITS_TO_PLUS_MINUS_1_25 converts AC coefficients to 2.14 fixed point.
+pri const FROM_4_BITS_TO_PLUS_MINUS_1_25 : roarray[16] base.u16 = [
+        0xB000,  // -1.250
+        0xBAAB,  // -1.083
+        0xC555,  // -0.917
+        0xD000,  // -0.750
+        0xDAAB,  // -0.583
+        0xE555,  // -0.417
+        0xF000,  // -0.250
+        0xFAAB,  // -0.083
+        0x0555,  // +0.083
+        0x1000,  // +0.250
+        0x1AAB,  // +0.417
+        0x2555,  // +0.583
+        0x3000,  // +0.750
+        0x3AAB,  // +0.917
+        0x4555,  // +1.083
+        0x5000,  // +1.250
+]
+
+// COSINES[etc][cx - 1] holds cos(π * cx * (x + 0.5) / w) as 2.14 fixed point.
+// Each 6-element row is copied to fx[1 .. 7] and fy[1 .. 7]. fx[0] and fy[0]
+// are always equal to 0x4000, which is cos(0) as 2.14 fixed point. fx[7] and
+// fy[7] are unused.
+//
+// (x + 0.5) ranges from (0 + 0.5) to (w - 0.5). Valid w values are given by
+// the positive elements of DIMENSIONS_FROM_DIMENSION_CODES.
+pri const COSINES : roarray[159] roarray[6] base.u16 = [
+        // w = 14
+        [0x3F99, 0x3E65, 0x3C69, 0x39A9, 0x3631, 0x320A],  // x =  0
+        [0x3C69, 0x320A, 0x220D, 0x0E3E, 0xF8D6, 0xE43B],  // x =  1
+        [0x3631, 0x1BC5, 0xF8D6, 0xD819, 0xC397, 0xC19B],  // x =  2
+        [0x2D41, 0x0000, 0xD2BF, 0xC000, 0xD2BF, 0x0000],  // x =  3
+        [0x220D, 0xE43B, 0xC067, 0xD819, 0x1523, 0x3E65],  // x =  4
+        [0x1523, 0xCDF6, 0xC9CF, 0x0E3E, 0x3F99, 0x1BC5],  // x =  5
+        [0x072A, 0xC19B, 0xEADD, 0x39A9, 0x220D, 0xCDF6],  // x =  6
+        [0xF8D6, 0xC19B, 0x1523, 0x39A9, 0xDDF3, 0xCDF6],  // x =  7
+        [0xEADD, 0xCDF6, 0x3631, 0x0E3E, 0xC067, 0x1BC5],  // x =  8
+        [0xDDF3, 0xE43B, 0x3F99, 0xD819, 0xEADD, 0x3E65],  // x =  9
+        [0xD2BF, 0x0000, 0x2D41, 0xC000, 0x2D41, 0x0000],  // x = 10
+        [0xC9CF, 0x1BC5, 0x072A, 0xD819, 0x3C69, 0xC19B],  // x = 11
+        [0xC397, 0x320A, 0xDDF3, 0x0E3E, 0x072A, 0xE43B],  // x = 12
+        [0xC067, 0x3E65, 0xC397, 0x39A9, 0xC9CF, 0x320A],  // x = 13
+
+        // w = 18
+        [0x3FC2, 0x3F07, 0x3DD2, 0x3C24, 0x3A01, 0x376D],  // x =  0
+        [0x3DD2, 0x376D, 0x2D41, 0x2000, 0x1090, 0x0000],  // x =  1
+        [0x3A01, 0x2923, 0x1090, 0xF4E3, 0xDB4B, 0xC893],  // x =  2
+        [0x346D, 0x15E4, 0xEF70, 0xCEF9, 0xC03E, 0xC893],  // x =  3
+        [0x2D41, 0x0000, 0xD2BF, 0xC000, 0xD2BF, 0x0000],  // x =  4
+        [0x24B5, 0xEA1C, 0xC22E, 0xCEF9, 0x0594, 0x376D],  // x =  5
+        [0x1B0C, 0xD6DD, 0xC22E, 0xF4E3, 0x346D, 0x376D],  // x =  6
+        [0x1090, 0xC893, 0xD2BF, 0x2000, 0x3DD2, 0x0000],  // x =  7
+        [0x0594, 0xC0F9, 0xEF70, 0x3C24, 0x1B0C, 0xC893],  // x =  8
+        [0xFA6C, 0xC0F9, 0x1090, 0x3C24, 0xE4F4, 0xC893],  // x =  9
+        [0xEF70, 0xC893, 0x2D41, 0x2000, 0xC22E, 0x0000],  // x = 10
+        [0xE4F4, 0xD6DD, 0x3DD2, 0xF4E3, 0xCB93, 0x376D],  // x = 11
+        [0xDB4B, 0xEA1C, 0x3DD2, 0xCEF9, 0xFA6C, 0x376D],  // x = 12
+        [0xD2BF, 0x0000, 0x2D41, 0xC000, 0x2D41, 0x0000],  // x = 13
+        [0xCB93, 0x15E4, 0x1090, 0xCEF9, 0x3FC2, 0xC893],  // x = 14
+        [0xC5FF, 0x2923, 0xEF70, 0xF4E3, 0x24B5, 0xC893],  // x = 15
+        [0xC22E, 0x376D, 0xD2BF, 0x2000, 0xEF70, 0x0000],  // x = 16
+        [0xC03E, 0x3F07, 0xC22E, 0x3C24, 0xC5FF, 0x376D],  // x = 17
+
+        // w = 19
+        [0x3FC8, 0x3F21, 0x3E0B, 0x3C88, 0x3A9C, 0x3849],  // x =  0
+        [0x3E0B, 0x3849, 0x2F16, 0x2301, 0x14C8, 0x0549],  // x =  1
+        [0x3A9C, 0x2B59, 0x14C8, 0xFAB7, 0xE18A, 0xCD7F],  // x =  2
+        [0x3594, 0x19B5, 0xF577, 0xD4A7, 0xC1F5, 0xC378],  // x =  3
+        [0x2F16, 0x0549, 0xD8B1, 0xC0DF, 0xCA6C, 0xF04A],  // x =  4
+        [0x274F, 0xF04A, 0xC564, 0xC7B7, 0xF577, 0x2B59],  // x =  5
+        [0x1E76, 0xDCFF, 0xC038, 0xE64B, 0x274F, 0x3F21],  // x =  6
+        [0x14C8, 0xCD7F, 0xCA6C, 0x0FB6, 0x3FC8, 0x19B5],  // x =  7
+        [0x0A89, 0xC378, 0xE18A, 0x3281, 0x2F16, 0xDCFF],  // x =  8
+        [0x0000, 0xC000, 0x0000, 0x4000, 0x0000, 0xC000],  // x =  9
+        [0xF577, 0xC378, 0x1E76, 0x3281, 0xD0EA, 0xDCFF],  // x = 10
+        [0xEB38, 0xCD7F, 0x3594, 0x0FB6, 0xC038, 0x19B5],  // x = 11
+        [0xE18A, 0xDCFF, 0x3FC8, 0xE64B, 0xD8B1, 0x3F21],  // x = 12
+        [0xD8B1, 0xF04A, 0x3A9C, 0xC7B7, 0x0A89, 0x2B59],  // x = 13
+        [0xD0EA, 0x0549, 0x274F, 0xC0DF, 0x3594, 0xF04A],  // x = 14
+        [0xCA6C, 0x19B5, 0x0A89, 0xD4A7, 0x3E0B, 0xC378],  // x = 15
+        [0xC564, 0x2B59, 0xEB38, 0xFAB7, 0x1E76, 0xCD7F],  // x = 16
+        [0xC1F5, 0x3849, 0xD0EA, 0x2301, 0xEB38, 0x0549],  // x = 17
+        [0xC038, 0x3F21, 0xC1F5, 0x3C88, 0xC564, 0x3849],  // x = 18
+
+        // w = 23
+        [0x3FDA, 0x3F67, 0x3EA9, 0x3DA0, 0x3C4E, 0x3AB4],  // x =  0
+        [0x3EA9, 0x3AB4, 0x3449, 0x2BAF, 0x2141, 0x156F],  // x =  1
+        [0x3C4E, 0x31A5, 0x2141, 0x0D05, 0xF749, 0xE28E],  // x =  2
+        [0x38D3, 0x24E8, 0x08B7, 0xEA91, 0xD13A, 0xC260],  // x =  3
+        [0x3449, 0x156F, 0xEEBC, 0xCE5B, 0xC026, 0xC951],  // x =  4
+        [0x2EC6, 0x045E, 0xD79C, 0xC099, 0xCBB7, 0xF2FB],  // x =  5
+        [0x2864, 0xF2FB, 0xC72D, 0xC54C, 0xEEBC, 0x24E8],  // x =  6
+        [0x2141, 0xE28E, 0xC026, 0xDB18, 0x197F, 0x3F67],  // x =  7
+        [0x197F, 0xD451, 0xC3B2, 0xFBA2, 0x38D3, 0x31A5],  // x =  8
+        [0x1144, 0xC951, 0xD13A, 0x1D72, 0x3EA9, 0x045E],  // x =  9
+        [0x08B7, 0xC260, 0xE681, 0x36AF, 0x2864, 0xD451],  // x = 10
+        [0x0000, 0xC000, 0x0000, 0x4000, 0x0000, 0xC000],  // x = 11
+        [0xF749, 0xC260, 0x197F, 0x36AF, 0xD79C, 0xD451],  // x = 12
+        [0xEEBC, 0xC951, 0x2EC6, 0x1D72, 0xC157, 0x045E],  // x = 13
+        [0xE681, 0xD451, 0x3C4E, 0xFBA2, 0xC72D, 0x31A5],  // x = 14
+        [0xDEBF, 0xE28E, 0x3FDA, 0xDB18, 0xE681, 0x3F67],  // x = 15
+        [0xD79C, 0xF2FB, 0x38D3, 0xC54C, 0x1144, 0x24E8],  // x = 16
+        [0xD13A, 0x045E, 0x2864, 0xC099, 0x3449, 0xF2FB],  // x = 17
+        [0xCBB7, 0x156F, 0x1144, 0xCE5B, 0x3FDA, 0xC951],  // x = 18
+        [0xC72D, 0x24E8, 0xF749, 0xEA91, 0x2EC6, 0xC260],  // x = 19
+        [0xC3B2, 0x31A5, 0xDEBF, 0x0D05, 0x08B7, 0xE28E],  // x = 20
+        [0xC157, 0x3AB4, 0xCBB7, 0x2BAF, 0xDEBF, 0x156F],  // x = 21
+        [0xC026, 0x3F67, 0xC157, 0x3DA0, 0xC3B2, 0x3AB4],  // x = 22
+
+        // w = 26
+        [0x3FE2, 0x3F89, 0x3EF4, 0x3E24, 0x3D1A, 0x3BD7],  // x =  0
+        [0x3EF4, 0x3BD7, 0x36C5, 0x2FE8, 0x2778, 0x1DBE],  // x =  1
+        [0x3D1A, 0x34AC, 0x2778, 0x16B2, 0x03DD, 0xF0AF],  // x =  2
+        [0x3A5D, 0x2A71, 0x130A, 0xF849, 0xDEE4, 0xCB54],  // x =  3
+        [0x36C5, 0x1DBE, 0xFC23, 0xDBA5, 0xC5A3, 0xC077],  // x =  4
+        [0x3261, 0x0F51, 0xE5BC, 0xC755, 0xC10C, 0xD58F],  // x =  5
+        [0x2D41, 0x0000, 0xD2BF, 0xC000, 0xD2BF, 0x0000],  // x =  6
+        [0x2778, 0xF0AF, 0xC5A3, 0xC755, 0xF477, 0x2A71],  // x =  7
+        [0x211C, 0xE242, 0xC01E, 0xDBA5, 0x1A44, 0x3F89],  // x =  8
+        [0x1A44, 0xD58F, 0xC2E6, 0xF849, 0x36C5, 0x34AC],  // x =  9
+        [0x130A, 0xCB54, 0xCD9F, 0x16B2, 0x3FE2, 0x0F51],  // x = 10
+        [0x0B89, 0xC429, 0xDEE4, 0x2FE8, 0x3261, 0xE242],  // x = 11
+        [0x03DD, 0xC077, 0xF477, 0x3E24, 0x130A, 0xC429],  // x = 12
+        [0xFC23, 0xC077, 0x0B89, 0x3E24, 0xECF6, 0xC429],  // x = 13
+        [0xF477, 0xC429, 0x211C, 0x2FE8, 0xCD9F, 0xE242],  // x = 14
+        [0xECF6, 0xCB54, 0x3261, 0x16B2, 0xC01E, 0x0F51],  // x = 15
+        [0xE5BC, 0xD58F, 0x3D1A, 0xF849, 0xC93B, 0x34AC],  // x = 16
+        [0xDEE4, 0xE242, 0x3FE2, 0xDBA5, 0xE5BC, 0x3F89],  // x = 17
+        [0xD888, 0xF0AF, 0x3A5D, 0xC755, 0x0B89, 0x2A71],  // x = 18
+        [0xD2BF, 0x0000, 0x2D41, 0xC000, 0x2D41, 0x0000],  // x = 19
+        [0xCD9F, 0x0F51, 0x1A44, 0xC755, 0x3EF4, 0xD58F],  // x = 20
+        [0xC93B, 0x1DBE, 0x03DD, 0xDBA5, 0x3A5D, 0xC077],  // x = 21
+        [0xC5A3, 0x2A71, 0xECF6, 0xF849, 0x211C, 0xCB54],  // x = 22
+        [0xC2E6, 0x34AC, 0xD888, 0x16B2, 0xFC23, 0xF0AF],  // x = 23
+        [0xC10C, 0x3BD7, 0xC93B, 0x2FE8, 0xD888, 0x1DBE],  // x = 24
+        [0xC01E, 0x3F89, 0xC10C, 0x3E24, 0xC2E6, 0x3BD7],  // x = 25
+
+        // w = 27
+        [0x3FE4, 0x3F91, 0x3F07, 0x3E46, 0x3D50, 0x3C24],  // x =  0
+        [0x3F07, 0x3C24, 0x376D, 0x3107, 0x2923, 0x2000],  // x =  1
+        [0x3D50, 0x3579, 0x2923, 0x1959, 0x076E, 0xF4E3],  // x =  2
+        [0x3AC4, 0x2BEB, 0x15E4, 0xFC47, 0xE347, 0xCEF9],  // x =  3
+        [0x376D, 0x2000, 0x0000, 0xE000, 0xC893, 0xC000],  // x =  4
+        [0x3356, 0x125B, 0xEA1C, 0xCA87, 0xC01C, 0xCEF9],  // x =  5
+        [0x2E8D, 0x03B9, 0xD6DD, 0xC06F, 0xCCAA, 0xF4E3],  // x =  6
+        [0x2923, 0xF4E3, 0xC893, 0xC3DC, 0xEA1C, 0x2000],  // x =  7
+        [0x232B, 0xE6A7, 0xC0F9, 0xD415, 0x0EC2, 0x3C24],  // x =  8
+        [0x1CB9, 0xD9C8, 0xC0F9, 0xEDA5, 0x2E8D, 0x3C24],  // x =  9
+        [0x15E4, 0xCEF9, 0xC893, 0x0B1D, 0x3F07, 0x2000],  // x = 10
+        [0x0EC2, 0xC6CF, 0xD6DD, 0x2638, 0x3AC4, 0xF4E3],  // x = 11
+        [0x076E, 0xC1BA, 0xEA1C, 0x3931, 0x232B, 0xCEF9],  // x = 12
+        [0x0000, 0xC000, 0x0000, 0x4000, 0x0000, 0xC000],  // x = 13
+        [0xF892, 0xC1BA, 0x15E4, 0x3931, 0xDCD5, 0xCEF9],  // x = 14
+        [0xF13E, 0xC6CF, 0x2923, 0x2638, 0xC53C, 0xF4E3],  // x = 15
+        [0xEA1C, 0xCEF9, 0x376D, 0x0B1D, 0xC0F9, 0x2000],  // x = 16
+        [0xE347, 0xD9C8, 0x3F07, 0xEDA5, 0xD173, 0x3C24],  // x = 17
+        [0xDCD5, 0xE6A7, 0x3F07, 0xD415, 0xF13E, 0x3C24],  // x = 18
+        [0xD6DD, 0xF4E3, 0x376D, 0xC3DC, 0x15E4, 0x2000],  // x = 19
+        [0xD173, 0x03B9, 0x2923, 0xC06F, 0x3356, 0xF4E3],  // x = 20
+        [0xCCAA, 0x125B, 0x15E4, 0xCA87, 0x3FE4, 0xCEF9],  // x = 21
+        [0xC893, 0x2000, 0x0000, 0xE000, 0x376D, 0xC000],  // x = 22
+        [0xC53C, 0x2BEB, 0xEA1C, 0xFC47, 0x1CB9, 0xCEF9],  // x = 23
+        [0xC2B0, 0x3579, 0xD6DD, 0x1959, 0xF892, 0xF4E3],  // x = 24
+        [0xC0F9, 0x3C24, 0xC893, 0x3107, 0xD6DD, 0x2000],  // x = 25
+        [0xC01C, 0x3F91, 0xC0F9, 0x3E46, 0xC2B0, 0x3C24],  // x = 26
+
+        // w = 32
+        [0x3FEC, 0x3FB1, 0x3F4F, 0x3EC5, 0x3E15, 0x3D3F],  // x =  0
+        [0x3F4F, 0x3D3F, 0x39DB, 0x3537, 0x2F6C, 0x289A],  // x =  1
+        [0x3E15, 0x3871, 0x2F6C, 0x238E, 0x1590, 0x0646],  // x =  2
+        [0x3C42, 0x3179, 0x20E7, 0x0C7C, 0xF69C, 0xE1D5],  // x =  3
+        [0x39DB, 0x289A, 0x0F8D, 0xF384, 0xD9E0, 0xC78F],  // x =  4
+        [0x36E5, 0x1E2B, 0xFCDC, 0xDC72, 0xC625, 0xC04F],  // x =  5
+        [0x3368, 0x1294, 0xEA70, 0xCAC9, 0xC014, 0xCE87],  // x =  6
+        [0x2F6C, 0x0646, 0xD9E0, 0xC13B, 0xC91B, 0xED6C],  // x =  7
+        [0x2AFB, 0xF9BA, 0xCC98, 0xC13B, 0xDF19, 0x1294],  // x =  8
+        [0x2620, 0xED6C, 0xC3BE, 0xCAC9, 0xFCDC, 0x3179],  // x =  9
+        [0x20E7, 0xE1D5, 0xC014, 0xDC72, 0x1B5D, 0x3FB1],  // x = 10
+        [0x1B5D, 0xD766, 0xC1EB, 0xF384, 0x3368, 0x3871],  // x = 11
+        [0x1590, 0xCE87, 0xC91B, 0x0C7C, 0x3F4F, 0x1E2B],  // x = 12
+        [0x0F8D, 0xC78F, 0xD505, 0x238E, 0x3C42, 0xF9BA],  // x = 13
+        [0x0964, 0xC2C1, 0xE4A3, 0x3537, 0x2AFB, 0xD766],  // x = 14
+        [0x0324, 0xC04F, 0xF69C, 0x3EC5, 0x0F8D, 0xC2C1],  // x = 15
+        [0xFCDC, 0xC04F, 0x0964, 0x3EC5, 0xF073, 0xC2C1],  // x = 16
+        [0xF69C, 0xC2C1, 0x1B5D, 0x3537, 0xD505, 0xD766],  // x = 17
+        [0xF073, 0xC78F, 0x2AFB, 0x238E, 0xC3BE, 0xF9BA],  // x = 18
+        [0xEA70, 0xCE87, 0x36E5, 0x0C7C, 0xC0B1, 0x1E2B],  // x = 19
+        [0xE4A3, 0xD766, 0x3E15, 0xF384, 0xCC98, 0x3871],  // x = 20
+        [0xDF19, 0xE1D5, 0x3FEC, 0xDC72, 0xE4A3, 0x3FB1],  // x = 21
+        [0xD9E0, 0xED6C, 0x3C42, 0xCAC9, 0x0324, 0x3179],  // x = 22
+        [0xD505, 0xF9BA, 0x3368, 0xC13B, 0x20E7, 0x1294],  // x = 23
+        [0xD094, 0x0646, 0x2620, 0xC13B, 0x36E5, 0xED6C],  // x = 24
+        [0xCC98, 0x1294, 0x1590, 0xCAC9, 0x3FEC, 0xCE87],  // x = 25
+        [0xC91B, 0x1E2B, 0x0324, 0xDC72, 0x39DB, 0xC04F],  // x = 26
+        [0xC625, 0x289A, 0xF073, 0xF384, 0x2620, 0xC78F],  // x = 27
+        [0xC3BE, 0x3179, 0xDF19, 0x0C7C, 0x0964, 0xE1D5],  // x = 28
+        [0xC1EB, 0x3871, 0xD094, 0x238E, 0xEA70, 0x0646],  // x = 29
+        [0xC0B1, 0x3D3F, 0xC625, 0x3537, 0xD094, 0x289A],  // x = 30
+        [0xC014, 0x3FB1, 0xC0B1, 0x3EC5, 0xC1EB, 0x3D3F],  // x = 31
+]
diff --git a/test/c/std/thumbhash.c b/test/c/std/thumbhash.c
index af8f484..9715dfc 100644
--- a/test/c/std/thumbhash.c
+++ b/test/c/std/thumbhash.c
@@ -76,7 +76,7 @@
   return do_test__wuffs_base__image_decoder(
       wuffs_thumbhash__decoder__upcast_as__wuffs_base__image_decoder(&dec),
       "test/data/artificial-thumbhash/3OcRJYB4d3h_iIeHeEh3eIhw-j3A.th", 0,
-      SIZE_MAX, 32, 32, 0xFF00F8F8);
+      SIZE_MAX, 32, 23, 0xFF56632E);
 }
 
 const char*  //
diff --git a/test/nia-checksums-of-data.txt b/test/nia-checksums-of-data.txt
index 88fc72d..ac32f86 100644
--- a/test/nia-checksums-of-data.txt
+++ b/test/nia-checksums-of-data.txt
@@ -26,15 +26,15 @@
 OK. 0564b364 test/data/artificial-png/apng-skip-idat.png
 OK. e08a7cc8 test/data/artificial-png/exif.png
 OK. e08a7cc8 test/data/artificial-png/key-value-pairs.png
-OK. 0508510a test/data/artificial-thumbhash/1QcSHQRnh493V4dIh4eXh1h4kJUI.th
-OK. 0508510a test/data/artificial-thumbhash/2IqDBQQnxnj0JoLYdM3f8ahpuDeHiHdwZw.th
-OK. 0508510a test/data/artificial-thumbhash/2fcZFIB3iId_h3iJh4aIYJ2V8g.th
-OK. 0508510a test/data/artificial-thumbhash/3OcRJYB4d3h_iIeHeEh3eIhw-j3A.th
-OK. 0508510a test/data/artificial-thumbhash/3PcNNYSFeXh_d3eld0iHZoZgVwh2.th
-OK. 0508510a test/data/artificial-thumbhash/HBkSHYSIeHiPiHh8eJd4eTN0EEQG.th
-OK. 0508510a test/data/artificial-thumbhash/IQgSLYZ6iHePh4h1eFeHh4dwgwg3.th
-OK. 0508510a test/data/artificial-thumbhash/VggKDYAW6lZvdYd6d2iZh_p4GE_k.th
-OK. 0508510a test/data/artificial-thumbhash/YJqGPQw7sFlslqhFafSE-Q6oJ1h2iHB2Rw.th
+OK. 2b606fe3 test/data/artificial-thumbhash/1QcSHQRnh493V4dIh4eXh1h4kJUI.th
+OK. 70203d96 test/data/artificial-thumbhash/2IqDBQQnxnj0JoLYdM3f8ahpuDeHiHdwZw.th
+OK. 025631eb test/data/artificial-thumbhash/2fcZFIB3iId_h3iJh4aIYJ2V8g.th
+OK. 4ab0d622 test/data/artificial-thumbhash/3OcRJYB4d3h_iIeHeEh3eIhw-j3A.th
+OK. 4ba04a51 test/data/artificial-thumbhash/3PcNNYSFeXh_d3eld0iHZoZgVwh2.th
+OK. 5b7fd9b7 test/data/artificial-thumbhash/HBkSHYSIeHiPiHh8eJd4eTN0EEQG.th
+OK. 69ad2c3d test/data/artificial-thumbhash/IQgSLYZ6iHePh4h1eFeHh4dwgwg3.th
+OK. 27a6e456 test/data/artificial-thumbhash/VggKDYAW6lZvdYd6d2iZh_p4GE_k.th
+OK. 4965134c test/data/artificial-thumbhash/YJqGPQw7sFlslqhFafSE-Q6oJ1h2iHB2Rw.th
 OK. 076cb375 test/data/bricks-color.bmp
 OK. bdbbfadb test/data/bricks-color.etc1.pkm
 OK. 41e6110e test/data/bricks-color.etc1s.pkm