Full Code of dbohdan/hicolor for AI

master 1c437f80229a cached
13 files
54.0 KB
17.3k tokens
34 symbols
1 requests
Download .txt
Repository: dbohdan/hicolor
Branch: master
Commit: 1c437f80229a
Files: 13
Total size: 54.0 KB

Directory structure:
gitextract_as8h71n0/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── install-deps.sh
├── .gitignore
├── AUTHORS
├── GNUmakefile
├── LICENSE
├── README.md
├── cli.c
├── format.md
├── hicolor.h
├── scripts/
│   ├── bayer-matrix.tcl
│   └── conversion-tables.tcl
└── tests/
    └── hicolor.test

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push, pull_request]
jobs:
  bsd:
    runs-on: ${{ matrix.os.host }}
    strategy:
      matrix:
        os:
          - name: freebsd
            architecture: x86-64
            version: '14.1'
            host: ubuntu-latest

          - name: netbsd
            architecture: x86-64
            version: '10.0'
            host: ubuntu-latest

          - name: openbsd
            architecture: x86-64
            version: '7.5'
            host: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Run CI script on ${{ matrix.os.name }}
        uses: cross-platform-actions/action@v0.25.0
        with:
          operating_system: ${{ matrix.os.name }}
          architecture: ${{ matrix.os.architecture }}
          version: ${{ matrix.os.version }}
          shell: bash
          run: |
            # Use sudo(1) rather than doas(1) on OpenBSD.
            # doas(1) isn't configured.
            # See https://github.com/cross-platform-actions/action/issues/75
            sudo .github/workflows/install-deps.sh
            gmake test

  linux:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Install dependencies
      run: |
        sudo .github/workflows/install-deps.sh

    - name: Test
      run: |
        gmake test

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: hicolor-linux-x86_64
        path: |
          hicolor

  mac:
    runs-on: macos-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Install dependencies
      run: |
        .github/workflows/install-deps.sh

    - name: Build and test
      run: |
        make test

    - name: Bundle dynamic libraries
      run: |
        dylibbundler --bundle-deps --create-dir --fix-file hicolor

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: hicolor-macos-arm64
        path: |
          hicolor
          libs/

  windows:
    runs-on: windows-latest
    steps:
    - name: 'Disable `autocrlf` in Git'
      run: git config --global core.autocrlf false

    - name: Checkout
      uses: actions/checkout@v4

    - name: Set up MSYS2
      uses: msys2/setup-msys2@v2
      with:
        update: true
        msystem: mingw32
        install: |
          make
          mingw-w64-i686-gcc
          mingw-w64-i686-libpng
          mingw-w64-i686-pkgconf
          mingw-w64-i686-zlib
          tcl

    - name: Test
      shell: msys2 {0}
      run: |
        make test

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: hicolor-win32
        path: |
          hicolor.exe


================================================
FILE: .github/workflows/install-deps.sh
================================================
#! /bin/sh
set -e

if [ "$(uname)" = Darwin ]; then
    brew install dylibbundler tcl-tk
fi

if [ "$(uname)" = Linux ]; then
    apt-get install -y graphicsmagick libpng-dev pkgconf
fi

if [ "$(uname)" = FreeBSD ]; then
    pkg install -y gmake GraphicsMagick pkgconf png tcl86
    ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh
fi

if [ "$(uname)" = NetBSD ]; then
    pkgin -y install gmake GraphicsMagick pkgconf png tcl zlib
fi

if [ "$(uname)" = OpenBSD ]; then
    pkg_add -I gmake GraphicsMagick pkgconf png tcl%8.6
    ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh
fi


