blob: b3b0c7456ce4a3deb90e024f9279de303b79ec54 [file] [log] [blame]
// 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
// ----------------
// Package uncompng encodes pixel data as not-compressed PNG-formatted bytes.
// The outputs are valid PNG files but avoids any compression techniques built
// into the PNG format. This obviously results in larger files than what the
// standard library's image/png package produces. Most programmers should use
// the standard package instead.
//
// The main purpose of this alternative PNG-encoding package is its simplicity
// of implementation, including all of its transitive dependencies. Its Go code
// can easily be studied (if you want to learn about the PNG file format),
// tested (the test code can depend on all of the standard library, including
// its image/png package), customized or ported to other languages. Unlike the
// standard package, this package has only two dependencies (the errors.New
// function and the io.Writer interface) and both are trivial.
//
// A secondary purpose is that, when starting with a slice of pixel data,
// producing an uncompressed PNG is faster than producing a compressed one - an
// extreme example of the general trade-off between compression speed and
// compression ratio. Similarly, decoding an uncompressed PNG can also be
// faster, even if the uncompressed PNG is bigger (in terms of byte size). If
// using PNG as a commonly-spoken format between two processes (connected by a
// Unix-like pipe) or between two libraries within the same process, and the
// transient PNG data never hits a disk drive or bandwidth-limited network, it
// may be better (faster) overall to skip any compression and decompression on
// either side of the communication channel.
//
// The Encoder.Encode method also makes no further memory allocations (above
// whatever its given io.Writer makes, if any). It is a lower-level API. See
// the encodeImage function in this package's test code for a higher-level API
// (with more dependencies and without the no-allocation guarantee) that takes
// an image.Image argument.
package uncompng
import (
"errors"
"io"
)
// ColorType states how to interpret the []byte pixel data as colors.
type ColorType byte
const (
// ColorTypeGray means 1 byte per pixel (or 2 for Depth16, big-endian).
//
// This matches Go's image.Gray.Pix (or Gray16, for Depth16) layout.
ColorTypeGray = ColorType(1)
// ColorTypeRGBX means 4 bytes per pixel (or 8 for Depth16, big-endian).
// Red, Green, Blue and the 4th channel is ignored.
//
// This matches Go's image.RGBA.Pix and image.NRGBA.Pix layouts (or RGBA64
// or NRGBA64, for Depth16), provided that all of the pixels' alpha values
// are 0xFF (or 0xFFFF, for Depth16).
ColorTypeRGBX = ColorType(2)
// ColorTypeRGBX means 4 bytes per pixel (or 8 for Depth16, big-endian).
// Red, Green, Blue and Alpha. RGB uses non-premultiplied alpha.
//
// This matches Go's image.NRGBA.Pix layout (or NRGBA64, for Depth16). If
// all of the pixels' alpha values are 0xFF (or 0xFFFF, for Depth16) then
// either ColorTypeRGBX or ColorTypeNRGBA will produce the same PNG output
// (in terms of pixels) but smaller (ColorTypeRGBX) or larger
// (ColorTypeNRGBA) output in terms of byte count.
ColorTypeNRGBA = ColorType(3)
)
func (c ColorType) pngFileFormatEncoding() byte {
// https://www.w3.org/TR/2003/REC-PNG-20031110/#6Colour-values
switch c {
case ColorTypeGray:
return 0
case ColorTypeRGBX:
return 2
case ColorTypeNRGBA:
return 6
}
return 0xFF
}
// Depth is the number of bits per channel.
type Depth byte
const (
// Depth8 means one byte per pixel.
Depth8 = Depth(8)
// Depth16 means two bytes per pixel. Values are big-endian like the Go
// standard library's Gray16, RGBA64 and NRGBA64 image types.
Depth16 = Depth(16)
)
// Encoder is an opaque type that can convert a slice of pixel data to
// PNG-formatted bytes.
//
// It contains all of the buffers needed for the Encode method. Once the
// Encoder itself is allocated, Encoder.Encode makes no further allocations
// (above whatever its given io.Writer makes, if any).
//
// It can be re-used (but it is not thread-safe). One Encoder can make multiple
// Encode calls, each call producing a complete, stand-alone PNG image.
type Encoder struct {
// buf is the fixed-size buffer to accumulate PNG-formatted output.
//
// A minimal PNG file consists of:
// - An 8-byte magic signature.
// - An IHDR chunk.
// - One or more IDAT chunks.
// - An IEND chunk.
//
// Each chunk consists of:
// - A 4-byte chunk payload length.
// - A 4-byte chunk type.
// - The chunk payload.
// - A 4-byte CRC-32/IEEE checksum of the chunk type and payload.
//
// Concatenating the IDAT payloads produces a ZLIB-compressed data stream.
// The ZLIB-decoded data consists of (((BPP * W) + 1) * H) bytes, where:
// - BPP is the number of bytes per pixel (e.g. 4 for RGBA).
// - W is the image width in pixels.
// - H is the image height in pixels.
//
// The +1 is because every row's pixel data is preceded by one byte for the
// per-row PNG filter. This package hard-codes a zero byte (no filtering).
//
// For a normal PNG encoder, the ZLIB-encoded data is shorter than the
// ZLIB-decoded data (as compression is the whole point of ZLIB). This PNG
// encoder prioritizes simplicity of implementation over compression ratio,
// so the ZLIB-encoded data is actually longer. The ZLIB-formatted stream
// consists of:
// - A 2-byte header.
// - A raw DEFLATE stream.
// - A 4-byte Adler-32 checksum of the ZLIB-decoded data.
//
// A raw DEFLATE stream is just one or more DEFLATE blocks.
//
// The DEFLATE format allows for uncompressed and compressed blocks. This
// encoder only emits uncompressed blocks. Each uncompressed block is:
// - A 1-byte header (0 means non-final, 1 means final).
// - A 2-byte u16le payload length.
// - That 2-byte u16le payload length again, xor'ed with 0xFFFF.
// - The uncompressed block payload (literal bytes).
//
// ----
//
// After that long preamble, here's the data layout of the buf field. Each
// time a slice of buf is passed to an io.Writer, it contains exactly one
// IDAT chunk. The final time will also contain an IEND chunk after the
// IDAT chunk, unless it won't fit, in which case there will be one more
// Write call with just the 12-byte IEND chunk (and no IDAT chunk).
//
// For all but the first Write, the 4-byte IDAT chunk length starts at
// buf[0x0000:]. For the first Write, it will start at buf[0x0021:], since
// it is preceded by the magic signature and the IHDR chunk.
//
// Either way, each IDAT chunk contains exactly one DEFLATE block (an
// uncompressed block). The very first IDAT (in the very first Write) also
// contains the 2-byte ZLIB header. The very last IDAT also contains the
// 4-byte Adler-32 checksum.
//
// Each DEFLATE uncompressed block's payload spans e.buf[ei:ej], for two
// indexes ei and ej, either explicit local variables or implicit state.
// Writing pixel data to the Encoder increments ej, flushing to the
// io.Writer beforehand (which resets ej = ei) if the data won't fit. ei is
// 0x0030 before the first Write call and at 0x000D afterwards.
//
// "The data won't fit" if ej would end up greater than ejMax, where ejMax
// is the size of the buffer (0x10000) minus 8 bytes that leaves enough
// room for two trailing 4-byte checksums (Adler-32 and CRC-32/IEEE).
//
// The last 4 bytes are also repurposed to hold the in-progress Adler-32
// checksum state, as these 4 bytes won't be overwritten unless emitting
// the final IDAT chunk (containing the final DEFLATE block).
buf [65536]byte
}
const (
eiFirst = 0x0030
eiLater = 0x000D
ejMax = 0xFFF8
)
// Encode writes the pixel data to w. It makes no allocations above whatever
// w.Write makes, if any.
//
// pix holds the pixel data, either 1 or 4 bytes per pixel (doubled for
// Depth16) depending on the colorType. width and height are measured in
// pixels. stride is measured in bytes. depth must be either Depth8 or Depth16.
func (e *Encoder) Encode(w io.Writer, pix []byte, width int, height int, stride int, depth Depth, colorType ColorType) error {
if (width < 0) || (height < 0) ||
((depth != Depth8) && (depth != Depth16)) ||
(colorType.pngFileFormatEncoding() == 0xFF) {
return errors.New("uncompng: invalid argument")
} else if (width > 0xFFFFFF) || (height > 0xFFFFFF) {
return errors.New("uncompng: unsupported image size")
}
e.init(width, height, depth, colorType)
ej := eiFirst
for y := 0; y < height; y++ {
if (ej + 1) > ejMax {
if err := e.flush(w, ej, false); err != nil {
return err
}
ej = eiLater
}
e.buf[ej+0] = 0 // PNG 'none' filter.
ej += 1
row := pix[y*stride:]
switch ColorType(depth) | colorType {
case 0x08 | ColorTypeGray:
row = row[:1*width]
for x := 0; x < width; x++ {
if (ej + 1) > ejMax {
if err := e.flush(w, ej, false); err != nil {
return err
}
ej = eiLater
}
e.buf[ej+0] = row[0]
ej += 1
row = row[1:]
}
case 0x08 | ColorTypeRGBX:
row = row[:4*width]
for x := 0; x < width; x++ {
if (ej + 3) > ejMax {
if err := e.flush(w, ej, false); err != nil {
return err
}
ej = eiLater
}
e.buf[ej+0] = row[0]
e.buf[ej+1] = row[1]
e.buf[ej+2] = row[2]
ej += 3
row = row[4:]
}
case 0x08 | ColorTypeNRGBA:
row = row[:4*width]
for x := 0; x < width; x++ {
if (ej + 4) > ejMax {
if err := e.flush(w, ej, false); err != nil {
return err
}
ej = eiLater
}
e.buf[ej+0] = row[0]
e.buf[ej+1] = row[1]
e.buf[ej+2] = row[2]
e.buf[ej+3] = row[3]
ej += 4
row = row[4:]
}
case 0x10 | ColorTypeGray:
row = row[:2*width]
for x := 0; x < width; x++ {
if (ej + 2) > ejMax {
if err := e.flush(w, ej, false); err != nil {
return err
}
ej = eiLater
}
e.buf[ej+0] = row[0]
e.buf[ej+1] = row[1]
ej += 2
row = row[2:]
}
case 0x10 | ColorTypeRGBX:
row = row[:8*width]
for x := 0; x < width; x++ {
if (ej + 6) > ejMax {
if err := e.flush(w, ej, false); err != nil {
return err
}
ej = eiLater
}
e.buf[ej+0] = row[0]
e.buf[ej+1] = row[1]
e.buf[ej+2] = row[2]
e.buf[ej+3] = row[3]
e.buf[ej+4] = row[4]
e.buf[ej+5] = row[5]
ej += 6
row = row[8:]
}
case 0x10 | ColorTypeNRGBA:
row = row[:8*width]
for x := 0; x < width; x++ {
if (ej + 8) > ejMax {
if err := e.flush(w, ej, false); err != nil {
return err
}
ej = eiLater
}
e.buf[ej+0] = row[0]
e.buf[ej+1] = row[1]
e.buf[ej+2] = row[2]
e.buf[ej+3] = row[3]
e.buf[ej+4] = row[4]
e.buf[ej+5] = row[5]
e.buf[ej+6] = row[6]
e.buf[ej+7] = row[7]
ej += 8
row = row[8:]
}
}
}
return e.flush(w, ej, true)
}
func (e *Encoder) init(width int, height int, depth Depth, colorType ColorType) {
// PNG magic signature.
e.buf[0x0000] = 0x89
e.buf[0x0001] = 'P'
e.buf[0x0002] = 'N'
e.buf[0x0003] = 'G'
e.buf[0x0004] = 0x0D
e.buf[0x0005] = 0x0A
e.buf[0x0006] = 0x1A
e.buf[0x0007] = 0x0A
// IHDR chunk length.
e.buf[0x0008] = 0
e.buf[0x0009] = 0
e.buf[0x000A] = 0
e.buf[0x000B] = 0x0D
// IHDR chunk type.
e.buf[0x000C] = 'I'
e.buf[0x000D] = 'H'
e.buf[0x000E] = 'D'
e.buf[0x000F] = 'R'
// IHDR chunk payload.
e.buf[0x0010] = byte(width >> 24)
e.buf[0x0011] = byte(width >> 16)
e.buf[0x0012] = byte(width >> 8)
e.buf[0x0013] = byte(width >> 0)
e.buf[0x0014] = byte(height >> 24)
e.buf[0x0015] = byte(height >> 16)
e.buf[0x0016] = byte(height >> 8)
e.buf[0x0017] = byte(height >> 0)
e.buf[0x0018] = byte(depth)
e.buf[0x0019] = colorType.pngFileFormatEncoding()
e.buf[0x001A] = 0 // Compression method.
e.buf[0x001B] = 0 // Filter method.
e.buf[0x001C] = 0 // Interlace method.
// IHDR CRC-32/IEEE checksum.
ihdrCRC32 := crc32IEEE(e.buf[0x000C:0x001D])
e.buf[0x001D] = byte(ihdrCRC32 >> 24)
e.buf[0x001E] = byte(ihdrCRC32 >> 16)
e.buf[0x001F] = byte(ihdrCRC32 >> 8)
e.buf[0x0020] = byte(ihdrCRC32 >> 0)
// IDAT chunk length placeholder.
e.buf[0x0021] = 0
e.buf[0x0022] = 0
e.buf[0x0023] = 0
e.buf[0x0024] = 0
// IDAT chunk type.
e.buf[0x0025] = 'I'
e.buf[0x0026] = 'D'
e.buf[0x0027] = 'A'
e.buf[0x0028] = 'T'
// ZLIB header: CM=8, CINFO=7, FDICT=0, FLEVEL=0. See RFC 1950.
e.buf[0x0029] = 0x78
e.buf[0x002A] = 0x01
// DEFLATE uncompressed block header, with 4-byte placeholder for length.
e.buf[0x002B] = 0
e.buf[0x002C] = 0
e.buf[0x002D] = 0
e.buf[0x002E] = 0
e.buf[0x002F] = 0
// eiFirst is therefore 0x0030.
// AdlerB=0, Adler32A=1.
e.buf[0xFFFC] = 0
e.buf[0xFFFD] = 0
e.buf[0xFFFE] = 0
e.buf[0xFFFF] = 1
}
func (e *Encoder) updateAdler32(ei int, ej int) {
b := (uint32(e.buf[0xFFFC]) << 8) | uint32(e.buf[0xFFFD])
a := (uint32(e.buf[0xFFFE]) << 8) | uint32(e.buf[0xFFFF])
for ei < ej {
end := ei + 5552
if end > ej {
end = ej
}
for ; ei < end; ei++ {
a += uint32(e.buf[ei])
b += a
}
a %= 65521
b %= 65521
}
e.buf[0xFFFC] = byte(b >> 8)
e.buf[0xFFFD] = byte(b >> 0)
e.buf[0xFFFE] = byte(a >> 8)
e.buf[0xFFFF] = byte(a >> 0)
}
func (e *Encoder) flush(w io.Writer, ej int, final bool) error {
// Update the IDAT chunk length placeholder.
crc32Start, ei := 0, 0
if e.buf[0x0004] == 0x0D { // First IDAT chunk.
idatChunkLen := ej - 0x0029
if final {
idatChunkLen += 4
}
e.buf[0x0021] = byte(idatChunkLen >> 24)
e.buf[0x0022] = byte(idatChunkLen >> 16)
e.buf[0x0023] = byte(idatChunkLen >> 8)
e.buf[0x0024] = byte(idatChunkLen >> 0)
crc32Start, ei = 0x0025, eiFirst
} else { // Later IDAT chunk.
idatChunkLen := ej - 0x0008
if final {
idatChunkLen += 4
}
e.buf[0x0000] = byte(idatChunkLen >> 24)
e.buf[0x0001] = byte(idatChunkLen >> 16)
e.buf[0x0002] = byte(idatChunkLen >> 8)
e.buf[0x0003] = byte(idatChunkLen >> 0)
crc32Start, ei = 0x0004, eiLater
}
// Update the DEFLATE uncompressed block header placeholder.
deflateBlockLen := ej - ei
e.buf[ei-5] = btou8(final)
e.buf[ei-4] = 0x00 ^ byte(deflateBlockLen>>0)
e.buf[ei-3] = 0x00 ^ byte(deflateBlockLen>>8)
e.buf[ei-2] = 0xFF ^ byte(deflateBlockLen>>0)
e.buf[ei-1] = 0xFF ^ byte(deflateBlockLen>>8)
// Update (and maybe write) the Adler-32 checksum.
e.updateAdler32(ei, ej)
if final {
e.buf[ej+0] = e.buf[0xFFFC]
e.buf[ej+1] = e.buf[0xFFFD]
e.buf[ej+2] = e.buf[0xFFFE]
e.buf[ej+3] = e.buf[0xFFFF]
ej += 4
}
// Write the CRC-32/IEEE checksum.
idatCRC32 := crc32IEEE(e.buf[crc32Start:ej])
e.buf[ej+0] = byte(idatCRC32 >> 24)
e.buf[ej+1] = byte(idatCRC32 >> 16)
e.buf[ej+2] = byte(idatCRC32 >> 8)
e.buf[ej+3] = byte(idatCRC32 >> 0)
ej += 4
if !final {
if _, err := w.Write(e.buf[:ej]); err != nil {
return err
}
// Write (or reserve space for) 0x000D bytes total:
// - 4 bytes for the IDAT chunk length placeholder.
// - 4 bytes for the IDAT chunk type.
// - 5 bytes for the DEFLATE uncompressed block header placeholder.
//
// eiLater is therefore 0x000D.
e.buf[0x0004] = 'I'
e.buf[0x0005] = 'D'
e.buf[0x0006] = 'A'
e.buf[0x0007] = 'T'
return nil
}
const iendChunk = "\x00\x00\x00\x00IEND\xAE\x42\x60\x82"
writeSeparateIENDChunk := (ej + len(iendChunk)) > len(e.buf)
if !writeSeparateIENDChunk {
copy(e.buf[ej:ej+len(iendChunk)], iendChunk)
ej += len(iendChunk)
}
if _, err := w.Write(e.buf[:ej]); err != nil {
return err
}
if writeSeparateIENDChunk {
copy(e.buf[:], iendChunk)
if _, err := w.Write(e.buf[:len(iendChunk)]); err != nil {
return err
}
}
return nil
}
func btou8(a bool) uint8 {
if a {
return 1
}
return 0
}
func crc32IEEE(b []byte) uint32 {
hash := uint32(0xFFFF_FFFF)
for _, v := range b {
hash = crc32IEEETable[uint8(hash)^v] ^ (hash >> 8)
}
return hash ^ uint32(0xFFFF_FFFF)
}
var crc32IEEETable = [256]uint32{
0x0000_0000, 0x7707_3096, 0xEE0E_612C, 0x9909_51BA, 0x076D_C419, 0x706A_F48F, 0xE963_A535, 0x9E64_95A3,
0x0EDB_8832, 0x79DC_B8A4, 0xE0D5_E91E, 0x97D2_D988, 0x09B6_4C2B, 0x7EB1_7CBD, 0xE7B8_2D07, 0x90BF_1D91,
0x1DB7_1064, 0x6AB0_20F2, 0xF3B9_7148, 0x84BE_41DE, 0x1ADA_D47D, 0x6DDD_E4EB, 0xF4D4_B551, 0x83D3_85C7,
0x136C_9856, 0x646B_A8C0, 0xFD62_F97A, 0x8A65_C9EC, 0x1401_5C4F, 0x6306_6CD9, 0xFA0F_3D63, 0x8D08_0DF5,
0x3B6E_20C8, 0x4C69_105E, 0xD560_41E4, 0xA267_7172, 0x3C03_E4D1, 0x4B04_D447, 0xD20D_85FD, 0xA50A_B56B,
0x35B5_A8FA, 0x42B2_986C, 0xDBBB_C9D6, 0xACBC_F940, 0x32D8_6CE3, 0x45DF_5C75, 0xDCD6_0DCF, 0xABD1_3D59,
0x26D9_30AC, 0x51DE_003A, 0xC8D7_5180, 0xBFD0_6116, 0x21B4_F4B5, 0x56B3_C423, 0xCFBA_9599, 0xB8BD_A50F,
0x2802_B89E, 0x5F05_8808, 0xC60C_D9B2, 0xB10B_E924, 0x2F6F_7C87, 0x5868_4C11, 0xC161_1DAB, 0xB666_2D3D,
0x76DC_4190, 0x01DB_7106, 0x98D2_20BC, 0xEFD5_102A, 0x71B1_8589, 0x06B6_B51F, 0x9FBF_E4A5, 0xE8B8_D433,
0x7807_C9A2, 0x0F00_F934, 0x9609_A88E, 0xE10E_9818, 0x7F6A_0DBB, 0x086D_3D2D, 0x9164_6C97, 0xE663_5C01,
0x6B6B_51F4, 0x1C6C_6162, 0x8565_30D8, 0xF262_004E, 0x6C06_95ED, 0x1B01_A57B, 0x8208_F4C1, 0xF50F_C457,
0x65B0_D9C6, 0x12B7_E950, 0x8BBE_B8EA, 0xFCB9_887C, 0x62DD_1DDF, 0x15DA_2D49, 0x8CD3_7CF3, 0xFBD4_4C65,
0x4DB2_6158, 0x3AB5_51CE, 0xA3BC_0074, 0xD4BB_30E2, 0x4ADF_A541, 0x3DD8_95D7, 0xA4D1_C46D, 0xD3D6_F4FB,
0x4369_E96A, 0x346E_D9FC, 0xAD67_8846, 0xDA60_B8D0, 0x4404_2D73, 0x3303_1DE5, 0xAA0A_4C5F, 0xDD0D_7CC9,
0x5005_713C, 0x2702_41AA, 0xBE0B_1010, 0xC90C_2086, 0x5768_B525, 0x206F_85B3, 0xB966_D409, 0xCE61_E49F,
0x5EDE_F90E, 0x29D9_C998, 0xB0D0_9822, 0xC7D7_A8B4, 0x59B3_3D17, 0x2EB4_0D81, 0xB7BD_5C3B, 0xC0BA_6CAD,
0xEDB8_8320, 0x9ABF_B3B6, 0x03B6_E20C, 0x74B1_D29A, 0xEAD5_4739, 0x9DD2_77AF, 0x04DB_2615, 0x73DC_1683,
0xE363_0B12, 0x9464_3B84, 0x0D6D_6A3E, 0x7A6A_5AA8, 0xE40E_CF0B, 0x9309_FF9D, 0x0A00_AE27, 0x7D07_9EB1,
0xF00F_9344, 0x8708_A3D2, 0x1E01_F268, 0x6906_C2FE, 0xF762_575D, 0x8065_67CB, 0x196C_3671, 0x6E6B_06E7,
0xFED4_1B76, 0x89D3_2BE0, 0x10DA_7A5A, 0x67DD_4ACC, 0xF9B9_DF6F, 0x8EBE_EFF9, 0x17B7_BE43, 0x60B0_8ED5,
0xD6D6_A3E8, 0xA1D1_937E, 0x38D8_C2C4, 0x4FDF_F252, 0xD1BB_67F1, 0xA6BC_5767, 0x3FB5_06DD, 0x48B2_364B,
0xD80D_2BDA, 0xAF0A_1B4C, 0x3603_4AF6, 0x4104_7A60, 0xDF60_EFC3, 0xA867_DF55, 0x316E_8EEF, 0x4669_BE79,
0xCB61_B38C, 0xBC66_831A, 0x256F_D2A0, 0x5268_E236, 0xCC0C_7795, 0xBB0B_4703, 0x2202_16B9, 0x5505_262F,
0xC5BA_3BBE, 0xB2BD_0B28, 0x2BB4_5A92, 0x5CB3_6A04, 0xC2D7_FFA7, 0xB5D0_CF31, 0x2CD9_9E8B, 0x5BDE_AE1D,
0x9B64_C2B0, 0xEC63_F226, 0x756A_A39C, 0x026D_930A, 0x9C09_06A9, 0xEB0E_363F, 0x7207_6785, 0x0500_5713,
0x95BF_4A82, 0xE2B8_7A14, 0x7BB1_2BAE, 0x0CB6_1B38, 0x92D2_8E9B, 0xE5D5_BE0D, 0x7CDC_EFB7, 0x0BDB_DF21,
0x86D3_D2D4, 0xF1D4_E242, 0x68DD_B3F8, 0x1FDA_836E, 0x81BE_16CD, 0xF6B9_265B, 0x6FB0_77E1, 0x18B7_4777,
0x8808_5AE6, 0xFF0F_6A70, 0x6606_3BCA, 0x1101_0B5C, 0x8F65_9EFF, 0xF862_AE69, 0x616B_FFD3, 0x166C_CF45,
0xA00A_E278, 0xD70D_D2EE, 0x4E04_8354, 0x3903_B3C2, 0xA767_2661, 0xD060_16F7, 0x4969_474D, 0x3E6E_77DB,
0xAED1_6A4A, 0xD9D6_5ADC, 0x40DF_0B66, 0x37D8_3BF0, 0xA9BC_AE53, 0xDEBB_9EC5, 0x47B2_CF7F, 0x30B5_FFE9,
0xBDBD_F21C, 0xCABA_C28A, 0x53B3_9330, 0x24B4_A3A6, 0xBAD0_3605, 0xCDD7_0693, 0x54DE_5729, 0x23D9_67BF,
0xB366_7A2E, 0xC461_4AB8, 0x5D68_1B02, 0x2A6F_2B94, 0xB40B_BE37, 0xC30C_8EA1, 0x5A05_DF1B, 0x2D02_EF8D,
}