[
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\non: [push, pull_request]\njobs:\n  bsd:\n    runs-on: ${{ matrix.os.host }}\n    strategy:\n      matrix:\n        os:\n          - name: freebsd\n            architecture: x86-64\n            version: '14.1'\n            host: ubuntu-latest\n\n          - name: netbsd\n            architecture: x86-64\n            version: '10.0'\n            host: ubuntu-latest\n\n          - name: openbsd\n            architecture: x86-64\n            version: '7.5'\n            host: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Run CI script on ${{ matrix.os.name }}\n        uses: cross-platform-actions/action@v0.25.0\n        with:\n          operating_system: ${{ matrix.os.name }}\n          architecture: ${{ matrix.os.architecture }}\n          version: ${{ matrix.os.version }}\n          shell: bash\n          run: |\n            # Use sudo(1) rather than doas(1) on OpenBSD.\n            # doas(1) isn't configured.\n            # See https://github.com/cross-platform-actions/action/issues/75\n            sudo .github/workflows/install-deps.sh\n            gmake test\n\n  linux:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Install dependencies\n      run: |\n        sudo .github/workflows/install-deps.sh\n\n    - name: Test\n      run: |\n        gmake test\n\n    - name: Upload artifacts\n      uses: actions/upload-artifact@v4\n      with:\n        name: hicolor-linux-x86_64\n        path: |\n          hicolor\n\n  mac:\n    runs-on: macos-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Install dependencies\n      run: |\n        .github/workflows/install-deps.sh\n\n    - name: Build and test\n      run: |\n        make test\n\n    - name: Bundle dynamic libraries\n      run: |\n        dylibbundler --bundle-deps --create-dir --fix-file hicolor\n\n    - name: Upload artifacts\n      uses: actions/upload-artifact@v4\n      with:\n        name: hicolor-macos-arm64\n        path: |\n          hicolor\n          libs/\n\n  windows:\n    runs-on: windows-latest\n    steps:\n    - name: 'Disable `autocrlf` in Git'\n      run: git config --global core.autocrlf false\n\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Set up MSYS2\n      uses: msys2/setup-msys2@v2\n      with:\n        update: true\n        msystem: mingw32\n        install: |\n          make\n          mingw-w64-i686-gcc\n          mingw-w64-i686-libpng\n          mingw-w64-i686-pkgconf\n          mingw-w64-i686-zlib\n          tcl\n\n    - name: Test\n      shell: msys2 {0}\n      run: |\n        make test\n\n    - name: Upload artifacts\n      uses: actions/upload-artifact@v4\n      with:\n        name: hicolor-win32\n        path: |\n          hicolor.exe\n"
  },
  {
    "path": ".github/workflows/install-deps.sh",
    "content": "#! /bin/sh\nset -e\n\nif [ \"$(uname)\" = Darwin ]; then\n    brew install dylibbundler tcl-tk\nfi\n\nif [ \"$(uname)\" = Linux ]; then\n    apt-get install -y graphicsmagick libpng-dev pkgconf\nfi\n\nif [ \"$(uname)\" = FreeBSD ]; then\n    pkg install -y gmake GraphicsMagick pkgconf png tcl86\n    ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh\nfi\n\nif [ \"$(uname)\" = NetBSD ]; then\n    pkgin -y install gmake GraphicsMagick pkgconf png tcl zlib\nfi\n\nif [ \"$(uname)\" = OpenBSD ]; then\n    pkg_add -I gmake GraphicsMagick pkgconf png tcl%8.6\n    ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh\nfi\n"
  },
  {
    "path": ".gitignore",
    "content": "/attic/\n/bin/\n/hicolor\n/hicolor.exe\n/tests/*.hi*\n"
  },
  {
    "path": "AUTHORS",
    "content": "# The AUTHORS Certificate\n# First edition, Fourteenth draft\n#\n# By proposing a change to this project that adds a line like\n#\n#     Name <E-Mail> (URL) [Working For]\n#\n# below, you certify:\n#\n# 1. All of your contributions to this project are and will be your own,\n#    original work, licensed on the same terms as this project.\n#\n# 2. If someone else might own intellectual property in your\n#    contributions, like an employer or client, you've added their legal\n#    name in square brackets and got their written permission to submit\n#    your contribution.\n#\n# 3. If you haven't added a name in square brackets, you are sure that\n#    you have the legal right to license all your contributions so far\n#    by yourself.\n#\n# 4. If you make any future contribution under different intellectual\n#    property circumstances, you'll propose a change to add another line\n#    to this file for that contribution.\n#\n# 5. The name, e-mail, and URL you've added to this file are yours. You\n#    understand the project will make this file public.\nD. Bohdan <see commit history> https://dbohdan.com/\n"
  },
  {
    "path": "GNUmakefile",
    "content": "PLATFORM ?= $(shell uname)\n\nifneq ($(PLATFORM), Darwin)\n    PLATFORM_CFLAGS ?= -static -Wl,--gc-sections\nendif\n\nLIBPNG_CFLAGS ?= $(shell pkg-config --cflags libpng)\nLIBPNG_LIBS ?= $(shell pkg-config --libs libpng)\nZLIB_CFLAGS ?= $(shell pkg-config --cflags zlib)\nZLIB_LIBS ?= $(shell pkg-config --libs zlib)\n\nCFLAGS ?= -std=c99 -g -O3 $(PLATFORM_CFLAGS) -ffunction-sections -fdata-sections -Wall -Wextra $(LIBPNG_CFLAGS) $(ZLIB_CFLAGS)\nLIBS ?= $(LIBPNG_LIBS) $(ZLIB_LIBS) -lm\nPREFIX ?= /usr/local\n\nall: hicolor\n\nhicolor: cli.c hicolor.h\n\t$(CC) $< -o $@ $(CFLAGS) $(LIBS)\n\nclean: clean-no-ext clean-exe\nclean-exe:\n\t-rm -f hicolor.exe\nclean-no-ext:\n\t-rm -f hicolor\n\ninstall: install-bin install-include\ninstall-bin: hicolor\n\tinstall $< $(DESTDIR)$(PREFIX)/bin/hicolor\ninstall-include: hicolor.h\n\tinstall -m 0644 $< $(DESTDIR)$(PREFIX)/include\n\nuninstall: uninstall-bin uninstall-include\nuninstall-bin:\n\t-rm $(DESTDIR)$(PREFIX)/bin/hicolor\nuninstall-include:\n\t-rm $(DESTDIR)$(PREFIX)/include/hicolor.h\n\nrelease: clean-no-ext test\n\tcp hicolor hicolor-v\"$$(./hicolor version | head -n 1 | awk '{ print $$2 }')\"-\"$$(uname | tr 'A-Z' 'a-z')\"-\"$$(uname -m)\"\n\ntest: all\n\ttests/hicolor.test\n\n.PHONY: all clean clean-exe clean-no-ext install install-bin install-include release test uninstall uninstall-bin uninstall-include\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2021, 2023-2025 D. Bohdan and contributors listed in AUTHORS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# HiColor\n\n![A building with a dithered gradient of the sky behind it.\nA jet airplane is taking off in the sky.](bordeaux-15bit.png)\n\n*(The image above has 15-bit color.)*\n\nHiColor is a program and a C library for converting images to 15- and 16-bit RGB color,\nthe color depth of old display modes known as [&ldquo;high color&rdquo;](https://en.wikipedia.org/wiki/High_color).\nI wrote it because I wanted to create images with the characteristic high-color look.\n\n## Contents\n\n- [Description](#description)\n- [Known bugs and limitations](#known-bugs-and-limitations)\n- [Usage](#usage)\n- [Building](#building)\n- [Alternatives](#alternatives)\n- [License](#license)\n\n## Description\n\nHiColor reduces images to two-byte 15- or 16-bit color.\nIn 15-bit mode images have 5 bits for each of red, green, and blue, with the last bit reserved.\nIn 16-bit mode green, the color the human eye is generally most sensitive to, is given 6 bits.\n\nHiColor implements its own simple [file format](format.md) and converts between this format and PNG.\nIt can also convert standard PNG to standard PNG with only high-color color values.\n(This simulates a roundtrip through HiColor without creating a temporary file.)\nHiColor files have either the extension `.hic` or `.hi5` for 15-bit and `.hi6` for 16-bit respectively.\n\nBy default,\nHiColor applies the [Bayer ordered dithering](https://en.wikipedia.org/wiki/Ordered_dithering) algorithm\nto reduce the quantization error\n(the difference between the original and the high-color pixel).\nHistorical software and hardware used it for dithering in high-color modes.\nHiColor can also use [&ldquo;a dither&rdquo;](https://pippin.gimp.org/a_dither/) instead.\nDithering can be selected or disabled with command-line flags.\n\nQuantized images compress better than their originals,\nso HiColor can be a less-lossy alternative to the 256-color [pngquant](https://pngquant.org/).\nQuantizing a PNG file to PNG preserves transparency (but does not quantize the alpha channel).\nConversion to and from the HiColor format does not preserve transparency.\n\nThe program is written in C with two external dependencies, libpng and zlib, and builds as a static binary.\nIt is known to work on\nLinux (aarch64, i386, riscv64, x86_64),\nFreeBSD,\nNetBSD,\nOpenBSD,\nand Windows 98 Second Edition,\n2000 Service Pack 4,\nXP,\n7,\nand 10.\n\nThe library is a single C99 header file.\nIt is designed to be easy to understand and modify\nat a cost to performance.\nThe design makes it unsuitable for real-time graphics.\n\n## Known bugs and limitations\n\n### Security\n\nThe command-line program (but not the library) was vulnerable to malicious PNG files\nbecause it used a PNG library intended only for trusted input.\nThe vulnerabilities were fixed in version 0.6.0 by switching to libpng.\n\n### PNG file size\n\nPNG files produced by HiColor are not highly optimized.\nRun them through [OptiPNG](http://optipng.sourceforge.net/) or [Oxipng](https://github.com/shssoichiro/oxipng) to significantly reduce their size.\n\n### Generation loss\n\nWith Bayer dithering or no dithering, there is no [generation loss](https://en.wikipedia.org/wiki/Generation_loss) after the initial quantization.\nApplying &ldquo;a dither&rdquo; repeatedly to the same image will result in generation loss.\nIn tests the loss converges to zero after 32 or 64 generations\n(in 15-bit and 16-bit mode respectively).\n\nHiColor 0.1.0&ndash;0.2.1 suffered from generation loss with Bayer dithering due to an implementation error.\nThe error was fixed in version 0.3.0.\n\n## Usage\n\nHiColor has a Git-style CLI.\n\nThe actions `encode` and `decode` convert images between PNG and HiColor's own image format.\n`quantize` round-trips an image through the converter and outputs a standard 32-bit PNG.\nUse `quantize` to create high-color images readable by other programs.\n`info` prints information about a HiColor file: version (`5` for 15-bit or `6` for 16), width, and height.\n\n```none\nHiColor 1.0.1\nCreate 15/16-bit color RGB images.\n\nusage:\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\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```\n\n## Building\n\n### Debian/Ubuntu\n\n```sh\nsudo apt install -y build-essential graphicsmagick linpng-dev pkgconf tclsh zlib1g-dev\ngmake test\n```\n\n### FreeBSD\n\n```sh\nsudo pkg install -y gmake GraphicsMagick pkgconf png tcl86\nln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh\nfi\ngmake test\n```\n\n### macOS\n\nInstall [Homebrew](https://brew.sh/).\nRun the following commands in a clone of the HiColor repository.\n\n```sh\nbrew install libpng tcl-tk\nmake test\n```\n\n### NetBSD\n\n```sh\nsudo pkgin -y install gmake GraphicsMagick pkgconf png tcl zlib\ngmake test\n```\n\n### OpenBSD\n\n```sh\ndoas pkg_add -I gmake GraphicsMagick pkgconf png tcl%8.6\nln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh\ngmake test\n```\n\n### Windows\n\nInstall [MSYS2](https://www.msys2.org/).\nRun the following commands in the MSYS2 mingw32 shell\nin a clone of the HiColor repository.\nThis will build an x86 executable for Windows.\n\n```sh\npacman -Syuu make mingw-w64-i686-gcc mingw-w64-i686-libpng mingw-w64-i686-pkgconf mingw-w64-i686-zlib tcl\nmake test\n```\n\n## Alternatives\n\nI wrote HiColor because nothing seemed to support high color.\nActually,\n[FFmpeg](https://www.madox.net/blog/2011/06/06/converting-tofrom-rgb565-in-ubuntu-using-ffmpeg/),\n[GIMP](https://docs.gimp.org/2.10/en/gimp-filter-dither.html),\nand\n[ImageMagick](https://www.imagemagick.org/Usage/quantize/#16bit_colormap)\ncan reduce images to 15- and 16-bit color.\nWhat differentiates HiColor is being a small dedicated tool and embeddable C library and having its own file format.\n\n## License\n\nMIT.\n\nHiColor uses [libpng](http://www.libpng.org/pub/png/libpng.html) and [zlib](https://www.zlib.net/).\nFollow the links for their respective licenses.\n\n### Photos from Unsplash\n\n[&ldquo;plane in flight&rdquo;](https://unsplash.com/photos/AwtncJT1qKs) (`bordeaux-15bit.png`) by olaf wisser.\n\n[&ldquo;houses beside trees&rdquo;](https://unsplash.com/photos/PWBXQJ7PUkI) (`tests/photo.png`) by Orlova Maria.\n\n#### License\n\n> 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.\n"
  },
  {
    "path": "cli.c",
    "content": "/* HiColor CLI.\n *\n * Copyright (c) 2021, 2023-2024 D. Bohdan and contributors listed in AUTHORS.\n * License: MIT.\n */\n\n#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n\n#include <png.h>\n#include <zlib.h>\n\n#define HICOLOR_IMPLEMENTATION\n#include \"hicolor.h\"\n\n#define HICOLOR_CLI_ERROR \"error: \"\n#define HICOLOR_CLI_LIB_NAME_FORMAT \"%-9s\"\n#define HICOLOR_CLI_LIBPNG_COMPRESSION_LEVEL 6\n#define HICOLOR_CLI_NO_MEMORY_EXIT_CODE 255\n\n#define HICOLOR_CLI_CMD_ENCODE \"encode\"\n#define HICOLOR_CLI_CMD_QUANTIZE \"quantize\"\n#define HICOLOR_CLI_CMD_DECODE \"decode\"\n#define HICOLOR_CLI_CMD_INFO \"info\"\n#define HICOLOR_CLI_CMD_VERSION \"version\"\n#define HICOLOR_CLI_CMD_HELP \"help\"\n\nconst char* png_error_msg = \"no error recorded\";\n\nvoid libpng_error_handler(\n    png_structp png_ptr,\n    png_const_charp error_msg\n)\n{\n    png_error_msg = error_msg;\n    longjmp(png_jmpbuf(png_ptr), 1);\n}\n\nbool load_png(\n    const char* filename,\n    int* width,\n    int* height,\n    hicolor_rgb** rgb_img,\n    uint8_t** alpha\n)\n{\n    FILE* fp = fopen(filename, \"rb\");\n    if (!fp) {\n        png_error_msg = \"failed to open for reading\";\n        return false;\n    }\n\n    png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, libpng_error_handler, NULL);\n    if (png == NULL) {\n        png_error_msg = \"`png_create_read_struct` returned null\";\n        fclose(fp);\n        return false;\n    }\n\n    png_infop info = png_create_info_struct(png);\n    if (info == NULL) {\n        png_error_msg = \"`png_create_info_struct` returned null\";\n        png_destroy_read_struct(&png, NULL, NULL);\n        fclose(fp);\n        return false;\n    }\n\n    if (setjmp(png_jmpbuf(png))) {\n        /* Do not overwrite `png_error_msg` set by the handler. */\n        png_destroy_read_struct(&png, &info, NULL);\n        fclose(fp);\n        return false;\n    }\n\n    png_init_io(png, fp);\n    png_read_info(png, info);\n\n    *width = png_get_image_width(png, info);\n    *height = png_get_image_height(png, info);\n    png_byte color_type = png_get_color_type(png, info);\n    png_byte bit_depth = png_get_bit_depth(png, info);\n\n    if (bit_depth == 16) {\n        png_set_strip_16(png);\n    }\n\n    if (color_type == PNG_COLOR_TYPE_PALETTE) {\n        png_set_palette_to_rgb(png);\n    }\n\n    if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) {\n        png_set_expand_gray_1_2_4_to_8(png);\n    }\n\n    if (png_get_valid(png, info, PNG_INFO_tRNS)) {\n        png_set_tRNS_to_alpha(png);\n    }\n\n    if (color_type == PNG_COLOR_TYPE_RGB\n        || color_type == PNG_COLOR_TYPE_GRAY\n        || color_type == PNG_COLOR_TYPE_PALETTE) {\n        png_set_filler(png, 0xFF, PNG_FILLER_AFTER);\n    }\n\n    if (color_type == PNG_COLOR_TYPE_GRAY\n        || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) {\n        png_set_gray_to_rgb(png);\n    }\n\n    png_read_update_info(png, info);\n\n    *rgb_img = malloc(sizeof(hicolor_rgb) * *width * *height);\n    *alpha = malloc(sizeof(uint8_t) * *width * *height);\n\n    if (*rgb_img == NULL || *alpha == NULL) {\n        png_error_msg = \"failed to allocate memory for `rgb_img` or `alpha`\";\n        png_destroy_read_struct(&png, &info, NULL);\n        fclose(fp);\n        return false;\n    }\n\n    png_bytep row = malloc(png_get_rowbytes(png, info));\n    if (row == NULL) {\n        png_error_msg = \"failed to allocate memory for `row`\";\n        free(*rgb_img);\n        free(*alpha);\n        png_destroy_read_struct(&png, &info, NULL);\n        fclose(fp);\n        return false;\n    }\n\n    for (int y = 0; y < *height; y++) {\n        png_read_row(png, row, NULL);\n\n        for (int x = 0; x < *width; x++) {\n            png_bytep pixel = &(row[x * 4]);\n            (*rgb_img)[y * (*width) + x].r = pixel[0];\n            (*rgb_img)[y * (*width) + x].g = pixel[1];\n            (*rgb_img)[y * (*width) + x].b = pixel[2];\n            (*alpha)[y * (*width) + x] = pixel[3];\n        }\n    }\n\n    free(row);\n    png_destroy_read_struct(&png, &info, NULL);\n    fclose(fp);\n\n    return true;\n}\n\nbool save_png(\n    const char* filename,\n    int width,\n    int height,\n    const hicolor_rgb* rgb_img,\n    const uint8_t* alpha\n)\n{\n    FILE* fp = fopen(filename, \"wb\");\n    if (!fp) {\n        png_error_msg = \"failed to open for writing\";\n        return false;\n    }\n\n    png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, libpng_error_handler, NULL);\n    if (png == NULL) {\n        png_error_msg = \"`png_create_write_struct` returned null\";\n        fclose(fp);\n        return false;\n    }\n\n    png_infop info = png_create_info_struct(png);\n    if (info == NULL) {\n        png_error_msg = \"`png_create_info_struct` returned null\";\n        png_destroy_write_struct(&png, NULL);\n        fclose(fp);\n        return false;\n    }\n\n    if (setjmp(png_jmpbuf(png))) {\n        /* Do not overwrite `png_error_msg` set by the handler. */\n        png_destroy_write_struct(&png, &info);\n        fclose(fp);\n        return false;\n    }\n\n    png_init_io(png, fp);\n\n    png_set_IHDR(\n        png,\n        info,\n        width,\n        height,\n        8,\n        PNG_COLOR_TYPE_RGBA,\n        PNG_INTERLACE_NONE,\n        PNG_COMPRESSION_TYPE_DEFAULT,\n        PNG_FILTER_TYPE_DEFAULT\n    );\n    png_set_compression_level(png, HICOLOR_CLI_LIBPNG_COMPRESSION_LEVEL);\n    png_write_info(png, info);\n\n    png_bytep row = malloc(png_get_rowbytes(png, info));\n    if (row == NULL) {\n        png_error_msg = \"failed to allocate memory for `row`\";\n        png_destroy_write_struct(&png, &info);\n        fclose(fp);\n        return false;\n    }\n\n    for (int y = 0; y < height; y++) {\n        for (int x = 0; x < width; x++) {\n            png_bytep pixel = &(row[x * 4]);\n            pixel[0] = rgb_img[y * width + x].r;\n            pixel[1] = rgb_img[y * width + x].g;\n            pixel[2] = rgb_img[y * width + x].b;\n            pixel[3] = alpha == NULL ? 255 : alpha[y * width + x];\n        }\n\n        png_write_row(png, row);\n    }\n\n    free(row);\n    png_write_end(png, NULL);\n    png_destroy_write_struct(&png, &info);\n    fclose(fp);\n\n    return true;\n}\n\nbool check_and_report_error(\n    char* step,\n    hicolor_result res\n)\n{\n    if (res == HICOLOR_OK) {\n        return false;\n    }\n\n    fprintf(\n        stderr,\n        HICOLOR_CLI_ERROR \"%s: %s\\n\",\n        step,\n        hicolor_error_message(res)\n    );\n\n    return true;\n}\n\nbool check_src_exists(\n    const char* src\n)\n{\n    if (access(src, F_OK) != 0) {\n        fprintf(\n            stderr,\n            HICOLOR_CLI_ERROR \"source image \\\"%s\\\" doesn't exist\\n\",\n            src\n        );\n        return false;\n    }\n\n    return true;\n}\n\nbool png_to_hicolor(\n    hicolor_version version,\n    hicolor_dither dither,\n    const char* src,\n    const char* dest\n)\n{\n    hicolor_result res;\n\n    bool exists = check_src_exists(src);\n    if (!exists) {\n        return false;\n    }\n\n    int width, height;\n    hicolor_rgb* rgb_img = NULL;\n    uint8_t* alpha = NULL;\n    if (!load_png(src, &width, &height, &rgb_img, &alpha)) {\n        fprintf(\n            stderr,\n            HICOLOR_CLI_ERROR \"can't load PNG file \\\"%s\\\": %s\\n\",\n            src,\n            png_error_msg\n        );\n        return false;\n    }\n\n    FILE* hi_file = fopen(dest, \"wb\");\n    if (hi_file == NULL) {\n        fprintf(\n            stderr,\n            HICOLOR_CLI_ERROR \"can't open file \\\"%s\\\" for writing\\n\",\n            dest\n        );\n\n        free(rgb_img);\n        return false;\n    }\n\n    hicolor_metadata meta = {\n        .version = version,\n        .width = width,\n        .height = height\n    };\n    res = hicolor_write_header(hi_file, meta);\n\n    bool success = false;\n    if (check_and_report_error(\"can't write header\", res)) {\n        goto clean_up_file;\n    }\n\n    res = hicolor_quantize_rgb_image(meta, dither, rgb_img);\n    if (check_and_report_error(\"can't quantize image\", res)) {\n        goto clean_up_images;\n    }\n\n    res = hicolor_write_rgb_image(hi_file, meta, rgb_img);\n    if (check_and_report_error(\"can't write image data\", res)) {\n        goto clean_up_images;\n    }\n\n    success = true;\n\nclean_up_images:\n    free(rgb_img);\n\nclean_up_file:\n    fclose(hi_file);\n\n    return success;\n}\n\nbool png_quantize(\n    hicolor_version version,\n    hicolor_dither dither,\n    const char* src,\n    const char* dest\n)\n{\n    hicolor_result res;\n\n    bool exists = check_src_exists(src);\n    if (!exists) {\n        return false;\n    }\n\n    int width, height;\n    hicolor_rgb* rgb_img = NULL;\n    uint8_t* alpha = NULL;\n    if (!load_png(src, &width, &height, &rgb_img, &alpha)) {\n        fprintf(\n            stderr,\n            HICOLOR_CLI_ERROR \"can't load PNG file \\\"%s\\\": %s\\n\",\n            src,\n            png_error_msg\n        );\n        return false;\n    }\n\n    hicolor_metadata meta = {\n        .version = version,\n        .width = width,\n        .height = height\n    };\n\n    res = hicolor_quantize_rgb_image(meta, dither, rgb_img);\n    bool success = false;\n    if (check_and_report_error(\"can't quantize image\", res)) {\n        goto clean_up_images;\n    }\n\n    if (!save_png(dest, width, height, rgb_img, alpha)) {\n        fprintf(\n            stderr,\n            HICOLOR_CLI_ERROR \"can't save PNG: %s\\n\",\n            png_error_msg\n        );\n        goto clean_up_images;\n    }\n\n    success = true;\n\nclean_up_images:\n    free(rgb_img);\n\n    return success;\n}\n\nbool hicolor_to_png(\n    const char* src,\n    const char* dest\n)\n{\n    hicolor_result res;\n\n    bool exists = check_src_exists(src);\n    if (!exists) {\n        return false;\n    }\n\n    FILE* hi_file = fopen(src, \"rb\");\n    if (hi_file == NULL) {\n        fprintf(stderr, HICOLOR_CLI_ERROR \"can't open source image \\\"%s\\\" for reading\\n\", src);\n        return false;\n    }\n\n    hicolor_metadata meta;\n    res = hicolor_read_header(hi_file, &meta);\n    bool success = false;\n    if (check_and_report_error(\"can't read header\", res)) {\n        goto clean_up_file;\n    }\n\n    hicolor_rgb* rgb_img = malloc(sizeof(hicolor_rgb) * meta.width * meta.height);\n    if (rgb_img == NULL) {\n        goto clean_up_file;\n    }\n    res = hicolor_read_rgb_image(hi_file, meta, rgb_img);\n    if (check_and_report_error(\"can't read image data\", res)) {\n        goto clean_up_rgb_img;\n    }\n\n    if (!save_png(dest, meta.width, meta.height, rgb_img, NULL)) {\n        fprintf(\n            stderr,\n            HICOLOR_CLI_ERROR \"can't save PNG: %s\\n\",\n            png_error_msg\n        );\n        goto clean_up_rgb_img;\n    }\n\n    success = true;\n\nclean_up_rgb_img:\n    free(rgb_img);\n\nclean_up_file:\n    fclose(hi_file);\n\n    return success;\n}\n\nbool hicolor_print_info(\n    const char* src\n)\n{\n    hicolor_result res;\n\n    bool exists = check_src_exists(src);\n    if (!exists) {\n        return false;\n    }\n\n    FILE* hi_file = fopen(src, \"rb\");\n    if (hi_file == NULL) {\n        fprintf(\n            stderr,\n            HICOLOR_CLI_ERROR \"can't open source image \\\"%s\\\" for reading\\n\",\n            src\n        );\n        return false;\n    }\n\n    hicolor_metadata meta;\n    res = hicolor_read_header(hi_file, &meta);\n    bool success = false;\n    if (check_and_report_error(\"can't read header\", res)) {\n        goto clean_up_file;\n    }\n\n    uint8_t vch = '\\0';\n    res = hicolor_version_to_char(meta.version, &vch);\n    if (check_and_report_error(\"can't decode version\", res)) {\n        goto clean_up_file;\n    }\n\n    printf(\n        \"%c %i %i\\n\",\n        vch,\n        meta.width,\n        meta.height\n    );\n\n    success = true;\n\nclean_up_file:\n    fclose(hi_file);\n\n    return success;\n}\n\nvoid usage(\n    FILE* output\n)\n{\n    fprintf(\n        output,\n        \"usage:\\n\"\n        \"  hicolor (encode|quantize) [-5|-6] [-a|-b|-n] [--] <src> [<dest>]\\n\"\n        \"  hicolor decode <src> [<dest>]\\n\"\n        \"  hicolor info <file>\\n\"\n        \"  hicolor (version|help|-h|--help)\\n\"\n    );\n}\n\nvoid version(\n    bool full\n)\n{\n    if (full) {\n        printf(\n            HICOLOR_CLI_LIB_NAME_FORMAT,\n            \"HiColor\"\n        );\n    }\n\n    uint32_t program_version = HICOLOR_LIBRARY_VERSION;\n    printf(\n        \"%u.%u.%u\\n\",\n        program_version / 10000,\n        program_version % 10000 / 100,\n        program_version % 100\n    );\n\n    if (!full) {\n        return;\n    }\n\n    png_uint_32 libpng_version = png_access_version_number();\n    printf(\n        HICOLOR_CLI_LIB_NAME_FORMAT \"%u.%u.%u\\n\",\n        \"libpng\",\n        libpng_version / 10000,\n        libpng_version % 10000 / 100,\n        libpng_version % 100\n    );\n\n    printf(\n        HICOLOR_CLI_LIB_NAME_FORMAT \"%s\\n\",\n        \"zlib\",\n        ZLIB_VERSION\n    );\n}\n\nvoid help()\n{\n    printf(\n        \"HiColor \"\n    );\n    version(false);\n    printf(\n        \"Create 15/16-bit color RGB images.\\n\\n\"\n    );\n    usage(stdout);\n    printf(\n        \"\\ncommands:\\n\"\n        \"  encode           convert PNG to HiColor\\n\"\n        \"  decode           convert HiColor to PNG\\n\"\n        \"  quantize         quantize PNG to PNG\\n\"\n        \"  info             print HiColor image version and resolution\\n\"\n        \"  version          print version of HiColor, libpng, and zlib\\n\"\n        \"  help             print this help message\\n\"\n        \"\\noptions:\\n\"\n        \"  -5, --15-bit     15-bit color\\n\"\n        \"  -6, --16-bit     16-bit color\\n\"\n        \"  -a, --a-dither   dither image with \\\"a dither\\\"\\n\"\n        \"  -b, --bayer      dither image with Bayer algorithm (default)\\n\"\n        \"  -n, --no-dither  do not dither image\\n\"\n    );\n}\n\nbool str_prefix(\n    const char* ref,\n    const char* str\n)\n{\n    size_t i;\n\n    for (i = 0; str[i] != '\\0'; i++) {\n        if (str[i] != ref[i]) {\n            return false;\n        }\n    }\n\n    if (i == 0) {\n        return false;\n    }\n\n    return true;\n}\n\ntypedef enum command {\n    ENCODE, DECODE, QUANTIZE, INFO, VERSION, HELP\n} command;\n\nint main(\n    int argc,\n    char** argv\n)\n{\n    command opt_command = ENCODE;\n    hicolor_dither opt_dither = HICOLOR_BAYER;\n    hicolor_version opt_version = HICOLOR_VERSION_6;\n    const char* command_name;\n    char* arg_src;\n    char* arg_dest;\n    bool allow_opts = true;\n    int min_pos_args = 1;\n    int max_pos_args = 2;\n\n    if (argc <= 1) {\n        help();\n        return 1;\n    }\n\n    /* The regular \"help\" command is handled later with the rest. */\n    for (int i = 0; i < argc; i++) {\n        if (strcmp(argv[i], \"-h\") == 0\n            || strcmp(argv[i], \"--help\") == 0) {\n            help();\n            return 0;\n        }\n    }\n\n    int i = 1;\n\n    if (str_prefix(HICOLOR_CLI_CMD_ENCODE, argv[i])) {\n        command_name = HICOLOR_CLI_CMD_ENCODE;\n        opt_command = ENCODE;\n    } else if (str_prefix(HICOLOR_CLI_CMD_DECODE, argv[i])) {\n        allow_opts = false;\n        command_name = HICOLOR_CLI_CMD_DECODE;\n        opt_command = DECODE;\n    } else if (str_prefix(HICOLOR_CLI_CMD_QUANTIZE, argv[i])) {\n        command_name = HICOLOR_CLI_CMD_QUANTIZE;\n        opt_command = QUANTIZE;\n    } else if (str_prefix(HICOLOR_CLI_CMD_INFO, argv[i])) {\n        allow_opts = false;\n        command_name = HICOLOR_CLI_CMD_INFO;\n        max_pos_args = 1;\n        opt_command = INFO;\n    } else if (str_prefix(HICOLOR_CLI_CMD_VERSION, argv[i])) {\n        allow_opts = false;\n        command_name = HICOLOR_CLI_CMD_VERSION;\n        min_pos_args = 0;\n        max_pos_args = 0;\n        opt_command = VERSION;\n    } else if (str_prefix(HICOLOR_CLI_CMD_HELP, argv[i])) {\n        allow_opts = false;\n        command_name = HICOLOR_CLI_CMD_HELP;\n        min_pos_args = 0;\n        max_pos_args = 0;\n        opt_command = HELP;\n    } else {\n        usage(stderr);\n        fprintf(\n            stderr,\n            \"\\n\" HICOLOR_CLI_ERROR \"unknown command \\\"%s\\\"\\n\",\n            argv[i]\n        );\n        return 1;\n    }\n\n    i++;\n\n    if (allow_opts) {\n        while (i < argc && argv[i][0] == '-') {\n            if (strcmp(argv[i], \"--\") == 0) {\n                i++;\n                break;\n            } else if (strcmp(argv[i], \"-5\") == 0\n                || strcmp(argv[i], \"--15-bit\") == 0) {\n                opt_version = HICOLOR_VERSION_5;\n            } else if (strcmp(argv[i], \"-6\") == 0\n                || strcmp(argv[i], \"--16-bit\") == 0) {\n                opt_version = HICOLOR_VERSION_6;\n            } else if (strcmp(argv[i], \"-a\") == 0\n                || strcmp(argv[i], \"--a-dither\") == 0) {\n                opt_dither = HICOLOR_A_DITHER;\n            } else if (strcmp(argv[i], \"-b\") == 0\n                || strcmp(argv[i], \"--bayer\") == 0) {\n                opt_dither = HICOLOR_BAYER;\n            } else if (strcmp(argv[i], \"-n\") == 0\n                || strcmp(argv[i], \"--no-dither\") == 0) {\n                opt_dither = HICOLOR_NO_DITHER;\n            } else {\n                usage(stderr);\n                fprintf(\n                    stderr,\n                    \"\\n\" HICOLOR_CLI_ERROR \"unknown option \\\"%s\\\"\\n\",\n                    argv[i]\n                );\n                return 1;\n            }\n\n            i++;\n        }\n    }\n\n    int rem_args = argc - i;\n\n    if (rem_args < min_pos_args) {\n        usage(stderr);\n        fprintf(\n            stderr,\n            \"\\n\" HICOLOR_CLI_ERROR \"no source image given to command \\\"%s\\\"\\n\",\n            command_name\n        );\n        return 1;\n    }\n\n    if (rem_args > max_pos_args) {\n        usage(stderr);\n        fprintf(\n            stderr,\n            \"\\n\" HICOLOR_CLI_ERROR \"too many arguments to command \\\"%s\\\"\\n\",\n            command_name\n        );\n        return 1;\n    }\n\n    arg_src = argv[i];\n    i++;\n\n    if (i == argc) {\n        arg_dest = malloc(strlen(arg_src) + 5);\n        if (arg_dest == NULL) {\n            return HICOLOR_CLI_NO_MEMORY_EXIT_CODE;\n        }\n        sprintf(\n            arg_dest,\n            opt_command == ENCODE ? \"%s.hic\" : \"%s.png\",\n            arg_src\n        );\n    } else {\n        arg_dest = argv[i];\n    }\n    i++;\n\n    switch (opt_command) {\n    case ENCODE:\n        return !png_to_hicolor(opt_version, opt_dither, arg_src, arg_dest);\n    case DECODE:\n        return !hicolor_to_png(arg_src, arg_dest);\n    case QUANTIZE:\n        return !png_quantize(opt_version, opt_dither, arg_src, arg_dest);\n    case INFO:\n        return !hicolor_print_info(arg_src);\n    case VERSION:\n        version(true);\n        return 0;\n    case HELP:\n        help();\n        return 0;\n    }\n}\n"
  },
  {
    "path": "format.md",
    "content": "# File format\n\n- Magic: 7 bytes, `HiColor`.\n- Version: 1 byte, `5` for 15-bit color, `6` for 16-bit color.\n  Other versions may be added later.\n- Width: 2 bytes: WB1, WB2.\n  Width = WB1 + 256×WB2.\n- Height: 2 bytes: HB1, HB2.\n  Height = HB1 + 256×HB2.\n- Data: Width×Height×2 bytes.\n  The Data consist of Width×Height Values.\n  Each Value is 2 bytes: VB1, VB2.\n  Value = VB1 + 256×VB2.\n\nThe Data part encodes the lines of pixels comprising the image from top to bottom and each line from left to right.\nThe first Value of the data is the top-left pixel, the next is the one to its right, etc.\n\n## Values\n\n- Version `5`:\n    - 5 bits red, 5 bits green, 5 bits blue, 0.\n- Version `6`:\n    - 5 bits red, 6 bits green, 5 bits blue.\n"
  },
  {
    "path": "hicolor.h",
    "content": "/* HiColor image file format encoder/decoder library.\n *\n * Copyright (c) 2021, 2023-2025 D. Bohdan and contributors listed in AUTHORS.\n * License: MIT.\n *\n * This header file contains both the interface and the implementation for\n * HiColor. To instantiate the implementation put the line\n *     #define HICOLOR_IMPLEMENTATION\n * in a single source code file of your project above where you include this\n * file.\n */\n\n#ifndef HICOLOR_H\n#define HICOLOR_H\n\n#include <inttypes.h>\n#include <math.h>\n#include <stdbool.h>\n#include <stdio.h>\n\n#define HICOLOR_BAYER_SIZE 8\n#define HICOLOR_LIBRARY_VERSION 10001\n\n/* Types. */\n\nstatic const uint8_t hicolor_magic[7] = \"HiColor\";\n\n/* These arrays are generated with `scripts/conversion-tables.tcl`. */\nstatic const uint8_t hicolor_256_to_32[] = {\n    0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,\n    3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5,\n    6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8,\n    9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11,\n    11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13,\n    13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15,\n    16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18,\n    18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20,\n    20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,\n    22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24,\n    25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27,\n    27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29,\n    29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31,\n    31, 31\n};\n\nstatic const uint8_t hicolor_256_to_64[] = {\n    0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5,\n    6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11,\n    11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15,\n    16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20,\n    20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24,\n    25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29,\n    29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 32, 32, 32, 32, 33, 33, 33, 33,\n    34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38,\n    38, 38, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42,\n    43, 43, 43, 43, 44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47,\n    47, 47, 48, 48, 48, 48, 49, 49, 49, 49, 50, 50, 50, 50, 51, 51, 51, 51,\n    52, 52, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 56, 56,\n    56, 56, 57, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59, 60, 60, 60, 60,\n    61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 63\n};\n\nstatic const uint8_t hicolor_32_to_256[] = {\n    0, 8, 16, 24, 33, 41, 49, 57, 66, 74, 82, 90, 99, 107, 115, 123, 132,\n    140, 148, 156, 165, 173, 181, 189, 198, 206, 214, 222, 231, 239, 247,\n    255\n};\n\nstatic const uint8_t hicolor_64_to_256[] = {\n    0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 65, 69, 73,\n    77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 130, 134,\n    138, 142, 146, 150, 154, 158, 162, 166, 170, 174, 178, 182, 186, 190,\n    195, 199, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 243, 247,\n    251, 255\n};\n\n/* The values in this array are the output of\n * `scripts/bayer-matrix.tcl`.\n */\nstatic const double hicolor_bayer[HICOLOR_BAYER_SIZE * HICOLOR_BAYER_SIZE] = {\n     0.0/64, 48.0/64, 12.0/64, 60.0/64,  3.0/64, 51.0/64, 15.0/64, 63.0/64,\n    32.0/64, 16.0/64, 44.0/64, 28.0/64, 35.0/64, 19.0/64, 47.0/64, 31.0/64,\n     8.0/64, 56.0/64,  4.0/64, 52.0/64, 11.0/64, 59.0/64,  7.0/64, 55.0/64,\n    40.0/64, 24.0/64, 36.0/64, 20.0/64, 43.0/64, 27.0/64, 39.0/64, 23.0/64,\n     2.0/64, 50.0/64, 14.0/64, 62.0/64,  1.0/64, 49.0/64, 13.0/64, 61.0/64,\n    34.0/64, 18.0/64, 46.0/64, 30.0/64, 33.0/64, 17.0/64, 45.0/64, 29.0/64,\n    10.0/64, 58.0/64,  6.0/64, 54.0/64,  9.0/64, 57.0/64,  5.0/64, 53.0/64,\n    42.0/64, 26.0/64, 38.0/64, 22.0/64, 41.0/64, 25.0/64, 37.0/64, 21.0/64\n};\n\ntypedef enum hicolor_version {\n    HICOLOR_VERSION_5,\n    HICOLOR_VERSION_6\n} hicolor_version;\n\ntypedef struct hicolor_metadata {\n    hicolor_version version;\n    uint16_t width;\n    uint16_t height;\n} hicolor_metadata;\n\ntypedef enum hicolor_result {\n    HICOLOR_OK,\n    HICOLOR_IO_ERROR,\n    HICOLOR_UNKNOWN_VERSION,\n    HICOLOR_INVALID_VALUE,\n    HICOLOR_INSUFFICIENT_DATA,\n    HICOLOR_BAD_MAGIC\n} hicolor_result;\n\ntypedef enum hicolor_dither {\n    HICOLOR_A_DITHER,\n    HICOLOR_BAYER,\n    HICOLOR_NO_DITHER\n} hicolor_dither;\n\ntypedef struct hicolor_rgb {\n    uint8_t r;\n    uint8_t g;\n    uint8_t b;\n} hicolor_rgb;\n\ntypedef uint16_t hicolor_value;\n\n/* Functions. */\n\nconst char* hicolor_error_message(hicolor_result res);\n\nhicolor_result hicolor_char_to_version(\n    const uint8_t ch,\n    hicolor_version* version\n);\nhicolor_result hicolor_version_to_char(\n    const hicolor_version version,\n    uint8_t* ch\n);\n\nhicolor_result hicolor_value_to_rgb(\n    const hicolor_version version,\n    const hicolor_value value,\n    hicolor_rgb* rgb\n);\nhicolor_result hicolor_rgb_to_value(\n    const hicolor_version version,\n    const hicolor_rgb rgb,\n    hicolor_value* value\n);\n\nhicolor_result hicolor_read_header(\n    FILE* stream,\n    hicolor_metadata* meta\n);\nhicolor_result hicolor_write_header(\n    FILE* stream,\n    const hicolor_metadata meta\n);\n\n/* Quantize using optional dithering. */\nhicolor_result hicolor_quantize_rgb_image(\n    const hicolor_metadata meta,\n    hicolor_dither dither,\n    hicolor_rgb* image\n);\n\nhicolor_result hicolor_read_rgb_image(\n    FILE* stream,\n    const hicolor_metadata meta,\n    hicolor_rgb* image\n);\nhicolor_result hicolor_write_rgb_image(\n    FILE* stream,\n    const hicolor_metadata meta,\n    const hicolor_rgb* image\n);\n\n#endif /* HICOLOR_H */\n\n/* -------------------------------------------------------------------------- */\n\n#ifdef HICOLOR_IMPLEMENTATION\n\nconst char* hicolor_error_message(hicolor_result res)\n{\n    switch (res) {\n    case HICOLOR_OK:\n        return \"OK\";\n    case HICOLOR_IO_ERROR:\n        return \"I/O error\";\n    case HICOLOR_UNKNOWN_VERSION:\n        return \"unknown version\";\n    case HICOLOR_INVALID_VALUE:\n        return \"invalid value\";\n    case HICOLOR_INSUFFICIENT_DATA:\n        return \"insufficient data\";\n    case HICOLOR_BAD_MAGIC:\n        return \"bad magic value\";\n    default:\n        return \"\";\n    }\n}\n\nhicolor_result hicolor_char_to_version(\n    const uint8_t ch,\n    hicolor_version* version\n)\n{\n    switch (ch) {\n    case '5':\n        *version = HICOLOR_VERSION_5;\n        return HICOLOR_OK;\n    case '6':\n        *version = HICOLOR_VERSION_6;\n        return HICOLOR_OK;\n    default:\n        return HICOLOR_UNKNOWN_VERSION;\n    };\n}\n\n\nhicolor_result hicolor_version_to_char(\n    const hicolor_version version,\n    uint8_t* ch\n)\n{\n    switch (version) {\n    case HICOLOR_VERSION_5:\n        *ch = '5';\n        return HICOLOR_OK;\n    case HICOLOR_VERSION_6:\n        *ch = '6';\n        return HICOLOR_OK;\n    default:\n        return HICOLOR_UNKNOWN_VERSION;\n    };\n}\n\nhicolor_result hicolor_value_to_rgb(\n    const hicolor_version version,\n    const hicolor_value value,\n    hicolor_rgb* rgb\n)\n{\n    switch (version) {\n    case HICOLOR_VERSION_5:\n        if (value & 0x8000) return HICOLOR_INVALID_VALUE;\n        rgb->r = hicolor_32_to_256[value & 0x1f];\n        rgb->g = hicolor_32_to_256[(value & 0x3ff) >> 5];\n        rgb->b = hicolor_32_to_256[(value & 0x7fff) >> 10];\n        return HICOLOR_OK;\n    case HICOLOR_VERSION_6:\n        rgb->r = hicolor_32_to_256[value & 0x1f];\n        rgb->g = hicolor_64_to_256[(value & 0x7ff) >> 5];\n        rgb->b = hicolor_32_to_256[value >> 11];\n        return HICOLOR_OK;\n    default:\n        return HICOLOR_UNKNOWN_VERSION;\n    };\n}\n\nhicolor_result hicolor_rgb_to_value(\n    const hicolor_version version,\n    const hicolor_rgb rgb,\n    hicolor_value* value\n)\n{\n    switch (version) {\n    case HICOLOR_VERSION_5:\n        *value = hicolor_256_to_32[rgb.r]\n            | hicolor_256_to_32[rgb.g] << 5\n            | hicolor_256_to_32[rgb.b] << 10;\n        return HICOLOR_OK;\n    case HICOLOR_VERSION_6:\n        *value = hicolor_256_to_32[rgb.r]\n            | hicolor_256_to_64[rgb.g] << 5\n            | hicolor_256_to_32[rgb.b] << 11;\n        return HICOLOR_OK;\n    default:\n        return HICOLOR_UNKNOWN_VERSION;\n    };\n}\n\nhicolor_result hicolor_read_header(\n    FILE* stream,\n    hicolor_metadata* meta\n)\n{\n    size_t total = 0;\n    hicolor_result res;\n\n    uint8_t magic[7];\n    total += fread(magic, 1, sizeof(magic), stream);\n    if (memcmp(magic, hicolor_magic, sizeof(magic)) != 0) {\n        return HICOLOR_BAD_MAGIC;\n    }\n\n    uint8_t vch;\n    total += fread(&vch, 1, sizeof(vch), stream);\n    res = hicolor_char_to_version(vch, &meta->version);\n    if (res != HICOLOR_OK) {\n        return res;\n    }\n\n    uint8_t b[2];\n    total += fread(&b, 1, sizeof(b), stream);\n    meta->width = b[0] + (b[1] << 8);\n    total += fread(&b, 1, sizeof(b), stream);\n    meta->height = b[0] + (b[1] << 8);\n\n    if (total == 12) return HICOLOR_OK;\n\n    return HICOLOR_INSUFFICIENT_DATA;\n}\n\nhicolor_result hicolor_write_header(\n    FILE* stream,\n    const hicolor_metadata meta\n)\n{\n    size_t total = 0;\n\n    total += fwrite(hicolor_magic, 1, sizeof(hicolor_magic), stream);\n\n    uint8_t vch;\n    hicolor_result res = hicolor_version_to_char(meta.version, &vch);\n    if (res != HICOLOR_OK) return res;\n    total += fwrite(&vch, 1, sizeof(vch), stream);\n\n    uint8_t wb1 = meta.width & 0xff;\n    uint8_t wb2 = (meta.width >> 8) & 0xff;\n    total += fwrite(&wb1, 1, sizeof(wb1), stream);\n    total += fwrite(&wb2, 1, sizeof(wb2), stream);\n\n    uint8_t hb1 = meta.height & 0xff;\n    uint8_t hb2 = (meta.height >> 8) & 0xff;\n    total += fwrite(&hb1, 1, sizeof(hb1), stream);\n    total += fwrite(&hb2, 1, sizeof(hb2), stream);\n\n    if (total == 12) return HICOLOR_OK;\n\n    return HICOLOR_IO_ERROR;\n}\n\n/* \"a dither\" is a public-domain dithering algorithm by Øyvind Kolås.\n * This function implements pattern 3.\n * https://pippin.gimp.org/a_dither/\n */\nuint8_t hicolor_a_dither_channel(\n    uint8_t intensity,\n    uint16_t x,\n    uint16_t y,\n    double levels\n)\n{\n    double mask = (double) ((x + y * 237) * 119 & 255) / 255.0;\n    double normalized = (double) intensity / 255.0;\n    double dithered_normalized = floor(levels * normalized + mask) / levels;\n    if (dithered_normalized > 1) {\n        dithered_normalized = 1;\n    }\n\n    uint8_t result = dithered_normalized * 255;\n    return result;\n}\n\nvoid hicolor_a_dither_rgb(\n    hicolor_version version,\n    uint16_t x,\n    uint16_t y,\n    const hicolor_rgb rgb,\n    hicolor_rgb* output\n)\n{\n    double levels = 32.0;\n    double levels_g = version == HICOLOR_VERSION_5 ? levels : 64.0;\n\n    output->r = hicolor_a_dither_channel(rgb.r, x, y, levels);\n    output->g = hicolor_a_dither_channel(rgb.g, x, y, levels_g);\n    output->b = hicolor_a_dither_channel(rgb.b, x, y, levels);\n}\n\n/* Ordered (Bayer) dithering. */\nuint8_t hicolor_bayerize_channel(\n    uint8_t intensity,\n    double factor,\n    double step\n)\n{\n    double dithered = ((double) intensity) / 255 + step / 256 * factor;\n\n    double levels = 128.0 / step;\n    return (uint8_t) (round(dithered * levels) / levels * 255);\n}\n\nvoid hicolor_bayerize_rgb(\n    hicolor_version version,\n    uint16_t x,\n    uint16_t y,\n    const hicolor_rgb rgb,\n    hicolor_rgb* output\n)\n{\n    uint8_t bayer_coord =\n        (y % HICOLOR_BAYER_SIZE) * HICOLOR_BAYER_SIZE +\n        x % HICOLOR_BAYER_SIZE;\n    double factor = hicolor_bayer[bayer_coord];\n\n    double step = 8.0;\n    double step_g = version == HICOLOR_VERSION_5 ? step : 4.0;\n\n    output->r = hicolor_bayerize_channel(rgb.r, factor, step);\n    output->g = hicolor_bayerize_channel(rgb.g, factor, step_g);\n    output->b = hicolor_bayerize_channel(rgb.b, factor, step);\n}\n\nhicolor_result hicolor_quantize_rgb_image(\n    const hicolor_metadata meta,\n    hicolor_dither dither,\n    hicolor_rgb* image\n)\n{\n    hicolor_rgb rgb;\n    hicolor_value value;\n\n    for (uint16_t y = 0; y < meta.height; y++) {\n        for (uint16_t x = 0; x < meta.width; x++) {\n            rgb = image[y * meta.width + x];\n\n            hicolor_rgb quant_rgb = rgb;\n            if (dither == HICOLOR_A_DITHER) {\n                hicolor_a_dither_rgb(meta.version, x, y, rgb, &quant_rgb);\n            } else if (dither == HICOLOR_BAYER) {\n                hicolor_bayerize_rgb(meta.version, x, y, rgb, &quant_rgb);\n            }\n\n            hicolor_result res = hicolor_rgb_to_value(\n                meta.version,\n                quant_rgb,\n                &value\n            );\n            if (res != HICOLOR_OK) {\n                return res;\n            }\n\n            res = hicolor_value_to_rgb(\n                meta.version,\n                value,\n                &image[y * meta.width + x]\n            );\n            if (res != HICOLOR_OK) {\n                return res;\n            }\n        }\n    }\n\n    return HICOLOR_OK;\n};\n\nhicolor_result hicolor_read_rgb_image(\n    FILE* stream,\n    const hicolor_metadata meta,\n    hicolor_rgb* image\n)\n{\n    size_t total = 0;\n\n    for (int i = 0; i < meta.width * meta.height; i++) {\n        hicolor_value value;\n        total += fread(&value, 1, sizeof(value), stream);\n\n        hicolor_result res =\n            hicolor_value_to_rgb(meta.version, value, &image[i]);\n        if (res != HICOLOR_OK) return res;\n    }\n\n    if (total == 2 * meta.width * meta.height) return HICOLOR_OK;\n\n    return HICOLOR_INSUFFICIENT_DATA;\n}\n\nhicolor_result hicolor_write_rgb_image(\n    FILE* stream,\n    const hicolor_metadata meta,\n    const hicolor_rgb* image\n)\n{\n    size_t total = 0;\n\n    for (int i = 0; i < meta.width * meta.height; i++) {\n        hicolor_value value;\n        hicolor_result res =\n            hicolor_rgb_to_value(meta.version, image[i], &value);\n        if (res != HICOLOR_OK) return res;\n\n        total += fwrite(&value, 1, sizeof(value), stream);\n    }\n\n    if (total == 2 * meta.width * meta.height) return HICOLOR_OK;\n\n    return HICOLOR_IO_ERROR;\n}\n\n#endif /* HICOLOR_IMPLEMENTATION */\n"
  },
  {
    "path": "scripts/bayer-matrix.tcl",
    "content": "#! /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    40 24 36 20 43 27 39 23\n     2 50 14 62  1 49 13 61\n    34 18 46 30 33 17 45 29\n    10 58  6 54  9 57  5 53\n    42 26 38 22 41 25 37 21\n}\n\nset n 8\nset size [expr { $n * $n }]\n\nset fmt [lmap x $bm8 {\n    format %2i.0/%u $x $size\n}]\n\nfor {set i 0} {$i < $size} {incr i $n} {\n    lappend lines [join [lrange $fmt $i [expr { $i + $n - 1 }]] {, }]\n}\n\nputs \"\\n    [join $lines \",\\n    \"]\"\n"
  },
  {
    "path": "scripts/conversion-tables.tcl",
    "content": "#! /usr/bin/env tclsh\n\npackage require textutil\n\nset varDeclTemplate {static const uint8_t %s[] = {%s};}\n\nproc table {from to} {\n    set ratio [expr { $to * 1.0 / $from }]\n\n    for {set i 0} {$i < $from} {incr i} {\n        lappend table [expr { int($i * 1.0 / $from * ($to + $ratio) ) }]\n    }\n\n    return $table\n}\n\nproc format-table {name table} {\n    set lines [textutil::adjust [join $table {, }]]\n    set indented [join [split $lines \\n] \"\\n    \"]\n\n    return [format $::varDeclTemplate $name \"\\n    $indented\\n\"]\n}\n\nforeach {from to} {256 32 256 64 32 256 64 256} {\n    dict set tables hicolor_${from}_to_$to [table $from $to]\n}\n\nputs [join [lmap {key value} $tables {\n    format-table $key $value\n}] \\n\\n]\n"
  },
  {
    "path": "tests/hicolor.test",
    "content": "#! /usr/bin/env tclsh\n\npackage require Tcl 8.6 9\npackage require tcltest\n\ncd [file dirname [info script]]\n\nset hicolorCommand ../hicolor\nif {[info exists env(HICOLOR_COMMAND)]} {\n    set hicolorCommand $env(HICOLOR_COMMAND)\n}\n\ntry {\n    exec gm version\n} on ok _ {\n    tcltest::testConstraint gm true\n} on error _ {}\n\n\nproc hicolor args {\n    exec {*}$::hicolorCommand {*}$args\n}\n\nproc read-file path {\n    try {\n        set ch [open $path rb]\n        read $ch\n    } finally {\n        close $ch\n    }\n}\n\nproc prefixes s {\n    for {set i 0} {$i < [string length $s]} {incr i} {\n        lappend prefixes [string range $s 0 $i]\n    }\n\n    return $prefixes\n}\n\n\ntcltest::test version-1.1 {} -body {\n    hicolor version\n} -match regexp -result {\\d+\\.\\d+\\.\\d+}\n\ntcltest::test version-1.2 {} -body {\n    hicolor version file.hi5\n} -returnCodes error -match glob -result {*too many arg*}\n\ntcltest::test version-2.1 {} -body {\n    set action version\n    set output [hicolor $action]\n\n    lmap prefix [prefixes $action] {\n        expr {\n            [hicolor $prefix] eq $output\n        }\n    }\n} -match regexp -result {1(?: 1)+}\n\n\ntcltest::test help-1.1 {} -body {\n    hicolor help\n} -match glob -result *options:*\n\n\ntcltest::test help-1.2 {} -body {\n    hicolor -h\n} -match glob -result *options:*\n\n\ntcltest::test help-1.3 {} -body {\n    hicolor --help\n} -match glob -result *options:*\n\n\ntcltest::test encode-1.1 {} -body {\n    hicolor encode\n} -returnCodes error -match glob -result {*no source image given to command\\\n    \"encode\"*}\n\ntcltest::test encode-1.2 {} -body {\n    hicolor encode photo.png\n} -result {}\n\ntcltest::test encode-1.3 {} -body {\n    hicolor encode photo.png photo.png.hic\n} -result {}\n\ntcltest::test encode-1.4 {} -body {\n    hicolor encode photo.png photo.png.hic ascii.txt 2>@1\n} -returnCodes error -match glob -result {*too many arg*}\n\ntcltest::test encode-1.5 {} -body {\n    hicolor encode photo.png photo.png.hic ascii.txt foo bar baz 2>@1\n} -returnCodes error -match glob -result {*too many arg*}\n\ntcltest::test encode-1.6 {} -body {\n    hicolor encode [file tail [info script]]\n} -returnCodes error -match glob -result {*Not a PNG file*}\n\n\ntcltest::test encode-2.1 {encode flags} -body {\n    hicolor encode -5 photo.png photo.png.hic\n    hicolor info photo.png.hic\n} -result {5 640 427}\n\ntcltest::test encode-2.2 {encode flags} -body {\n    hicolor encode --15-bit photo.png photo.png.hic\n    hicolor info photo.png.hic\n} -result {5 640 427}\n\ntcltest::test encode-2.3 {encode flags} -body {\n    hicolor encode -6 photo.png photo.png.hic\n    hicolor info photo.png.hic\n} -result {6 640 427}\n\ntcltest::test encode-2.4 {encode flags} -body {\n    hicolor encode --16-bit photo.png photo.png.hic\n    hicolor info photo.png.hic\n} -result {6 640 427}\n\ntcltest::test encode-2.5 {encode flags} -body {\n    hicolor encode --16-bit photo.png\n    hicolor info photo.png.hic\n} -result {6 640 427}\n\ntcltest::test encode-2.6 {encode flags} -body {\n    hicolor encode -5 -6 -5 photo.png\n    hicolor info photo.png.hic\n} -result {5 640 427}\n\ntcltest::test encode-2.7 {encode flags} -body {\n    hicolor encode -n -n -n -n -n photo.png\n} -result {}\n\ntcltest::test encode-2.8 {encode flags} -body {\n    hicolor encode -6 -n -5 photo.png\n    hicolor info photo.png.hic\n} -result {5 640 427}\n\ntcltest::test encode-2.9 {} -body {\n    hicolor encode -b -a -n -a -n -a photo.png\n} -result {}\n\n\ntcltest::test encode-3.1 {bad input} -body {\n    hicolor encode truncated.png\n} -returnCodes error -result {error: can't load PNG file \"truncated.png\":\\\n    Read Error}\n\ntcltest::test encode-3.2 {bad input} -body {\n    hicolor encode wrong-size.png\n} -returnCodes error -match glob -result {error: can't load PNG file*}\n\n\nhicolor encode --15-bit photo.png photo.hi5\nhicolor encode --15-bit --a-dither photo.png photo-a-dither.hi5\nhicolor encode --16-bit photo.png photo.hi6\nhicolor encode --16-bit --a-dither photo.png photo-a-dither.hi6\n\n\ntcltest::test decode-1.1 {15-bit} -body {\n    hicolor decode photo.hi5\n    file exists photo.hi5.png\n} -result 1\n\ntcltest::test decode-1.2 {16-bit} -body {\n    hicolor decode photo.hi6\n    file exists photo.hi6.png\n} -result 1\n\ntcltest::test decode-1.3 {bad input} -body {\n    hicolor decode [file tail [info script]]\n} -returnCodes error -match glob -result {*bad magic*}\n\ntcltest::test decode-1.4 {bad input} -body {\n    hicolor decode -5 photo.hi5\n} -returnCodes error -match glob -result *error:*\n\n\ntcltest::test quantize-1.1 {} -body {\n    hicolor quantize photo.png photo.16-bit.png\n} -result {}\n\ntcltest::test quantize-1.2 {} -body {\n    hicolor quantize -5 photo.png photo.15-bit.png\n} -result {}\n\n\ntcltest::test quantize-2.1 {bad input} -body {\n    hicolor encode [file tail [info script]]\n} -returnCodes error -match glob -result {*Not a PNG file*}\n\ntcltest::test quantize-2.2 {bad input} -body {\n    hicolor quantize truncated.png\n} -returnCodes error -result {error: can't load PNG file \"truncated.png\":\\\n    Read Error}\n\ntcltest::test quantize-2.3 {bad input} -body {\n    hicolor quantize wrong-size.png\n} -returnCodes error -match glob -result {error: can't load PNG file*}\n\n\ntcltest::test unknown-command-1.1 {} -body {\n    hicolor -5 src.png\n} -returnCodes error -match glob -result {usage:*error: unknown command \"-5\"}\n\ntcltest::test unknown-command-1.2 {} -body {\n    hicolor encoder\n} -returnCodes error -match glob -result {usage:*error: unknown command \"encoder\"}\n\n\ntcltest::test unknown-option-1.1 {} -body {\n    hicolor encode --wrong fo bar\n} -returnCodes error -match glob -result {usage:*error: unknown option \"--wrong\"}\n\n\ntcltest::test no-arguments-1.1 {} -body {\n    hicolor\n} -returnCodes error -match glob -result *options:*\n\n\ntcltest::test dash-dash-1.1 {} -body {\n    hicolor encode -- --no-such-file\n} -returnCodes error -match glob -result {error: source image \"--no-such-file\"\\\n    doesn't exist}\n\n\ntcltest::test data-integrity-1.1 {roundtrip} -constraints gm -body {\n    hicolor decode photo.hi5 temp.png\n    exec gm compare -metric rmse photo.png temp.png\n} -match regexp -result {Total: 0.0[12]}\n\ntcltest::test data-integrity-1.2 {roundtrip} -constraints gm -body {\n    hicolor decode photo.hi6 temp.png\n    exec gm compare -metric rmse photo.png temp.png\n} -match regexp -result {Total: 0.0[12]}\n\ntcltest::test data-integrity-2.1 {roundtrip with \"a dither\"} -constraints gm -body {\n    hicolor decode photo-a-dither.hi5 temp.png\n    exec gm compare -metric rmse photo.png temp.png\n} -match regexp -result {Total: 0.0[12]}\n\ntcltest::test data-integrity-2.2 {roundtrip with \"a dither\"} -constraints gm -body {\n    hicolor decode photo-a-dither.hi6 temp.png\n    exec gm compare -metric rmse photo.png temp.png\n} -match regexp -result {Total: 0.0[12]}\n\ntcltest::test data-integrity-3.1 {alpha roundtrip} -constraints gm -body {\n    hicolor quant alpha.png alpha-q.png\n    exec gm compare -metric rmse alpha.png alpha-q.png\n} -match regexp -result {Total: 0.0+ }\n\n\nincr failed [expr {$tcltest::numTests(Failed) > 0}]\ntcltest::cleanupTests\n\nif {$failed > 0} {\n    exit 1\n}\n"
  }
]