================================================
FILE: .gitignore
================================================
/attic/
/bin/
/hicolor
/hicolor.exe
/tests/*.hi*


================================================
FILE: AUTHORS
================================================
# The AUTHORS Certificate
# First edition, Fourteenth draft
#
# By proposing a change to this project that adds a line like
#
#     Name <E-Mail> (URL) [Working For]
#
# below, you certify:
#
# 1. All of your contributions to this project are and will be your own,
#    original work, licensed on the same terms as this project.
#
# 2. If someone else might own intellectual property in your
#    contributions, like an employer or client, you've added their legal
#    name in square brackets and got their written permission to submit
#    your contribution.
#
# 3. If you haven't added a name in square brackets, you are sure that
#    you have the legal right to license all your contributions so far
#    by yourself.
#
# 4. If you make any future contribution under different intellectual
#    property circumstances, you'll propose a change to add another line
#    to this file for that contribution.
#
# 5. The name, e-mail, and URL you've added to this file are yours. You
#    understand the project will make this file public.
D. Bohdan <see commit history> https://dbohdan.com/


================================================
FILE: GNUmakefile
================================================
PLATFORM ?= $(shell uname)

ifneq ($(PLATFORM), Darwin)
    PLATFORM_CFLAGS ?= -static -Wl,--gc-sections
endif

LIBPNG_CFLAGS ?= $(shell pkg-config --cflags libpng)
LIBPNG_LIBS ?= $(shell pkg-config --libs libpng)
ZLIB_CFLAGS ?= $(shell pkg-config --cflags zlib)
ZLIB_LIBS ?= $(shell pkg-config --libs zlib)

CFLAGS ?= -std=c99 -g -O3 $(PLATFORM_CFLAGS) -ffunction-sections -fdata-sections -Wall -Wextra $(LIBPNG_CFLAGS) $(ZLIB_CFLAGS)
LIBS ?= $(LIBPNG_LIBS) $(ZLIB_LIBS) -lm
PREFIX ?= /usr/local

all: hicolor

hicolor: cli.c hicolor.h
	$(CC) $< -o $@ $(CFLAGS) $(LIBS)

clean: clean-no-ext clean-exe
clean-exe:
	-rm -f hicolor.exe
clean-no-ext:
	-rm -f hicolor

install: install-bin install-include
install-bin: hicolor
	install $< $(DESTDIR)$(PREFIX)/bin/hicolor
install-include: hicolor.h
	install -m 0644 $< $(DESTDIR)$(PREFIX)/include

uninstall: uninstall-bin uninstall-include
uninstall-bin:
	-rm $(DESTDIR)$(PREFIX)/bin/hicolor
uninstall-include:
	-rm $(DESTDIR)$(PREFIX)/include/hicolor.h

release: clean-no-ext test
	cp hicolor hicolor-v"$$(./hicolor version | head -n 1 | awk '{ print $$2 }')"-"$$(uname | tr 'A-Z' 'a-z')"-"$$(uname -m)"

test: all
	tests/hicolor.test

.PHONY: all clean clean-exe clean-no-ext install install-bin install-include release test uninstall uninstall-bin uninstall-include


================================================
FILE: LICENSE
================================================
Copyright (c) 2021, 2023-2025 D. Bohdan and contributors listed in AUTHORS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# HiColor

![A building with a dithered gradient of the sky behind it.
A jet airplane is taking off in the sky.](bordeaux-15bit.png)

*(The image above has 15-bit color.)*

HiColor is a program and a C library for converting images to 15- and 16-bit RGB color,
the color depth of old display modes known as [&ldquo;high color&rdquo;](https://en.wikipedia.org/wiki/High_color).
I wrote it because I wanted to create images with the characteristic high-color look.

## Contents

- [Description](#description)
- [Known bugs and limitations](#known-bugs-and-limitations)
- [Usage](#usage)
- [Building](#building)
- [Alternatives](#alternatives)
- [License](#license)

## Description

HiColor reduces images to two-byte 15- or 16-bit color.
In 15-bit mode images have 5 bits for each of red, green, and blue, with the last bit reserved.
In 16-bit mode green, the color the human eye is generally most sensitive to, is given 6 bits.

HiColor implements its own simple [file format](format.md) and converts between this format and PNG.
It can also convert standard PNG to standard PNG with only high-color color values.
(This simulates a roundtrip through HiColor without creating a temporary file.)
HiColor files have either the extension `.hic` or `.hi5` for 15-bit and `.hi6` for 16-bit respectively.

By default,
HiColor applies the [Bayer ordered dithering](https://en.wikipedia.org/wiki/Ordered_dithering) algorithm
to reduce the quantization error
(the difference between the original and the high-color pixel).
Historical software and hardware used it for dithering in high-color modes.
HiColor can also use [&ldquo;a dither&rdquo;](https://pippin.gimp.org/a_dither/) instead.
Dithering can be selected or disabled with command-line flags.

Quantized images compress better than their originals,
so HiColor can be a less-lossy alternative to the 256-color [pngquant](https://pngquant.org/).
Quantizing a PNG file to PNG preserves transparency (but does not quantize the alpha channel).
Conversion to and from the HiColor format does not preserve transparency.

The program is written in C with two external dependencies, libpng and zlib, and builds as a static binary.
It is known to work on
Linux (aarch64, i386, riscv64, x86_64),
FreeBSD,
NetBSD,
OpenBSD,
and Windows 98 Second Edition,
2000 Service Pack 4,
XP,
7,
and 10.

The library is a single C99 header file.
It is designed to be easy to understand and modify
at a cost to performance.
The design makes it unsuitable for real-time graphics.

## Known bugs and limitations

### Security

The command-line program (but not the library) was vulnerable to malicious PNG files
because it used a PNG library intended only for trusted input.
The vulnerabilities were fixed in version 0.6.0 by switching to libpng.

### PNG file size

PNG files produced by HiColor are not highly optimized.
Run them through [OptiPNG](http://optipng.sourceforge.net/) or [Oxipng](https://github.com/shssoichiro/oxipng) to significantly reduce their size.

### Generation loss

With Bayer dithering or no dithering, there is no [generation loss](https://en.wikipedia.org/wiki/Generation_loss) after the initial quantization.
Applying &ldquo;a dither&rdquo; repeatedly to the same image will result in generation loss.
In tests the loss converges to zero after 32 or 64 generations
(in 15-bit and 16-bit mode respectively).

HiColor 0.1.0&ndash;0.2.1 suffered from generation loss with Bayer dithering due to an implementation error.
The error was fixed in version 0.3.0.

## Usage

HiColor has a Git-style CLI.

The actions `encode` and `decode` convert images between PNG and HiColor's own image format.
`quantize` round-trips an image through the converter and outputs a standard 32-bit PNG.
Use `quantize` to create high-color images readable by other programs.
`info` prints information about a HiColor file: version (`5` for 15-bit or `6` for 16), width, and height.

```none
HiColor 1.0.1
Create 15/16-bit color RGB images.

usage:
  hicolor (encode|quantize) [-5|-6] [-a|-b|-n] [--] <src> [<dest>]
  hicolor decode <src> [<dest>]
  hicolor info <file>
  hicolor (version|help|-h|--help)

commands:
  encode           convert PNG to HiColor
  decode           convert HiColor to PNG
  quantize         quantize PNG to PNG
  info             print HiColor image version and resolution
  version          print version of HiColor, libpng, and zlib
  help             print this help message

options:
  -5, --15-bit     15-bit color
  -6, --16-bit     16-bit color
  -a, --a-dither   dither image with "a dither"
  -b, --bayer      dither image with Bayer algorithm (default)
  -n, --no-dither  do not dither image
```

## Building

### Debian/Ubuntu

```sh
sudo apt install -y build-essential graphicsmagick linpng-dev pkgconf tclsh zlib1g-dev
gmake test
```

### FreeBSD

```sh
sudo pkg install -y gmake GraphicsMagick pkgconf png tcl86
ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh
fi
gmake test
```

### macOS

Install [Homebrew](https://brew.sh/).
Run the following commands in a clone of the HiColor repository.

```sh
brew install libpng tcl-tk
make test
```

### NetBSD

```sh
sudo pkgin -y install gmake GraphicsMagick pkgconf png tcl zlib
gmake test
```

### OpenBSD

```sh
doas pkg_add -I gmake GraphicsMagick pkgconf png tcl%8.6
ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh
gmake test
```

### Windows

Install [MSYS2](https://www.msys2.org/).
Run the following commands in the MSYS2 mingw32 shell
in a clone of the HiColor repository.
This will build an x86 executable for Windows.

```sh
pacman -Syuu make mingw-w64-i686-gcc mingw-w64-i686-libpng mingw-w64-i686-pkgconf mingw-w64-i686-zlib tcl
make test
```

## Alternatives

I wrote HiColor because nothing seemed to support high color.
Actually,
[FFmpeg](https://www.madox.net/blog/2011/06/06/converting-tofrom-rgb565-in-ubuntu-using-ffmpeg/),
[GIMP](https://docs.gimp.org/2.10/en/gimp-filter-dither.html),
and
[ImageMagick](https://www.imagemagick.org/Usage/quantize/#16bit_colormap)
can reduce images to 15- and 16-bit color.
What differentiates HiColor is being a small dedicated tool and embeddable C library and having its own file format.

## License

MIT.

HiColor uses [libpng](http://www.libpng.org/pub/png/libpng.html) and [zlib](https://www.zlib.net/).
Follow the links for their respective licenses.

### Photos from Unsplash

[&ldquo;plane in flight&rdquo;](https://unsplash.com/photos/AwtncJT1qKs) (`bordeaux-15bit.png`) by olaf wisser.

[&ldquo;houses beside trees&rdquo;](https://unsplash.com/photos/PWBXQJ7PUkI) (`tests/photo.png`) by Orlova Maria.

#### License

> Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license to download, copy, modify, distribute, perform, and use photos from Unsplash for free, including for commercial purposes, without permission from or attributing the photographer or Unsplash. This license does not include the right to compile photos from Unsplash to replicate a similar or competing service.


================================================
FILE: cli.c
================================================
/* HiColor CLI.
 *
 * Copyright (c) 2021, 2023-2024 D. Bohdan and contributors listed in AUTHORS.
 * License: MIT.
 */

#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <png.h>
#include <zlib.h>

#define HICOLOR_IMPLEMENTATION
#include "hicolor.h"

#define HICOLOR_CLI_ERROR "error: "
#define HICOLOR_CLI_LIB_NAME_FORMAT "%-9s"
#define HICOLOR_CLI_LIBPNG_COMPRESSION_LEVEL 6
#define HICOLOR_CLI_NO_MEMORY_EXIT_CODE 255

#define HICOLOR_CLI_CMD_ENCODE "encode"
#define HICOLOR_CLI_CMD_QUANTIZE "quantize"
#define HICOLOR_CLI_CMD_DECODE "decode"
#define HICOLOR_CLI_CMD_INFO "info"
#define HICOLOR_CLI_CMD_VERSION "version"
#define HICOLOR_CLI_CMD_HELP "help"

const char* png_error_msg = "no error recorded";

void libpng_error_handler(
    png_structp png_ptr,
    png_const_charp error_msg
)
{
    png_error_msg = error_msg;
    longjmp(png_jmpbuf(png_ptr), 1);
}

