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 (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 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 [“high color”](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 [“a dither”](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 “a dither” 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–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] [--] [] hicolor decode [] hicolor info 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 [“plane in flight”](https://unsplash.com/photos/AwtncJT1qKs) (`bordeaux-15bit.png`) by olaf wisser. [“houses beside trees”](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 #include #include #include #include #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] [--] []\n" " hicolor decode []\n" " hicolor info \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 #include #include #include #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 }