blob: ed405e83a070a7e1968d038586226a53925f3dc5 [file]
// Copyright 2026 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
// ----------------
// This is a small, self-contained, single-file C library to decode basic
// Handsum image files. Basic means that it only supports Handsum's default
// configuration, c3q4: color=3 (RGB, no Alpha) and quality=4 (best).
//
// It is a C port of some (decoding only and c3q4 only) of this Go library:
// https://pkg.go.dev/github.com/google/wuffs/lib/handsum
//
// To decode other configurations, like c1q1 (Gray, worst quality) or c4q2
// (RGBA, medium-low quality), use the github.com/google/wuffs Go or
// C-transpiled-from-memory-safe-Wuffs implementations.
//
// To use this file as a "foo.c"-like implementation, instead of a "foo.h"-like
// header, #define BASIC_HANDSUM_DECODE_IMPLEMENTATION before #include'ing or
// compiling it.
//
// As an option, you may also #define
// BASIC_HANDSUM_DECODE_CONFIG__STATIC_FUNCTIONS to make these functions have
// static storage. This can help the compiler ignore or discard unused code,
// which can produce faster compiles and smaller binaries.
#ifndef BASIC_HANDSUM_DECODE_INCLUDE_GUARD
#define BASIC_HANDSUM_DECODE_INCLUDE_GUARD
#if defined(BASIC_HANDSUM_DECODE_CONFIG__STATIC_FUNCTIONS)
#define BASIC_HANDSUM_DECODE__MAYBE_STATIC static
#else
#define BASIC_HANDSUM_DECODE__MAYBE_STATIC
#endif
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
typedef struct basic_handsum_decode__pixel_buffer_type {
uint32_t width; // Measured in pixels, not bytes.
uint32_t height; // Measured in pixels, not bytes.
uint8_t bgra_pixels[1024];
} basic_handsum_decode__pixel_buffer;
// BASIC_HANDSUM_DECODE__RESULT__ETC can be returned by
// basic_handsum_decode__decode. Zero means OK. Non-zero means an error.
#define BASIC_HANDSUM_DECODE__RESULT__OK 0
#define BASIC_HANDSUM_DECODE__RESULT__DST_IS_NULL 1
#define BASIC_HANDSUM_DECODE__RESULT__SRC_IS_TOO_SHORT 2
#define BASIC_HANDSUM_DECODE__RESULT__SRC_IS_NOT_HANDSUM_C3Q4 3
// Decodes the source bytes into the destination pixel buffer, producing 4
// bytes per pixel (in B, G, R, A order). The bgra_pixels' row stride will be
// (4 * width) bytes.
//
// It only supports c3q4 Handsum files: color=3 (so alpha is always 0xFF) and
// quality=4 (best). Every valid c3q4 Handsum file is exactly 147 bytes long.
//
// On success, it returns zero and dst's width and height will range between 1
// and 16, inclusive.
//
// On failure, it returns non-zero and, if dst is non-NULL, its width and
// height will be set to zero.
//
// This function is thread-safe.
BASIC_HANDSUM_DECODE__MAYBE_STATIC int //
basic_handsum_decode__decode(basic_handsum_decode__pixel_buffer* dst,
const uint8_t* src_ptr,
const size_t src_len);
// --------
#ifdef BASIC_HANDSUM_DECODE_IMPLEMENTATION
#ifdef __wasm__
static void //
basic_handsum_decode__memcpy(uint8_t* restrict dst,
const uint8_t* restrict src,
size_t n) {
while (n--) {
*dst++ = *src++;
}
}
#else
#include <string.h>
#define basic_handsum_decode__memcpy memcpy
#endif
static void //
basic_handsum_decode__smooth_block_seams_16x16(uint8_t* b) {
static const uint8_t smoothing_pairs_16x16[28][2] = {
{0x07, 0x08}, {0x17, 0x18}, {0x27, 0x28}, {0x37, 0x38},
{0x47, 0x48}, {0x57, 0x58}, {0x67, 0x68},
{0x70, 0x80}, {0x71, 0x81}, {0x72, 0x82}, {0x73, 0x83},
{0x74, 0x84}, {0x75, 0x85}, {0x76, 0x86},
{0x79, 0x89}, {0x7A, 0x8A}, {0x7B, 0x8B}, {0x7C, 0x8C},
{0x7D, 0x8D}, {0x7E, 0x8E}, {0x7F, 0x8F},
{0x97, 0x98}, {0xA7, 0xA8}, {0xB7, 0xB8}, {0xC7, 0xC8},
{0xD7, 0xD8}, {0xE7, 0xE8}, {0xF7, 0xF8},
};
for (int i = 0; i < 28; i++) {
uint8_t p0 = smoothing_pairs_16x16[i][0];
uint8_t p1 = smoothing_pairs_16x16[i][1];
uint32_t v0 = b[p0];
uint32_t v1 = b[p1];
b[p0] = ((3 * v0) + v1 + 2) / 4;
b[p1] = ((3 * v1) + v0 + 2) / 4;
}
uint32_t v77 = b[0x77];
uint32_t v78 = b[0x78];
uint32_t v88 = b[0x88];
uint32_t v87 = b[0x87];
b[0x77] = ((9 * v77) + (3 * v78) + v88 + (3 * v87) + 8) / 16;
b[0x78] = ((9 * v78) + (3 * v88) + v87 + (3 * v77) + 8) / 16;
b[0x88] = ((9 * v88) + (3 * v87) + v77 + (3 * v78) + 8) / 16;
b[0x87] = ((9 * v87) + (3 * v77) + v78 + (3 * v88) + 8) / 16;
}
static void //
basic_handsum_decode__smooth_block_seams_8x8_q4(uint8_t* b) {
for (int y = 1; y < 7; y += 2) {
for (int x = 1; x < 7; x += 2) {
int o = (y * 8) + x;
uint32_t v0 = b[o + 0];
uint32_t v1 = b[o + 1];
uint32_t v9 = b[o + 9];
uint32_t v8 = b[o + 8];
b[o + 0] = ((9 * v0) + (3 * v1) + v9 + (3 * v8) + 8) / 16;
b[o + 1] = ((9 * v1) + (3 * v9) + v8 + (3 * v0) + 8) / 16;
b[o + 9] = ((9 * v9) + (3 * v8) + v0 + (3 * v1) + 8) / 16;
b[o + 8] = ((9 * v8) + (3 * v0) + v1 + (3 * v9) + 8) / 16;
}
{
uint32_t v0 = b[000 + y];
uint32_t v1 = b[001 + y];
b[000 + y] = ((3 * v0) + v1 + 2) / 4;
b[001 + y] = ((3 * v1) + v0 + 2) / 4;
}
{
uint32_t v0 = b[070 + y];
uint32_t v1 = b[071 + y];
b[070 + y] = ((3 * v0) + v1 + 2) / 4;
b[071 + y] = ((3 * v1) + v0 + 2) / 4;
}
int y8 = y * 8;
{
uint32_t v0 = b[000 + y8];
uint32_t v1 = b[010 + y8];
b[000 + y8] = ((3 * v0) + v1 + 2) / 4;
b[010 + y8] = ((3 * v1) + v0 + 2) / 4;
}
{
uint32_t v0 = b[007 + y8];
uint32_t v1 = b[017 + y8];
b[007 + y8] = ((3 * v0) + v1 + 2) / 4;
b[017 + y8] = ((3 * v1) + v0 + 2) / 4;
}
}
}
// Return (x + 0x80), if in range, otherwise clamped to 0x00 or 0xFF.
static uint8_t //
basic_handsum_decode__bias_and_clamp(int x) {
x += 0x80;
return ((x >> 8) == 0) ? x : ~(x >> 31);
}
static int //
basic_handsum_decode__decode_block_q4(uint8_t* dst_ptr,
size_t dst_stride,
const uint8_t* src_ptr,
size_t bit_offset) {
uint8_t tmp[64];
for (int i = 0; i < 16; i++) {
int nibble0 = (src_ptr[bit_offset >> 3] >> (bit_offset & 4)) & 15;
int dct0 = (nibble0 * 0x22) - 256;
bit_offset += 4;
int nibble1 = (src_ptr[bit_offset >> 3] >> (bit_offset & 4)) & 15;
int dct1 = (nibble1 * 0x10) - 128;
bit_offset += 4;
int nibble2 = (src_ptr[bit_offset >> 3] >> (bit_offset & 4)) & 15;
int dct2 = (nibble2 * 0x10) - 128;
bit_offset += 4;
int x2 = 2 * (i & 3);
int y2 = 2 * (i >> 2);
int o2 = (y2 * 8) + x2;
tmp[o2 + 0] =
basic_handsum_decode__bias_and_clamp((+dct0 + dct1 + dct2 + 1) >> 1);
tmp[o2 + 1] =
basic_handsum_decode__bias_and_clamp((+dct0 - dct1 + dct2 + 1) >> 1);
tmp[o2 + 8] =
basic_handsum_decode__bias_and_clamp((+dct0 + dct1 - dct2 + 1) >> 1);
tmp[o2 + 9] =
basic_handsum_decode__bias_and_clamp((+dct0 - dct1 - dct2 + 1) >> 1);
}
basic_handsum_decode__smooth_block_seams_8x8_q4(&tmp[0]);
for (size_t i = 0; i < 8; i++) {
size_t di = i * dst_stride;
size_t ti = i * 8;
basic_handsum_decode__memcpy(&dst_ptr[di], &tmp[ti], 8);
}
return bit_offset;
}
static void //
basic_handsum_decode__scale_and_bias_chroma_down(uint8_t* b) {
for (int i = 0; i < 64; i++) {
b[i] = (b[i] >> 1) + 0x3C;
}
}
static void //
basic_handsum_decode__upsample_from(uint8_t* restrict dst,
const uint8_t* restrict src) {
// dst is 16x16, src is 8x8.
for (int y = 0; y < 16; y++) {
// sy is: 0, +8, -8, +8, -8, ..., +8, -8, 0.
int sy = ((y == 0) || (y == 15)) ? 0 : ((y & 1) * 16) - 8;
for (int x = 0; x < 16; x++) {
// sx is: 0, +1, -1, +1, -1, ..., +1, -1, 0.
int sx = ((x == 0) || (x == 15)) ? 0 : ((x & 1) * 2) - 1;
int s0 = ((y >> 1) * 8) + (x >> 1);
int v00 = src[s0];
int v01 = src[s0 + sx];
int v10 = src[s0 + sy];
int v11 = src[s0 + sx + sy];
int d0 = (y * 16) + x;
dst[d0] = ((9 * v00) + (3 * v01) + (3 * v10) + v11 + 8) / 16;
}
}
}
static void //
basic_handsum_decode__scale_horizontal(
basic_handsum_decode__pixel_buffer* pixbuf) {
uint8_t dst[1024];
uint32_t w = pixbuf->width;
const uint8_t* src = pixbuf->bgra_pixels;
for (int y = 0; y < 16; y++) {
int dstx = 0;
uint32_t acc0 = 0;
uint32_t acc1 = 0;
uint32_t acc2 = 0;
uint32_t acc3 = 0;
uint32_t remainder = 16;
for (int srcx = 0; srcx < 16; srcx++) {
int si = (64 * y) + (4 * srcx);
uint32_t s0 = src[si + 0];
uint32_t s1 = src[si + 1];
uint32_t s2 = src[si + 2];
uint32_t s3 = src[si + 3];
if (remainder > w) {
remainder -= w;
acc0 += w * s0;
acc1 += w * s1;
acc2 += w * s2;
acc3 += w * s3;
} else {
acc0 += remainder * s0;
acc1 += remainder * s1;
acc2 += remainder * s2;
acc3 += remainder * s3;
int di = (4 * w * y) + (4 * dstx);
dst[di + 0] = (acc0 + 8) / 16;
dst[di + 1] = (acc1 + 8) / 16;
dst[di + 2] = (acc2 + 8) / 16;
dst[di + 3] = (acc3 + 8) / 16;
dstx++;
uint32_t partial = w - remainder;
acc0 = partial * s0;
acc1 = partial * s1;
acc2 = partial * s2;
acc3 = partial * s3;
remainder = 16 - partial;
}
}
}
for (int y = 0; y < 16; y++) {
basic_handsum_decode__memcpy(&pixbuf->bgra_pixels[4 * w * y],
&dst[4 * w * y], 4 * w);
}
}
static void //
basic_handsum_decode__scale_vertical(
basic_handsum_decode__pixel_buffer* pixbuf) {
uint8_t dst[1024];
uint32_t h = pixbuf->height;
const uint8_t* src = pixbuf->bgra_pixels;
for (int x = 0; x < 16; x++) {
int dsty = 0;
uint32_t acc0 = 0;
uint32_t acc1 = 0;
uint32_t acc2 = 0;
uint32_t acc3 = 0;
uint32_t remainder = 16;
for (int srcy = 0; srcy < 16; srcy++) {
int si = (64 * srcy) + (4 * x);
uint32_t s0 = src[si + 0];
uint32_t s1 = src[si + 1];
uint32_t s2 = src[si + 2];
uint32_t s3 = src[si + 3];
if (remainder > h) {
remainder -= h;
acc0 += h * s0;
acc1 += h * s1;
acc2 += h * s2;
acc3 += h * s3;
} else {
acc0 += remainder * s0;
acc1 += remainder * s1;
acc2 += remainder * s2;
acc3 += remainder * s3;
int di = (64 * dsty) + (4 * x);
dst[di + 0] = (acc0 + 8) / 16;
dst[di + 1] = (acc1 + 8) / 16;
dst[di + 2] = (acc2 + 8) / 16;
dst[di + 3] = (acc3 + 8) / 16;
dsty++;
uint32_t partial = h - remainder;
acc0 = partial * s0;
acc1 = partial * s1;
acc2 = partial * s2;
acc3 = partial * s3;
remainder = 16 - partial;
}
}
}
basic_handsum_decode__memcpy(&pixbuf->bgra_pixels[0], &dst[0], 64 * h);
}
static void //
basic_handsum_decode__convert_ycbcr_bgra(
basic_handsum_decode__pixel_buffer* dst,
const uint8_t* restrict yy,
const uint8_t* restrict cb,
const uint8_t* restrict cr) {
for (int i = 0; i < 256; i++) {
int32_t yy1 = (int32_t)(yy[i]) * 0x10101;
int32_t cb1 = (int32_t)(cb[i]) - 128;
int32_t cr1 = (int32_t)(cr[i]) - 128;
int32_t r = yy1 + (91881 * cr1);
if (((uint32_t)(r)&0xFF000000u) == 0) {
r >>= 16;
} else {
r = -1 ^ (r >> 31);
}
int32_t g = yy1 - (22554 * cb1) - (46802 * cr1);
if (((uint32_t)(g)&0xFF000000u) == 0) {
g >>= 16;
} else {
g = -1 ^ (g >> 31);
}
int32_t b = yy1 + (116130 * cb1);
if (((uint32_t)(b)&0xFF000000u) == 0) {
b >>= 16;
} else {
b = -1 ^ (b >> 31);
}
dst->bgra_pixels[(4 * i) + 0] = b;
dst->bgra_pixels[(4 * i) + 1] = g;
dst->bgra_pixels[(4 * i) + 2] = r;
dst->bgra_pixels[(4 * i) + 3] = 0xFF;
}
}
BASIC_HANDSUM_DECODE__MAYBE_STATIC int //
basic_handsum_decode__decode(basic_handsum_decode__pixel_buffer* dst,
const uint8_t* src_ptr,
const size_t src_len) {
if (dst == NULL) {
return BASIC_HANDSUM_DECODE__RESULT__DST_IS_NULL;
}
dst->width = 0;
dst->height = 0;
if (src_len < 147) {
return BASIC_HANDSUM_DECODE__RESULT__SRC_IS_TOO_SHORT;
} else if ((src_ptr[0] != 0xFE) || //
(src_ptr[1] != 0xD7) || //
((src_ptr[2] & 0xE0) != 0x60) || //
((src_ptr[2] & 0x1F) == 0x1F)) {
return BASIC_HANDSUM_DECODE__RESULT__SRC_IS_NOT_HANDSUM_C3Q4;
}
if ((src_ptr[2] & 0x10) == 0x00) { // Landscape.
dst->width = 16;
dst->height = 1 + (src_ptr[2] & 0x0F);
} else { // Portrait.
dst->width = 1 + (src_ptr[2] & 0x0F);
dst->height = 16;
}
uint8_t luma_quad_block[256];
uint8_t tmp[64];
uint8_t cb_quad_block[256];
uint8_t cr_quad_block[256];
size_t bit_offset = 24;
bit_offset = basic_handsum_decode__decode_block_q4( //
&luma_quad_block[0x00], 16, src_ptr, bit_offset);
bit_offset = basic_handsum_decode__decode_block_q4( //
&luma_quad_block[0x08], 16, src_ptr, bit_offset);
bit_offset = basic_handsum_decode__decode_block_q4( //
&luma_quad_block[0x80], 16, src_ptr, bit_offset);
bit_offset = basic_handsum_decode__decode_block_q4( //
&luma_quad_block[0x88], 16, src_ptr, bit_offset);
basic_handsum_decode__smooth_block_seams_16x16(&luma_quad_block[0]);
bit_offset = basic_handsum_decode__decode_block_q4( //
&tmp[0], 8, src_ptr, bit_offset);
basic_handsum_decode__scale_and_bias_chroma_down(&tmp[0]);
basic_handsum_decode__upsample_from(&cb_quad_block[0], &tmp[0]);
bit_offset = basic_handsum_decode__decode_block_q4( //
&tmp[0], 8, src_ptr, bit_offset);
basic_handsum_decode__scale_and_bias_chroma_down(&tmp[0]);
basic_handsum_decode__upsample_from(&cr_quad_block[0], &tmp[0]);
basic_handsum_decode__convert_ycbcr_bgra(
dst, &luma_quad_block[0], &cb_quad_block[0], &cr_quad_block[0]);
if (dst->width < 16) {
basic_handsum_decode__scale_horizontal(dst);
} else if (dst->height < 16) {
basic_handsum_decode__scale_vertical(dst);
}
return BASIC_HANDSUM_DECODE__RESULT__OK;
}
#endif // BASIC_HANDSUM_DECODE_IMPLEMENTATION
#endif // BASIC_HANDSUM_DECODE_INCLUDE_GUARD