bool load_png(
    const char* filename,
    int* width,
    int* height,
    hicolor_rgb** rgb_img,
    uint8_t** alpha
)
{
    FILE* fp = fopen(filename, "rb");
    if (!fp) {
        png_error_msg = "failed to open for reading";
        return false;
    }

    png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, libpng_error_handler, NULL);
    if (png == NULL) {
        png_error_msg = "`png_create_read_struct` returned null";
        fclose(fp);
        return false;
    }

    png_infop info = png_create_info_struct(png);
    if (info == NULL) {
        png_error_msg = "`png_create_info_struct` returned null";
        png_destroy_read_struct(&png, NULL, NULL);
        fclose(fp);
        return false;
    }

    if (setjmp(png_jmpbuf(png))) {
        /* Do not overwrite `png_error_msg` set by the handler. */
        png_destroy_read_struct(&png, &info, NULL);
        fclose(fp);
        return false;
    }

    png_init_io(png, fp);
    png_read_info(png, info);

    *width = png_get_image_width(png, info);
    *height = png_get_image_height(png, info);
    png_byte color_type = png_get_color_type(png, info);
    png_byte bit_depth = png_get_bit_depth(png, info);

    if (bit_depth == 16) {
        png_set_strip_16(png);
    }

    if (color_type == PNG_COLOR_TYPE_PALETTE) {
        png_set_palette_to_rgb(png);
    }

    if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {
        png_set_expand_gray_1_2_4_to_8(png);
    }

    if (png_get_valid(png, info, PNG_INFO_tRNS)) {
        png_set_tRNS_to_alpha(png);
    }

    if (color_type == PNG_COLOR_TYPE_RGB
        || color_type == PNG_COLOR_TYPE_GRAY
        || color_type == PNG_COLOR_TYPE_PALETTE) {
        png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
    }

    if (color_type == PNG_COLOR_TYPE_GRAY
        || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {
        png_set_gray_to_rgb(png);
    }

    png_read_update_info(png, info);

    *rgb_img = malloc(sizeof(hicolor_rgb) * *width * *height);
    *alpha = malloc(sizeof(uint8_t) * *width * *height);

    if (*rgb_img == NULL || *alpha == NULL) {
        png_error_msg = "failed to allocate memory for `rgb_img` or `alpha`";
        png_destroy_read_struct(&png, &info, NULL);
        fclose(fp);
        return false;
    }

    png_bytep row = malloc(png_get_rowbytes(png, info));
    if (row == NULL) {
        png_error_msg = "failed to allocate memory for `row`";
        free(*rgb_img);
        free(*alpha);
        png_destroy_read_struct(&png, &info, NULL);
        fclose(fp);
        return false;
    }

    for (int y = 0; y < *height; y++) {
        png_read_row(png, row, NULL);

        for (int x = 0; x < *width; x++) {
            png_bytep pixel = &(row[x * 4]);
            (*rgb_img)[y * (*width) + x].r = pixel[0];
            (*rgb_img)[y * (*width) + x].g = pixel[1];
            (*rgb_img)[y * (*width) + x].b = pixel[2];
            (*alpha)[y * (*width) + x] = pixel[3];
        }
    }

    free(row);
    png_destroy_read_struct(&png, &info, NULL);
    fclose(fp);

    return true;
}

bool save_png(
    const char* filename,
    int width,
    int height,
    const hicolor_rgb* rgb_img,
    const uint8_t* alpha
)
{
    FILE* fp = fopen(filename, "wb");
    if (!fp) {
        png_error_msg = "failed to open for writing";
        return false;
    }

    png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, libpng_error_handler, NULL);
    if (png == NULL) {
        png_error_msg = "`png_create_write_struct` returned null";
        fclose(fp);
        return false;
    }

    png_infop info = png_create_info_struct(png);
    if (info == NULL) {
        png_error_msg = "`png_create_info_struct` returned null";
        png_destroy_write_struct(&png, NULL);
        fclose(fp);
        return false;
    }

    if (setjmp(png_jmpbuf(png))) {
        /* Do not overwrite `png_error_msg` set by the handler. */
        png_destroy_write_struct(&png, &info);
        fclose(fp);
        return false;
    }

    png_init_io(png, fp);

    png_set_IHDR(
        png,
        info,
        width,
        height,
        8,
        PNG_COLOR_TYPE_RGBA,
        PNG_INTERLACE_NONE,
        PNG_COMPRESSION_TYPE_DEFAULT,
        PNG_FILTER_TYPE_DEFAULT
    );
    png_set_compression_level(png, HICOLOR_CLI_LIBPNG_COMPRESSION_LEVEL);
    png_write_info(png, info);

    png_bytep row = malloc(png_get_rowbytes(png, info));
    if (row == NULL) {
        png_error_msg = "failed to allocate memory for `row`";
        png_destroy_write_struct(&png, &info);
        fclose(fp);
        return false;
    }

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            png_bytep pixel = &(row[x * 4]);
            pixel[0] = rgb_img[y * width + x].r;
            pixel[1] = rgb_img[y * width + x].g;
            pixel[2] = rgb_img[y * width + x].b;
            pixel[3] = alpha == NULL ? 255 : alpha[y * width + x];
        }

        png_write_row(png, row);
    }

    free(row);
    png_write_end(png, NULL);
    png_destroy_write_struct(&png, &info);
    fclose(fp);

    return true;
}

bool check_and_report_error(
    char* step,
    hicolor_result res
)
{
    if (res == HICOLOR_OK) {
        return false;
    }

    fprintf(
        stderr,
        HICOLOR_CLI_ERROR "%s: %s\n",
        step,
        hicolor_error_message(res)
    );

    return true;
}

bool check_src_exists(
    const char* src
)
{
    if (access(src, F_OK) != 0) {
        fprintf(
            stderr,
            HICOLOR_CLI_ERROR "source image \"%s\" doesn't exist\n",
            src
        );
        return false;
    }

    return true;
}

bool png_to_hicolor(
    hicolor_version version,
    hicolor_dither dither,
    const char* src,
    const char* dest
)
{
    hicolor_result res;

    bool exists = check_src_exists(src);
    if (!exists) {
        return false;
    }

    int width, height;
    hicolor_rgb* rgb_img = NULL;
    uint8_t* alpha = NULL;
    if (!load_png(src, &width, &height, &rgb_img, &alpha)) {
        fprintf(
            stderr,
            HICOLOR_CLI_ERROR "can't load PNG file \"%s\": %s\n",
            src,
            png_error_msg
        );
        return false;
    }

    FILE* hi_file = fopen(dest, "wb");
    if (hi_file == NULL) {
        fprintf(
            stderr,
            HICOLOR_CLI_ERROR "can't open file \"%s\" for writing\n",
            dest
        );

        free(rgb_img);
        return false;
    }

    hicolor_metadata meta = {
        .version = version,
        .width = width,
        .height = height
    };
    res = hicolor_write_header(hi_file, meta);

    bool success = false;
    if (check_and_report_error("can't write header", res)) {
        goto clean_up_file;
    }

    res = hicolor_quantize_rgb_image(meta, dither, rgb_img);
    if (check_and_report_error("can't quantize image", res)) {
        goto clean_up_images;
    }

    res = hicolor_write_rgb_image(hi_file, meta, rgb_img);
    if (check_and_report_error("can't write image data", res)) {
        goto clean_up_images;
    }

    success = true;

clean_up_images:
    free(rgb_img);

clean_up_file:
    fclose(hi_file);

    return success;
}

bool png_quantize(
    hicolor_version version,
    hicolor_dither dither,
    const char* src,
    const char* dest
)
{
    hicolor_result res;

    bool exists = check_src_exists(src);
    if (!exists) {
        return false;
    }

    int width, height;
    hicolor_rgb* rgb_img = NULL;
    uint8_t* alpha = NULL;
    if (!load_png(src, &width, &height, &rgb_img, &alpha)) {
        fprintf(
            stderr,
            HICOLOR_CLI_ERROR "can't load PNG file \"%s\": %s\n",
            src,
            png_error_msg
        );
        return false;
    }

    hicolor_metadata meta = {
        .version = version,
        .width = width,
        .height = height
    };

    res = hicolor_quantize_rgb_image(meta, dither, rgb_img);
    bool success = false;
    if (check_and_report_error("can't quantize image", res)) {
        goto clean_up_images;
    }

    if (!save_png(dest, width, height, rgb_img, alpha)) {
        fprintf(
            stderr,
            HICOLOR_CLI_ERROR "can't save PNG: %s\n",
            png_error_msg
        );
        goto clean_up_images;
    }

    success = true;

clean_up_images:
    free(rgb_img);

    return success;
}

bool hicolor_to_png(
    const char* src,
    const char* dest
)
{
    hicolor_result res;

    bool exists = check_src_exists(src);
    if (!exists) {
        return false;
    }

    FILE* hi_file = fopen(src, "rb");
    if (hi_file == NULL) {
        fprintf(stderr, HICOLOR_CLI_ERROR "can't open source image \"%s\" for reading\n", src);
        return false;
    }

    hicolor_metadata meta;
    res = hicolor_read_header(hi_file, &meta);
    bool success = false;
    if (check_and_report_error("can't read header", res)) {
        goto clean_up_file;
    }

    hicolor_rgb* rgb_img = malloc(sizeof(hicolor_rgb) * meta.width * meta.height);
    if (rgb_img == NULL) {
        goto clean_up_file;
    }
    res = hicolor_read_rgb_image(hi_file, meta, rgb_img);
    if (check_and_report_error("can't read image data", res)) {
        goto clean_up_rgb_img;
    }

    if (!save_png(dest, meta.width, meta.height, rgb_img, NULL)) {
        fprintf(
            stderr,
            HICOLOR_CLI_ERROR "can't save PNG: %s\n",
            png_error_msg
        );
        goto clean_up_rgb_img;
    }

    success = true;

clean_up_rgb_img:
    free(rgb_img);

clean_up_file:
    fclose(hi_file);

    return success;
}

bool hicolor_print_info(
    const char* src
)
{
    hicolor_result res;

    bool exists = check_src_exists(src);
    if (!exists) {
        return false;
    }

    FILE* hi_file = fopen(src, "rb");
    if (hi_file == NULL) {
        fprintf(
            stderr,
            HICOLOR_CLI_ERROR "can't open source image \"%s\" for reading\n",
            src
        );
        return false;
    }

    hicolor_metadata meta;
    res = hicolor_read_header(hi_file, &meta);
    bool success = false;
    if (check_and_report_error("can't read header", res)) {
        goto clean_up_file;
    }

    uint8_t vch = '\0';
    res = hicolor_version_to_char(meta.version, &vch);
    if (check_and_report_error("can't decode version", res)) {
        goto clean_up_file;
    }

    printf(
        "%c %i %i\n",
        vch,
        meta.width,
        meta.height
    );

    success = true;

clean_up_file:
    fclose(hi_file);

    return success;
}

void usage(
    FILE* output
)
{
    fprintf(
        output,
        "usage:\n"
        "  hicolor (encode|quantize) [-5|-6] [-a|-b|-n] [--] <src> [<dest>]\n"
        "  hicolor decode <src> [<dest>]\n"
        "  hicolor info <file>\n"
        "  hicolor (version|help|-h|--help)\n"
    );
}

void version(
    bool full
)
{
    if (full) {
        printf(
            HICOLOR_CLI_LIB_NAME_FORMAT,
            "HiColor"
        );
    }

    uint32_t program_version = HICOLOR_LIBRARY_VERSION;
    printf(
        "%u.%u.%u\n",
        program_version / 10000,
        program_version % 10000 / 100,
        program_version % 100
    );

    if (!full) {
        return;
    }

    png_uint_32 libpng_version = png_access_version_number();
    printf(
        HICOLOR_CLI_LIB_NAME_FORMAT "%u.%u.%u\n",
        "libpng",
        libpng_version / 10000,
        libpng_version % 10000 / 100,
        libpng_version % 100
    );

    printf(
        HICOLOR_CLI_LIB_NAME_FORMAT "%s\n",
        "zlib",
        ZLIB_VERSION
    );
}

void help()
{
    printf(
        "HiColor "
    );
    version(false);
    printf(
        "Create 15/16-bit color RGB images.\n\n"
    );
    usage(stdout);
    printf(
        "\ncommands:\n"
        "  encode           convert PNG to HiColor\n"
        "  decode           convert HiColor to PNG\n"
        "  quantize         quantize PNG to PNG\n"
        "  info             print HiColor image version and resolution\n"
        "  version          print version of HiColor, libpng, and zlib\n"
        "  help             print this help message\n"
        "\noptions:\n"
        "  -5, --15-bit     15-bit color\n"
        "  -6, --16-bit     16-bit color\n"
        "  -a, --a-dither   dither image with \"a dither\"\n"
        "  -b, --bayer      dither image with Bayer algorithm (default)\n"
        "  -n, --no-dither  do not dither image\n"
    );
}

bool str_prefix(
    const char* ref,
    const char* str
)
{
    size_t i;

    for (i = 0; str[i] != '\0'; i++) {
        if (str[i] != ref[i]) {
            return false;
        }
    }

    if (i == 0) {
        return false;
    }

    return true;
}

typedef enum command {
    ENCODE, DECODE, QUANTIZE, INFO, VERSION, HELP
} command;

int main(
    int argc,
    char** argv
)
{
    command opt_command = ENCODE;
    hicolor_dither opt_dither = HICOLOR_BAYER;
    hicolor_version opt_version = HICOLOR_VERSION_6;
    const char* command_name;
    char* arg_src;
    char* arg_dest;
    bool allow_opts = true;
    int min_pos_args = 1;
    int max_pos_args = 2;

    if (argc <= 1) {
        help();
        return 1;
    }

    /* The regular "help" command is handled later with the rest. */
    for (int i = 0; i < argc; i++) {
        if (strcmp(argv[i], "-h") == 0
            || strcmp(argv[i], "--help") == 0) {
            help();
            return 0;
        }
    }

    int i = 1;

    if (str_prefix(HICOLOR_CLI_CMD_ENCODE, argv[i])) {
        command_name = HICOLOR_CLI_CMD_ENCODE;
        opt_command = ENCODE;
    } else if (str_prefix(HICOLOR_CLI_CMD_DECODE, argv[i])) {
        allow_opts = false;
        command_name = HICOLOR_CLI_CMD_DECODE;
        opt_command = DECODE;
    } else if (str_prefix(HICOLOR_CLI_CMD_QUANTIZE, argv[i])) {
        command_name = HICOLOR_CLI_CMD_QUANTIZE;
        opt_command = QUANTIZE;
    } else if (str_prefix(HICOLOR_CLI_CMD_INFO, argv[i])) {
        allow_opts = false;
        command_name = HICOLOR_CLI_CMD_INFO;
        max_pos_args = 1;
        opt_command = INFO;
    } else if (str_prefix(HICOLOR_CLI_CMD_VERSION, argv[i])) {
        allow_opts = false;
        command_name = HICOLOR_CLI_CMD_VERSION;
        min_pos_args = 0;
        max_pos_args = 0;
        opt_command = VERSION;
    } else if (str_prefix(HICOLOR_CLI_CMD_HELP, argv[i])) {
        allow_opts = false;
        command_name = HICOLOR_CLI_CMD_HELP;
        min_pos_args = 0;
        max_pos_args = 0;
        opt_command = HELP;
    } else {
        usage(stderr);
        fprintf(
            stderr,
            "\n" HICOLOR_CLI_ERROR "unknown command \"%s\"\n",
            argv[i]
        );
        return 1;
    }

    i++;

    if (allow_opts) {
        while (i < argc && argv[i][0] == '-') {
            if (strcmp(argv[i], "--") == 0) {
                i++;
                break;
            } else if (strcmp(argv[i], "-5") == 0
                || strcmp(argv[i], "--15-bit") == 0) {
                opt_version = HICOLOR_VERSION_5;
            } else if (strcmp(argv[i], "-6") == 0
                || strcmp(argv[i], "--16-bit") == 0) {
                opt_version = HICOLOR_VERSION_6;
            } else if (strcmp(argv[i], "-a") == 0
                || strcmp(argv[i], "--a-dither") == 0) {
                opt_dither = HICOLOR_A_DITHER;
            } else if (strcmp(argv[i], "-b") == 0
                || strcmp(argv[i], "--bayer") == 0) {
                opt_dither = HICOLOR_BAYER;
            } else if (strcmp(argv[i], "-n") == 0
                || strcmp(argv[i], "--no-dither") == 0) {
                opt_dither = HICOLOR_NO_DITHER;
            } else {
                usage(stderr);
                fprintf(
                    stderr,
                    "\n" HICOLOR_CLI_ERROR "unknown option \"%s\"\n",
                    argv[i]
                );
                return 1;
            }

            i++;
        }
    }

    int rem_args = argc - i;

    if (rem_args < min_pos_args) {
        usage(stderr);
        fprintf(
            stderr,
            "\n" HICOLOR_CLI_ERROR "no source image given to command \"%s\"\n",
            command_name
        );
        return 1;
    }

    if (rem_args > max_pos_args) {
        usage(stderr);
        fprintf(
            stderr,
            "\n" HICOLOR_CLI_ERROR "too many arguments to command \"%s\"\n",
            command_name
        );
        return 1;
    }

    arg_src = argv[i];
    i++;

    if (i == argc) {
        arg_dest = malloc(strlen(arg_src) + 5);
        if (arg_dest == NULL) {
            return HICOLOR_CLI_NO_MEMORY_EXIT_CODE;
        }
        sprintf(
            arg_dest,
            opt_command == ENCODE ? "%s.hic" : "%s.png",
            arg_src
        );
    } else {
        arg_dest = argv[i];
    }
    i++;

    switch (opt_command) {
    case ENCODE:
        return !png_to_hicolor(opt_version, opt_dither, arg_src, arg_dest);
    case DECODE:
        return !hicolor_to_png(arg_src, arg_dest);
    case QUANTIZE:
        return !png_quantize(opt_version, opt_dither, arg_src, arg_dest);
    case INFO:
        return !hicolor_print_info(arg_src);
    case VERSION:
        version(true);
        return 0;
    case HELP:
        help();
        return 0;
    }
}


================================================
FILE: format.md
================================================
# File format

- Magic: 7 bytes, `HiColor`.
- Version: 1 byte, `5` for 15-bit color, `6` for 16-bit color.
  Other versions may be added later.
- Width: 2 bytes: WB1, WB2.
  Width = WB1 + 256×WB2.
- Height: 2 bytes: HB1, HB2.
  Height = HB1 + 256×HB2.
- Data: Width×Height×2 bytes.
  The Data consist of Width×Height Values.
  Each Value is 2 bytes: VB1, VB2.
  Value = VB1 + 256×VB2.

The Data part encodes the lines of pixels comprising the image from top to bottom and each line from left to right.
The first Value of the data is the top-left pixel, the next is the one to its right, etc.

## Values

- Version `5`:
    - 5 bits red, 5 bits green, 5 bits blue, 0.
- Version `6`:
    - 5 bits red, 6 bits green, 5 bits blue.


================================================
FILE: hicolor.h
================================================
/* HiColor image file format encoder/decoder library.
 *
 * Copyright (c) 2021, 2023-2025 D. Bohdan and contributors listed in AUTHORS.
 * License: MIT.
 *
 * This header file contains both the interface and the implementation for
 * HiColor. To instantiate the implementation put the line
 *     #define HICOLOR_IMPLEMENTATION
 * in a single source code file of your project above where you include this
 * file.
 */

#ifndef HICOLOR_H
#define HICOLOR_H

#include <inttypes.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>

#define HICOLOR_BAYER_SIZE 8
#define HICOLOR_LIBRARY_VERSION 10001

/* Types. */

static const uint8_t hicolor_magic[7] = "HiColor";

/* These arrays are generated with `scripts/conversion-tables.tcl`. */
static const uint8_t hicolor_256_to_32[] = {
    0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
    3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5,
    6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8,
    9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11,
    11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13,
    13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15,
    16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18,
    18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20,
    20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
    22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24,
    25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27,
    27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29,
    29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31,
    31, 31
};

static const uint8_t hicolor_256_to_64[] = {
    0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5,
    6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11,
    11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15,
    16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20,
    20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24,
    25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29,
    29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 32, 32, 32, 32, 33, 33, 33, 33,
    34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38,
    38, 38, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42,
    43, 43, 43, 43, 44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47,
    47, 47, 48, 48, 48, 48, 49, 49, 49, 49, 50, 50, 50, 50, 51, 51, 51, 51,
    52, 52, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 56, 56,
    56, 56, 57, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59, 60, 60, 60, 60,
    61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 63
};

static const uint8_t hicolor_32_to_256[] = {
    0, 8, 16, 24, 33, 41, 49, 57, 66, 74, 82, 90, 99, 107, 115, 123, 132,
    140, 148, 156, 165, 173, 181, 189, 198, 206, 214, 222, 231, 239, 247,
    255
};

static const uint8_t hicolor_64_to_256[] = {
    0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 65, 69, 73,
    77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 130, 134,
    138, 142, 146, 150, 154, 158, 162, 166, 170, 174, 178, 182, 186, 190,
    195, 199, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 243, 247,
    251, 255
};

/* The values in this array are the output of
 * `scripts/bayer-matrix.tcl`.
 */
static const double hicolor_bayer[HICOLOR_BAYER_SIZE * HICOLOR_BAYER_SIZE] = {
     0.0/64, 48.0/64, 12.0/64, 60.0/64,  3.0/64, 51.0/64, 15.0/64, 63.0/64,
    32.0/64, 16.0/64, 44.0/64, 28.0/64, 35.0/64, 19.0/64, 47.0/64, 31.0/64,
     8.0/64, 56.0/64,  4.0/64, 52.0/64, 11.0/64, 59.0/64,  7.0/64, 55.0/64,
    40.0/64, 24.0/64, 36.0/64, 20.0/64, 43.0/64, 27.0/64, 39.0/64, 23.0/64,
     2.0/64, 50.0/64, 14.0/64, 62.0/64,  1.0/64, 49.0/64, 13.0/64, 61.0/64,
    34.0/64, 18.0/64, 46.0/64, 30.0/64, 33.0/64, 17.0/64, 45.0/64, 29.0/64,
    10.0/64, 58.0/64,  6.0/64, 54.0/64,  9.0/64, 57.0/64,  5.0/64, 53.0/64,
    42.0/64, 26.0/64, 38.0/64, 22.0/64, 41.0/64, 25.0/64, 37.0/64, 21.0/64
};

typedef enum hicolor_version {
    HICOLOR_VERSION_5,
    HICOLOR_VERSION_6
} hicolor_version;

typedef struct hicolor_metadata {
    hicolor_version version;
    uint16_t width;
    uint16_t height;
} hicolor_metadata;

typedef enum hicolor_result {
    HICOLOR_OK,
    HICOLOR_IO_ERROR,
    HICOLOR_UNKNOWN_VERSION,
    HICOLOR_INVALID_VALUE,
    HICOLOR_INSUFFICIENT_DATA,
    HICOLOR_BAD_MAGIC
} hicolor_result;

typedef enum hicolor_dither {
    HICOLOR_A_DITHER,
    HICOLOR_BAYER,
    HICOLOR_NO_DITHER
} hicolor_dither;

typedef struct hicolor_rgb {
    uint8_t r;
    uint8_t g;
    uint8_t b;
} hicolor_rgb;

typedef uint16_t hicolor_value;

/* Functions. */

const char* hicolor_error_message(hicolor_result res);

hicolor_result hicolor_char_to_version(
    const uint8_t ch,
    hicolor_version* version
);
hicolor_result hicolor_version_to_char(
    const hicolor_version version,
    uint8_t* ch
);

hicolor_result hicolor_value_to_rgb(
    const hicolor_version version,
    const hicolor_value value,
    hicolor_rgb* rgb
);
hicolor_result hicolor_rgb_to_value(
    const hicolor_version version,
    const hicolor_rgb rgb,
    hicolor_value* value
);

hicolor_result hicolor_read_header(
    FILE* stream,
    hicolor_metadata* meta
);
hicolor_result hicolor_write_header(
    FILE* stream,
    const hicolor_metadata meta
);

/* Quantize using optional dithering. */
hicolor_result hicolor_quantize_rgb_image(
    const hicolor_metadata meta,
    hicolor_dither dither,
    hicolor_rgb* image
);

hicolor_result hicolor_read_rgb_image(
    FILE* stream,
    const hicolor_metadata meta,
    hicolor_rgb* image
);
hicolor_result hicolor_write_rgb_image(
    FILE* stream,
    const hicolor_metadata meta,
    const hicolor_rgb* image
);

#endif /* HICOLOR_H */

/* -------------------------------------------------------------------------- */

#ifdef HICOLOR_IMPLEMENTATION

const char* hicolor_error_message(hicolor_result res)
{
    switch (res) {
    case HICOLOR_OK:
        return "OK";
    case HICOLOR_IO_ERROR:
        return "I/O error";
    case HICOLOR_UNKNOWN_VERSION:
        return "unknown version";
    case HICOLOR_INVALID_VALUE:
        return "invalid value";
    case HICOLOR_INSUFFICIENT_DATA:
        return "insufficient data";
    case HICOLOR_BAD_MAGIC:
        return "bad magic value";
    default:
        return "";
    }
}

hicolor_result hicolor_char_to_version(
    const uint8_t ch,
    hicolor_version* version
)
{
    switch (ch) {
    case '5':
        *version = HICOLOR_VERSION_5;
        return HICOLOR_OK;
    case '6':
        *version = HICOLOR_VERSION_6;
        return HICOLOR_OK;
    default:
        return HICOLOR_UNKNOWN_VERSION;
    };
}


hicolor_result hicolor_version_to_char(
    const hicolor_version version,
    uint8_t* ch
)
{
    switch (version) {
    case HICOLOR_VERSION_5:
        *ch = '5';
        return HICOLOR_OK;
    case HICOLOR_VERSION_6:
        *ch = '6';
        return HICOLOR_OK;
    default:
        return HICOLOR_UNKNOWN_VERSION;
    };
}

hicolor_result hicolor_value_to_rgb(
    const hicolor_version version,
    const hicolor_value value,
    hicolor_rgb* rgb
)
{
    switch (version) {
    case HICOLOR_VERSION_5:
        if (value & 0x8000) return HICOLOR_INVALID_VALUE;
        rgb->r = hicolor_32_to_256[value & 0x1f];
        rgb->g = hicolor_32_to_256[(value & 0x3ff) >> 5];
        rgb->b = hicolor_32_to_256[(value & 0x7fff) >> 10];
        return HICOLOR_OK;
    case HICOLOR_VERSION_6:
        rgb->r = hicolor_32_to_256[value & 0x1f];
        rgb->g = hicolor_64_to_256[(value & 0x7ff) >> 5];
        rgb->b = hicolor_32_to_256[value >> 11];
        return HICOLOR_OK;
    default:
        return HICOLOR_UNKNOWN_VERSION;
    };
}

hicolor_result hicolor_rgb_to_value(
    const hicolor_version version,
    const hicolor_rgb rgb,
    hicolor_value* value
)
{
    switch (version) {
    case HICOLOR_VERSION_5:
        *value = hicolor_256_to_32[rgb.r]
            | hicolor_256_to_32[rgb.g] << 5
            | hicolor_256_to_32[rgb.b] << 10;
        return HICOLOR_OK;
    case HICOLOR_VERSION_6:
        *value = hicolor_256_to_32[rgb.r]
            | hicolor_256_to_64[rgb.g] << 5
            | hicolor_256_to_32[rgb.b] << 11;
        return HICOLOR_OK;
    default:
        return HICOLOR_UNKNOWN_VERSION;
    };
}

hicolor_result hicolor_read_header(
    FILE* stream,
    hicolor_metadata* meta
)
{
    size_t total = 0;
    hicolor_result res;

    uint8_t magic[7];
    total += fread(magic, 1, sizeof(magic), stream);
    if (memcmp(magic, hicolor_magic, sizeof(magic)) != 0) {
        return HICOLOR_BAD_MAGIC;
    }

    uint8_t vch;
    total += fread(&vch, 1, sizeof(vch), stream);
    res = hicolor_char_to_version(vch, &meta->version);
    if (res != HICOLOR_OK) {
        return res;
    }

    uint8_t b[2];
    total += fread(&b, 1, sizeof(b), stream);
    meta->width = b[0] + (b[1] << 8);
    total += fread(&b, 1, sizeof(b), stream);
    meta->height = b[0] + (b[1] << 8);

    if (total == 12) return HICOLOR_OK;

    return HICOLOR_INSUFFICIENT_DATA;
}

hicolor_result hicolor_write_header(
    FILE* stream,
    const hicolor_metadata meta
)
{
    size_t total = 0;

    total += fwrite(hicolor_magic, 1, sizeof(hicolor_magic), stream);

    uint8_t vch;
    hicolor_result res = hicolor_version_to_char(meta.version, &vch);
    if (res != HICOLOR_OK) return res;
    total += fwrite(&vch, 1, sizeof(vch), stream);

    uint8_t wb1 = meta.width & 0xff;
    uint8_t wb2 = (meta.width >> 8) & 0xff;
    total += fwrite(&wb1, 1, sizeof(wb1), stream);
    total += fwrite(&wb2, 1, sizeof(wb2), stream);

    uint8_t hb1 = meta.height & 0xff;
    uint8_t hb2 = (meta.height >> 8) & 0xff;
    total += fwrite(&hb1, 1, sizeof(hb1), stream);
    total += fwrite(&hb2, 1, sizeof(hb2), stream);

    if (total == 12) return HICOLOR_OK;

    return HICOLOR_IO_ERROR;
}

/* "a dither" is a public-domain dithering algorithm by Øyvind Kolås.
 * This function implements pattern 3.
 * https://pippin.gimp.org/a_dither/
 */
uint8_t hicolor_a_dither_channel(
    uint8_t intensity,
    uint16_t x,
    uint16_t y,
    double levels
)
{
    double mask = (double) ((x + y * 237) * 119 & 255) / 255.0;
    double normalized = (double) intensity / 255.0;
    double dithered_normalized = floor(levels * normalized + mask) / levels;
    if (dithered_normalized > 1) {
        dithered_normalized = 1;
    }

    uint8_t result = dithered_normalized * 255;
    return result;
}

void hicolor_a_dither_rgb(
    hicolor_version version,
    uint16_t x,
    uint16_t y,
    const hicolor_rgb rgb,
    hicolor_rgb* output
)
{
    double levels = 32.0;
    double levels_g = version == HICOLOR_VERSION_5 ? levels : 64.0;

    output->r = hicolor_a_dither_channel(rgb.r, x, y, levels);
    output->g = hicolor_a_dither_channel(rgb.g, x, y, levels_g);
    output->b = hicolor_a_dither_channel(rgb.b, x, y, levels);
}

/* Ordered (Bayer) dithering. */
uint8_t hicolor_bayerize_channel(
    uint8_t intensity,
    double factor,
    double step
)
{
    double dithered = ((double) intensity) / 255 + step / 256 * factor;

    double levels = 128.0 / step;
    return (uint8_t) (round(dithered * levels) / levels * 255);
}

void hicolor_bayerize_rgb(
    hicolor_version version,
    uint16_t x,
    uint16_t y,
    const hicolor_rgb rgb,
    hicolor_rgb* output
)
{
    uint8_t bayer_coord =
        (y % HICOLOR_BAYER_SIZE) * HICOLOR_BAYER_SIZE +
        x % HICOLOR_BAYER_SIZE;
    double factor = hicolor_bayer[bayer_coord];

    double step = 8.0;
    double step_g = version == HICOLOR_VERSION_5 ? step : 4.0;

    output->r = hicolor_bayerize_channel(rgb.r, factor, step);
    output->g = hicolor_bayerize_channel(rgb.g, factor, step_g);
    output->b = hicolor_bayerize_channel(rgb.b, factor, step);
}

hicolor_result hicolor_quantize_rgb_image(
    const hicolor_metadata meta,
    hicolor_dither dither,
    hicolor_rgb* image
)
{
    hicolor_rgb rgb;
    hicolor_value value;

    for (uint16_t y = 0; y < meta.height; y++) {
        for (uint16_t x = 0; x < meta.width; x++) {
            rgb = image[y * meta.width + x];

            hicolor_rgb quant_rgb = rgb;
            if (dither == HICOLOR_A_DITHER) {
                hicolor_a_dither_rgb(meta.version, x, y, rgb, &quant_rgb);
            } else if (dither == HICOLOR_BAYER) {
                hicolor_bayerize_rgb(meta.version, x, y, rgb, &quant_rgb);
            }

            hicolor_result res = hicolor_rgb_to_value(
                meta.version,
                quant_rgb,
                &value
            );
            if (res != HICOLOR_OK) {
                return res;
            }

            res = hicolor_value_to_rgb(
                meta.version,
                value,
                &image[y * meta.width + x]
            );
            if (res != HICOLOR_OK) {
                return res;
            }
        }
    }

    return HICOLOR_OK;
};

hicolor_result hicolor_read_rgb_image(
    FILE* stream,
    const hicolor_metadata meta,
    hicolor_rgb* image
)
{
    size_t total = 0;

    for (int i = 0; i < meta.width * meta.height; i++) {
        hicolor_value value;
        total += fread(&value, 1, sizeof(value), stream);

        hicolor_result res =
            hicolor_value_to_rgb(meta.version, value, &image[i]);
        if (res != HICOLOR_OK) return res;
    }

    if (total == 2 * meta.width * meta.height) return HICOLOR_OK;

    return HICOLOR_INSUFFICIENT_DATA;
}

hicolor_result hicolor_write_rgb_image(
    FILE* stream,
    const hicolor_metadata meta,
    const hicolor_rgb* image
)
{
    size_t total = 0;

    for (int i = 0; i < meta.width * meta.height; i++) {
        hicolor_value value;
        hicolor_result res =
            hicolor_rgb_to_value(meta.version, image[i], &value);
        if (res != HICOLOR_OK) return res;

        total += fwrite(&value, 1, sizeof(value), stream);
    }

    if (total == 2 * meta.width * meta.height) return HICOLOR_OK;

    return HICOLOR_IO_ERROR;
}

#endif /* HICOLOR_IMPLEMENTATION */


================================================
FILE: scripts/bayer-matrix.tcl
================================================
#! /usr/bin/env tclsh

set bm8 {
     0 48 12 60  3 51 15 63
    32 16 44 28 35 19 47 31
     8 56  4 52 11 59  7 55
    40 24 36 20 43 27 39 23
     2 50 14 62  1 49 13 61
    34 18 46 30 33 17 45 29
    10 58  6 54  9 57  5 53
    42 26 38 22 41 25 37 21
}

set n 8
set size [expr { $n * $n }]

set fmt [lmap x $bm8 {
    format %2i.0/%u $x $size
}]

for {set i 0} {$i < $size} {incr i $n} {
    lappend lines [join [lrange $fmt $i [expr { $i + $n - 1 }]] {, }]
}

puts "\n    [join $lines ",\n    "]"


================================================
FILE: scripts/conversion-tables.tcl
================================================
#! /usr/bin/env tclsh

package require textutil

set varDeclTemplate {static const uint8_t %s[] = {%s};}

proc table {from to} {
    set ratio [expr { $to * 1.0 / $from }]

    for {set i 0} {$i < $from} {incr i} {
        lappend table [expr { int($i * 1.0 / $from * ($to + $ratio) ) }]
    }

    return $table
}

proc format-table {name table} {
    set lines [textutil::adjust [join $table {, }]]
    set indented [join [split $lines \n] "\n    "]

    return [format $::varDeclTemplate $name "\n    $indented\n"]
}

foreach {from to} {256 32 256 64 32 256 64 256} {
    dict set tables hicolor_${from}_to_$to [table $from $to]
}

puts [join [lmap {key value} $tables {
    format-table $key $value
}] \n\n]


================================================
FILE: tests/hicolor.test
================================================
#! /usr/bin/env tclsh

package require Tcl 8.6 9
package require tcltest

cd [file dirname [info script]]

set hicolorCommand ../hicolor
if {[info exists env(HICOLOR_COMMAND)]} {
    set hicolorCommand $env(HICOLOR_COMMAND)
}

try {
    exec gm version
} on ok _ {
    tcltest::testConstraint gm true
} on error _ {}


proc hicolor args {
    exec {*}$::hicolorCommand {*}$args
}

proc read-file path {
    try {
        set ch [open $path rb]
        read $ch
    } finally {
        close $ch
    }
}

proc prefixes s {
    for {set i 0} {$i < [string length $s]} {incr i} {
        lappend prefixes [string range $s 0 $i]
    }

    return $prefixes
}


tcltest::test version-1.1 {} -body {
    hicolor version
} -match regexp -result {\d+\.\d+\.\d+}

tcltest::test version-1.2 {} -body {
    hicolor version file.hi5
} -returnCodes error -match glob -result {*too many arg*}

tcltest::test version-2.1 {} -body {
    set action version
    set output [hicolor $action]

    lmap prefix [prefixes $action] {
        expr {
            [hicolor $prefix] eq $output
        }
    }
} -match regexp -result {1(?: 1)+}


tcltest::test help-1.1 {} -body {
    hicolor help
} -match glob -result *options:*


tcltest::test help-1.2 {} -body {
    hicolor -h
} -match glob -result *options:*


tcltest::test help-1.3 {} -body {
    hicolor --help
} -match glob -result *options:*


tcltest::test encode-1.1 {} -body {
    hicolor encode
} -returnCodes error -match glob -result {*no source image given to command\
    "encode"*}

tcltest::test encode-1.2 {} -body {
    hicolor encode photo.png
} -result {}

tcltest::test encode-1.3 {} -body {
    hicolor encode photo.png photo.png.hic
} -result {}

tcltest::test encode-1.4 {} -body {
    hicolor encode photo.png photo.png.hic ascii.txt 2>@1
} -returnCodes error -match glob -result {*too many arg*}

tcltest::test encode-1.5 {} -body {
    hicolor encode photo.png photo.png.hic ascii.txt foo bar baz 2>@1
} -returnCodes error -match glob -result {*too many arg*}

tcltest::test encode-1.6 {} -body {
    hicolor encode [file tail [info script]]
} -returnCodes error -match glob -result {*Not a PNG file*}


tcltest::test encode-2.1 {encode flags} -body {
    hicolor encode -5 photo.png photo.png.hic
    hicolor info photo.png.hic
} -result {5 640 427}

tcltest::test encode-2.2 {encode flags} -body {
    hicolor encode --15-bit photo.png photo.png.hic
    hicolor info photo.png.hic
} -result {5 640 427}

tcltest::test encode-2.3 {encode flags} -body {
    hicolor encode -6 photo.png photo.png.hic
    hicolor info photo.png.hic
} -result {6 640 427}

tcltest::test encode-2.4 {encode flags} -body {
    hicolor encode --16-bit photo.png photo.png.hic
    hicolor info photo.png.hic
} -result {6 640 427}

tcltest::test encode-2.5 {encode flags} -body {
    hicolor encode --16-bit photo.png
    hicolor info photo.png.hic
} -result {6 640 427}

tcltest::test encode-2.6 {encode flags} -body {
    hicolor encode -5 -6 -5 photo.png
    hicolor info photo.png.hic
} -result {5 640 427}

tcltest::test encode-2.7 {encode flags} -body {
    hicolor encode -n -n -n -n -n photo.png
} -result {}

tcltest::test encode-2.8 {encode flags} -body {
    hicolor encode -6 -n -5 photo.png
    hicolor info photo.png.hic
} -result {5 640 427}

tcltest::test encode-2.9 {} -body {
    hicolor encode -b -a -n -a -n -a photo.png
} -result {}


tcltest::test encode-3.1 {bad input} -body {
    hicolor encode truncated.png
} -returnCodes error -result {error: can't load PNG file "truncated.png":\
    Read Error}

tcltest::test encode-3.2 {bad input} -body {
    hicolor encode wrong-size.png
} -returnCodes error -match glob -result {error: can't load PNG file*}


hicolor encode --15-bit photo.png photo.hi5
hicolor encode --15-bit --a-dither photo.png photo-a-dither.hi5
hicolor encode --16-bit photo.png photo.hi6
hicolor encode --16-bit --a-dither photo.png photo-a-dither.hi6


tcltest::test decode-1.1 {15-bit} -body {
    hicolor decode photo.hi5
    file exists photo.hi5.png
} -result 1

tcltest::test decode-1.2 {16-bit} -body {
    hicolor decode photo.hi6
    file exists photo.hi6.png
} -result 1

tcltest::test decode-1.3 {bad input} -body {
    hicolor decode [file tail [info script]]
} -returnCodes error -match glob -result {*bad magic*}

tcltest::test decode-1.4 {bad input} -body {
    hicolor decode -5 photo.hi5
} -returnCodes error -match glob -result *error:*


tcltest::test quantize-1.1 {} -body {
    hicolor quantize photo.png photo.16-bit.png
} -result {}

tcltest::test quantize-1.2 {} -body {
    hicolor quantize -5 photo.png photo.15-bit.png
} -result {}


tcltest::test quantize-2.1 {bad input} -body {
    hicolor encode [file tail [info script]]
} -returnCodes error -match glob -result {*Not a PNG file*}

tcltest::test quantize-2.2 {bad input} -body {
    hicolor quantize truncated.png
} -returnCodes error -result {error: can't load PNG file "truncated.png":\
    Read Error}

tcltest::test quantize-2.3 {bad input} -body {
    hicolor quantize wrong-size.png
} -returnCodes error -match glob -result {error: can't load PNG file*}


tcltest::test unknown-command-1.1 {} -body {
    hicolor -5 src.png
} -returnCodes error -match glob -result {usage:*error: unknown command "-5"}

tcltest::test unknown-command-1.2 {} -body {
    hicolor encoder
} -returnCodes error -match glob -result {usage:*error: unknown command "encoder"}


tcltest::test unknown-option-1.1 {} -body {
    hicolor encode --wrong fo bar
} -returnCodes error -match glob -result {usage:*error: unknown option "--wrong"}


tcltest::test no-arguments-1.1 {} -body {
    hicolor
} -returnCodes error -match glob -result *options:*


tcltest::test dash-dash-1.1 {} -body {
    hicolor encode -- --no-such-file
} -returnCodes error -match glob -result {error: source image "--no-such-file"\
    doesn't exist}


tcltest::test data-integrity-1.1 {roundtrip} -constraints gm -body {
    hicolor decode photo.hi5 temp.png
    exec gm compare -metric rmse photo.png temp.png
} -match regexp -result {Total: 0.0[12]}

tcltest::test data-integrity-1.2 {roundtrip} -constraints gm -body {
    hicolor decode photo.hi6 temp.png
    exec gm compare -metric rmse photo.png temp.png
} -match regexp -result {Total: 0.0[12]}

tcltest::test data-integrity-2.1 {roundtrip with "a dither"} -constraints gm -body {
    hicolor decode photo-a-dither.hi5 temp.png
    exec gm compare -metric rmse photo.png temp.png
} -match regexp -result {Total: 0.0[12]}

tcltest::test data-integrity-2.2 {roundtrip with "a dither"} -constraints gm -body {
    hicolor decode photo-a-dither.hi6 temp.png
    exec gm compare -metric rmse photo.png temp.png
} -match regexp -result {Total: 0.0[12]}

tcltest::test data-integrity-3.1 {alpha roundtrip} -constraints gm -body {
    hicolor quant alpha.png alpha-q.png
    exec gm compare -metric rmse alpha.png alpha-q.png
} -match regexp -result {Total: 0.0+ }


incr failed [expr {$tcltest::numTests(Failed) > 0}]
tcltest::cleanupTests

if {$failed > 0} {
    exit 1
}
Download .txt
gitextract_as8h71n0/

├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── install-deps.sh
├── .gitignore
├── AUTHORS
├── GNUmakefile
├── LICENSE
├── README.md
├── cli.c
├── format.md
├── hicolor.h
├── scripts/
│   ├── bayer-matrix.tcl
│   └── conversion-tables.tcl
└── tests/
    └── hicolor.test
Download .txt
SYMBOL INDEX (34 symbols across 2 files)

FILE: cli.c
  function libpng_error_handler (line 31) | void libpng_error_handler(
  function load_png (line 40) | bool load_png(
  function save_png (line 152) | bool save_png(
  function check_and_report_error (line 232) | bool check_and_report_error(
  function check_src_exists (line 251) | bool check_src_exists(
  function png_to_hicolor (line 267) | bool png_to_hicolor(
  function png_quantize (line 339) | bool png_quantize(
  function hicolor_to_png (line 395) | bool hicolor_to_png(
  function hicolor_print_info (line 449) | bool hicolor_print_info(
  function usage (line 498) | void usage(
  function version (line 512) | void version(
  function help (line 551) | void help()
  function str_prefix (line 578) | bool str_prefix(
  type command (line 598) | typedef enum command {
  function main (line 602) | int main(

FILE: hicolor.h
  type hicolor_version (line 91) | typedef enum hicolor_version {
  type hicolor_metadata (line 96) | typedef struct hicolor_metadata {
  type hicolor_result (line 102) | typedef enum hicolor_result {
  type hicolor_dither (line 111) | typedef enum hicolor_dither {
  type hicolor_rgb (line 117) | typedef struct hicolor_rgb {
  type hicolor_value (line 123) | typedef uint16_t hicolor_value;
  function hicolor_result (line 202) | hicolor_result hicolor_char_to_version(
  function hicolor_result (line 220) | hicolor_result hicolor_version_to_char(
  function hicolor_result (line 237) | hicolor_result hicolor_value_to_rgb(
  function hicolor_result (line 260) | hicolor_result hicolor_rgb_to_value(
  function hicolor_result (line 282) | hicolor_result hicolor_read_header(
  function hicolor_result (line 314) | hicolor_result hicolor_write_header(
  function hicolor_a_dither_channel (line 347) | uint8_t hicolor_a_dither_channel(
  function hicolor_a_dither_rgb (line 365) | void hicolor_a_dither_rgb(
  function hicolor_bayerize_channel (line 382) | uint8_t hicolor_bayerize_channel(
  function hicolor_bayerize_rgb (line 394) | void hicolor_bayerize_rgb(
  function hicolor_result (line 415) | hicolor_result hicolor_quantize_rgb_image(
  function hicolor_result (line 458) | hicolor_result hicolor_read_rgb_image(
  function hicolor_result (line 480) | hicolor_result hicolor_write_rgb_image(
Condensed preview — 13 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (58K chars).
[
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2731,
    "preview": "name: CI\non: [push, pull_request]\njobs:\n  bsd:\n    runs-on: ${{ matrix.os.host }}\n    strategy:\n      matrix:\n        os"
  },
  {
    "path": ".github/workflows/install-deps.sh",
    "chars": 585,
    "preview": "#! /bin/sh\nset -e\n\nif [ \"$(uname)\" = Darwin ]; then\n    brew install dylibbundler tcl-tk\nfi\n\nif [ \"$(uname)\" = Linux ]; "
  },
  {
    "path": ".gitignore",
    "chars": 49,
    "preview": "/attic/\n/bin/\n/hicolor\n/hicolor.exe\n/tests/*.hi*\n"
  },
  {
    "path": "AUTHORS",
    "chars": 1091,
    "preview": "# The AUTHORS Certificate\n# First edition, Fourteenth draft\n#\n# By proposing a change to this project that adds a line l"
  },
  {
    "path": "GNUmakefile",
    "chars": 1314,
    "preview": "PLATFORM ?= $(shell uname)\n\nifneq ($(PLATFORM), Darwin)\n    PLATFORM_CFLAGS ?= -static -Wl,--gc-sections\nendif\n\nLIBPNG_C"
  },
  {
    "path": "LICENSE",
    "chars": 1099,
    "preview": "Copyright (c) 2021, 2023-2025 D. Bohdan and contributors listed in AUTHORS\n\nPermission is hereby granted, free of charge"
  },
  {
    "path": "README.md",
    "chars": 6983,
    "preview": "# HiColor\n\n![A building with a dithered gradient of the sky behind it.\nA jet airplane is taking off in the sky.](bordeau"
  },
  {
    "path": "cli.c",
    "chars": 18170,
    "preview": "/* HiColor CLI.\n *\n * Copyright (c) 2021, 2023-2024 D. Bohdan and contributors listed in AUTHORS.\n * License: MIT.\n */\n\n"
  },
  {
    "path": "format.md",
    "chars": 727,
    "preview": "# File format\n\n- Magic: 7 bytes, `HiColor`.\n- Version: 1 byte, `5` for 15-bit color, `6` for 16-bit color.\n  Other versi"
  },
  {
    "path": "hicolor.h",
    "chars": 14270,
    "preview": "/* HiColor image file format encoder/decoder library.\n *\n * Copyright (c) 2021, 2023-2025 D. Bohdan and contributors lis"
  },
  {
    "path": "scripts/bayer-matrix.tcl",
    "chars": 504,
    "preview": "#! /usr/bin/env tclsh\n\nset bm8 {\n     0 48 12 60  3 51 15 63\n    32 16 44 28 35 19 47 31\n     8 56  4 52 11 59  7 55\n   "
  },
  {
    "path": "scripts/conversion-tables.tcl",
    "chars": 712,
    "preview": "#! /usr/bin/env tclsh\n\npackage require textutil\n\nset varDeclTemplate {static const uint8_t %s[] = {%s};}\n\nproc table {fr"
  },
  {
    "path": "tests/hicolor.test",
    "chars": 7033,
    "preview": "#! /usr/bin/env tclsh\n\npackage require Tcl 8.6 9\npackage require tcltest\n\ncd [file dirname [info script]]\n\nset hicolorCo"
  }
]

About this extraction

This page contains the full source code of the dbohdan/hicolor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 13 files (54.0 KB), approximately 17.3k tokens, and a symbol index with 34 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!