├── .github └── workflows │ ├── ci.yml │ └── install-deps.sh ├── .gitignore ├── AUTHORS ├── GNUmakefile ├── LICENSE ├── README.md ├── bordeaux-15bit.png ├── cli.c ├── format.md ├── hicolor.h ├── scripts ├── bayer-matrix.tcl └── conversion-tables.tcl └── tests ├── alpha.png ├── hicolor.test ├── photo.png ├── truncated.png └── wrong-size.png /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | bsd: 5 | runs-on: ${{ matrix.os.host }} 6 | strategy: 7 | matrix: 8 | os: 9 | - name: freebsd 10 | architecture: x86-64 11 | version: '14.1' 12 | host: ubuntu-latest 13 | 14 | - name: netbsd 15 | architecture: x86-64 16 | version: '10.0' 17 | host: ubuntu-latest 18 | 19 | - name: openbsd 20 | architecture: x86-64 21 | version: '7.5' 22 | host: ubuntu-latest 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | 27 | - name: Run CI script on ${{ matrix.os.name }} 28 | uses: cross-platform-actions/action@v0.25.0 29 | with: 30 | operating_system: ${{ matrix.os.name }} 31 | architecture: ${{ matrix.os.architecture }} 32 | version: ${{ matrix.os.version }} 33 | shell: bash 34 | run: | 35 | # Use sudo(1) rather than doas(1) on OpenBSD. 36 | # doas(1) isn't configured. 37 | # See https://github.com/cross-platform-actions/action/issues/75 38 | sudo .github/workflows/install-deps.sh 39 | gmake test 40 | 41 | linux: 42 | runs-on: ubuntu-latest 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Install dependencies 48 | run: | 49 | sudo .github/workflows/install-deps.sh 50 | 51 | - name: Test 52 | run: | 53 | gmake test 54 | 55 | - name: Upload artifacts 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: hicolor-linux-x86_64 59 | path: | 60 | hicolor 61 | 62 | mac: 63 | runs-on: macos-latest 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | 68 | - name: Install dependencies 69 | run: | 70 | .github/workflows/install-deps.sh 71 | 72 | - name: Build and test 73 | run: | 74 | make test 75 | 76 | - name: Bundle dynamic libraries 77 | run: | 78 | dylibbundler --bundle-deps --create-dir --fix-file hicolor 79 | 80 | - name: Upload artifacts 81 | uses: actions/upload-artifact@v4 82 | with: 83 | name: hicolor-macos-arm64 84 | path: | 85 | hicolor 86 | libs/ 87 | 88 | windows: 89 | runs-on: windows-latest 90 | steps: 91 | - name: 'Disable `autocrlf` in Git' 92 | run: git config --global core.autocrlf false 93 | 94 | - name: Checkout 95 | uses: actions/checkout@v4 96 | 97 | - name: Set up MSYS2 98 | uses: msys2/setup-msys2@v2 99 | with: 100 | update: true 101 | msystem: mingw32 102 | install: | 103 | make 104 | mingw-w64-i686-gcc 105 | mingw-w64-i686-libpng 106 | mingw-w64-i686-pkgconf 107 | mingw-w64-i686-zlib 108 | tcl 109 | 110 | - name: Test 111 | shell: msys2 {0} 112 | run: | 113 | make test 114 | 115 | - name: Upload artifacts 116 | uses: actions/upload-artifact@v4 117 | with: 118 | name: hicolor-win32 119 | path: | 120 | hicolor.exe 121 | -------------------------------------------------------------------------------- /.github/workflows/install-deps.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | set -e 3 | 4 | if [ "$(uname)" = Darwin ]; then 5 | brew install dylibbundler tcl-tk 6 | fi 7 | 8 | if [ "$(uname)" = Linux ]; then 9 | apt-get install -y graphicsmagick libpng-dev pkgconf 10 | fi 11 | 12 | if [ "$(uname)" = FreeBSD ]; then 13 | pkg install -y gmake GraphicsMagick pkgconf png tcl86 14 | ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh 15 | fi 16 | 17 | if [ "$(uname)" = NetBSD ]; then 18 | pkgin -y install gmake GraphicsMagick pkgconf png tcl zlib 19 | fi 20 | 21 | if [ "$(uname)" = OpenBSD ]; then 22 | pkg_add -I gmake GraphicsMagick pkgconf png tcl%8.6 23 | ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh 24 | fi 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /attic/ 2 | /bin/ 3 | /hicolor 4 | /hicolor.exe 5 | /tests/*.hi* 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # The AUTHORS Certificate 2 | # First edition, Fourteenth draft 3 | # 4 | # By proposing a change to this project that adds a line like 5 | # 6 | # Name (URL) [Working For] 7 | # 8 | # below, you certify: 9 | # 10 | # 1. All of your contributions to this project are and will be your own, 11 | # original work, licensed on the same terms as this project. 12 | # 13 | # 2. If someone else might own intellectual property in your 14 | # contributions, like an employer or client, you've added their legal 15 | # name in square brackets and got their written permission to submit 16 | # your contribution. 17 | # 18 | # 3. If you haven't added a name in square brackets, you are sure that 19 | # you have the legal right to license all your contributions so far 20 | # by yourself. 21 | # 22 | # 4. If you make any future contribution under different intellectual 23 | # property circumstances, you'll propose a change to add another line 24 | # to this file for that contribution. 25 | # 26 | # 5. The name, e-mail, and URL you've added to this file are yours. You 27 | # understand the project will make this file public. 28 | D. Bohdan https://dbohdan.com/ 29 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | PLATFORM ?= $(shell uname) 2 | 3 | ifneq ($(PLATFORM), Darwin) 4 | PLATFORM_CFLAGS ?= -static -Wl,--gc-sections 5 | endif 6 | 7 | LIBPNG_CFLAGS ?= $(shell pkg-config --cflags libpng) 8 | LIBPNG_LIBS ?= $(shell pkg-config --libs libpng) 9 | ZLIB_CFLAGS ?= $(shell pkg-config --cflags zlib) 10 | ZLIB_LIBS ?= $(shell pkg-config --libs zlib) 11 | 12 | CFLAGS ?= -std=c99 -g -O3 $(PLATFORM_CFLAGS) -ffunction-sections -fdata-sections -Wall -Wextra $(LIBPNG_CFLAGS) $(ZLIB_CFLAGS) 13 | LIBS ?= $(LIBPNG_LIBS) $(ZLIB_LIBS) -lm 14 | PREFIX ?= /usr/local 15 | 16 | all: hicolor 17 | 18 | hicolor: cli.c hicolor.h 19 | $(CC) $< -o $@ $(CFLAGS) $(LIBS) 20 | 21 | clean: clean-no-ext clean-exe 22 | clean-exe: 23 | -rm -f hicolor.exe 24 | clean-no-ext: 25 | -rm -f hicolor 26 | 27 | install: install-bin install-include 28 | install-bin: hicolor 29 | install $< $(DESTDIR)$(PREFIX)/bin/hicolor 30 | install-include: hicolor.h 31 | install -m 0644 $< $(DESTDIR)$(PREFIX)/include 32 | 33 | uninstall: uninstall-bin uninstall-include 34 | uninstall-bin: 35 | -rm $(DESTDIR)$(PREFIX)/bin/hicolor 36 | uninstall-include: 37 | -rm $(DESTDIR)$(PREFIX)/include/hicolor.h 38 | 39 | release: clean-no-ext test 40 | cp hicolor hicolor-v"$$(./hicolor version | head -n 1 | awk '{ print $$2 }')"-"$$(uname | tr 'A-Z' 'a-z')"-"$$(uname -m)" 41 | 42 | test: all 43 | tests/hicolor.test 44 | 45 | .PHONY: all clean clean-exe clean-no-ext install install-bin install-include release test uninstall uninstall-bin uninstall-include 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, 2023-2025 D. Bohdan and contributors listed in AUTHORS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HiColor 2 | 3 | ![A building with a dithered gradient of the sky behind it. 4 | A jet airplane is taking off in the sky.](bordeaux-15bit.png) 5 | 6 | *(The image above has 15-bit color.)* 7 | 8 | HiColor is a program and a C library for converting images to 15- and 16-bit RGB color, 9 | the color depth of old display modes known as [“high color”](https://en.wikipedia.org/wiki/High_color). 10 | I wrote it because I wanted to create images with the characteristic high-color look. 11 | 12 | ## Contents 13 | 14 | - [Description](#description) 15 | - [Known bugs and limitations](#known-bugs-and-limitations) 16 | - [Usage](#usage) 17 | - [Building](#building) 18 | - [Alternatives](#alternatives) 19 | - [License](#license) 20 | 21 | ## Description 22 | 23 | HiColor reduces images to two-byte 15- or 16-bit color. 24 | In 15-bit mode images have 5 bits for each of red, green, and blue, with the last bit reserved. 25 | In 16-bit mode green, the color the human eye is generally most sensitive to, is given 6 bits. 26 | 27 | HiColor implements its own simple [file format](format.md) and converts between this format and PNG. 28 | It can also convert standard PNG to standard PNG with only high-color color values. 29 | (This simulates a roundtrip through HiColor without creating a temporary file.) 30 | HiColor files have either the extension `.hic` or `.hi5` for 15-bit and `.hi6` for 16-bit respectively. 31 | 32 | By default, 33 | HiColor applies the [Bayer ordered dithering](https://en.wikipedia.org/wiki/Ordered_dithering) algorithm 34 | to reduce the quantization error 35 | (the difference between the original and the high-color pixel). 36 | Historical software and hardware used it for dithering in high-color modes. 37 | HiColor can also use [“a dither”](https://pippin.gimp.org/a_dither/) instead. 38 | Dithering can be selected or disabled with command-line flags. 39 | 40 | Quantized images compress better than their originals, 41 | so HiColor can be a less-lossy alternative to the 256-color [pngquant](https://pngquant.org/). 42 | Quantizing a PNG file to PNG preserves transparency (but does not quantize the alpha channel). 43 | Conversion to and from the HiColor format does not preserve transparency. 44 | 45 | The program is written in C with two external dependencies, libpng and zlib, and builds as a static binary. 46 | It is known to work on 47 | Linux (aarch64, i386, riscv64, x86_64), 48 | FreeBSD, 49 | NetBSD, 50 | OpenBSD, 51 | and Windows 98 Second Edition, 52 | 2000 Service Pack 4, 53 | XP, 54 | 7, 55 | and 10. 56 | 57 | The library is a single C99 header file. 58 | It is designed to be easy to understand and modify 59 | at a cost to performance. 60 | The design makes it unsuitable for real-time graphics. 61 | 62 | ## Known bugs and limitations 63 | 64 | ### Security 65 | 66 | The command-line program (but not the library) was vulnerable to malicious PNG files 67 | because it used a PNG library intended only for trusted input. 68 | The vulnerabilities were fixed in version 0.6.0 by switching to libpng. 69 | 70 | ### PNG file size 71 | 72 | PNG files produced by HiColor are not highly optimized. 73 | Run them through [OptiPNG](http://optipng.sourceforge.net/) or [Oxipng](https://github.com/shssoichiro/oxipng) to significantly reduce their size. 74 | 75 | ### Generation loss 76 | 77 | With Bayer dithering or no dithering, there is no [generation loss](https://en.wikipedia.org/wiki/Generation_loss) after the initial quantization. 78 | Applying “a dither” repeatedly to the same image will result in generation loss. 79 | In tests the loss converges to zero after 32 or 64 generations 80 | (in 15-bit and 16-bit mode respectively). 81 | 82 | HiColor 0.1.0–0.2.1 suffered from generation loss with Bayer dithering due to an implementation error. 83 | The error was fixed in version 0.3.0. 84 | 85 | ## Usage 86 | 87 | HiColor has a Git-style CLI. 88 | 89 | The actions `encode` and `decode` convert images between PNG and HiColor's own image format. 90 | `quantize` round-trips an image through the converter and outputs a standard 32-bit PNG. 91 | Use `quantize` to create high-color images readable by other programs. 92 | `info` prints information about a HiColor file: version (`5` for 15-bit or `6` for 16), width, and height. 93 | 94 | ```none 95 | HiColor 1.0.1 96 | Create 15/16-bit color RGB images. 97 | 98 | usage: 99 | hicolor (encode|quantize) [-5|-6] [-a|-b|-n] [--] [] 100 | hicolor decode [] 101 | hicolor info 102 | hicolor (version|help|-h|--help) 103 | 104 | commands: 105 | encode convert PNG to HiColor 106 | decode convert HiColor to PNG 107 | quantize quantize PNG to PNG 108 | info print HiColor image version and resolution 109 | version print version of HiColor, libpng, and zlib 110 | help print this help message 111 | 112 | options: 113 | -5, --15-bit 15-bit color 114 | -6, --16-bit 16-bit color 115 | -a, --a-dither dither image with "a dither" 116 | -b, --bayer dither image with Bayer algorithm (default) 117 | -n, --no-dither do not dither image 118 | ``` 119 | 120 | ## Building 121 | 122 | ### Debian/Ubuntu 123 | 124 | ```sh 125 | sudo apt install -y build-essential graphicsmagick linpng-dev pkgconf tclsh zlib1g-dev 126 | gmake test 127 | ``` 128 | 129 | ### FreeBSD 130 | 131 | ```sh 132 | sudo pkg install -y gmake GraphicsMagick pkgconf png tcl86 133 | ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh 134 | fi 135 | gmake test 136 | ``` 137 | 138 | ### macOS 139 | 140 | Install [Homebrew](https://brew.sh/). 141 | Run the following commands in a clone of the HiColor repository. 142 | 143 | ```sh 144 | brew install libpng tcl-tk 145 | make test 146 | ``` 147 | 148 | ### NetBSD 149 | 150 | ```sh 151 | sudo pkgin -y install gmake GraphicsMagick pkgconf png tcl zlib 152 | gmake test 153 | ``` 154 | 155 | ### OpenBSD 156 | 157 | ```sh 158 | doas pkg_add -I gmake GraphicsMagick pkgconf png tcl%8.6 159 | ln -s /usr/local/bin/tclsh8.6 /usr/local/bin/tclsh 160 | gmake test 161 | ``` 162 | 163 | ### Windows 164 | 165 | Install [MSYS2](https://www.msys2.org/). 166 | Run the following commands in the MSYS2 mingw32 shell 167 | in a clone of the HiColor repository. 168 | This will build an x86 executable for Windows. 169 | 170 | ```sh 171 | pacman -Syuu make mingw-w64-i686-gcc mingw-w64-i686-libpng mingw-w64-i686-pkgconf mingw-w64-i686-zlib tcl 172 | make test 173 | ``` 174 | 175 | ## Alternatives 176 | 177 | I wrote HiColor because nothing seemed to support high color. 178 | Actually, 179 | [FFmpeg](https://www.madox.net/blog/2011/06/06/converting-tofrom-rgb565-in-ubuntu-using-ffmpeg/), 180 | [GIMP](https://docs.gimp.org/2.10/en/gimp-filter-dither.html), 181 | and 182 | [ImageMagick](https://www.imagemagick.org/Usage/quantize/#16bit_colormap) 183 | can reduce images to 15- and 16-bit color. 184 | What differentiates HiColor is being a small dedicated tool and embeddable C library and having its own file format. 185 | 186 | ## License 187 | 188 | MIT. 189 | 190 | HiColor uses [libpng](http://www.libpng.org/pub/png/libpng.html) and [zlib](https://www.zlib.net/). 191 | Follow the links for their respective licenses. 192 | 193 | ### Photos from Unsplash 194 | 195 | [“plane in flight”](https://unsplash.com/photos/AwtncJT1qKs) (`bordeaux-15bit.png`) by olaf wisser. 196 | 197 | [“houses beside trees”](https://unsplash.com/photos/PWBXQJ7PUkI) (`tests/photo.png`) by Orlova Maria. 198 | 199 | #### License 200 | 201 | > 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. 202 | -------------------------------------------------------------------------------- /bordeaux-15bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/hicolor/1c437f80229a4d7a49e2804b3d79c1896e8c46ad/bordeaux-15bit.png -------------------------------------------------------------------------------- /cli.c: -------------------------------------------------------------------------------- 1 | /* HiColor CLI. 2 | * 3 | * Copyright (c) 2021, 2023-2024 D. Bohdan and contributors listed in AUTHORS. 4 | * License: MIT. 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #define HICOLOR_IMPLEMENTATION 15 | #include "hicolor.h" 16 | 17 | #define HICOLOR_CLI_ERROR "error: " 18 | #define HICOLOR_CLI_LIB_NAME_FORMAT "%-9s" 19 | #define HICOLOR_CLI_LIBPNG_COMPRESSION_LEVEL 6 20 | #define HICOLOR_CLI_NO_MEMORY_EXIT_CODE 255 21 | 22 | #define HICOLOR_CLI_CMD_ENCODE "encode" 23 | #define HICOLOR_CLI_CMD_QUANTIZE "quantize" 24 | #define HICOLOR_CLI_CMD_DECODE "decode" 25 | #define HICOLOR_CLI_CMD_INFO "info" 26 | #define HICOLOR_CLI_CMD_VERSION "version" 27 | #define HICOLOR_CLI_CMD_HELP "help" 28 | 29 | const char* png_error_msg = "no error recorded"; 30 | 31 | void libpng_error_handler( 32 | png_structp png_ptr, 33 | png_const_charp error_msg 34 | ) 35 | { 36 | png_error_msg = error_msg; 37 | longjmp(png_jmpbuf(png_ptr), 1); 38 | } 39 | 40 | bool load_png( 41 | const char* filename, 42 | int* width, 43 | int* height, 44 | hicolor_rgb** rgb_img, 45 | uint8_t** alpha 46 | ) 47 | { 48 | FILE* fp = fopen(filename, "rb"); 49 | if (!fp) { 50 | png_error_msg = "failed to open for reading"; 51 | return false; 52 | } 53 | 54 | png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, libpng_error_handler, NULL); 55 | if (png == NULL) { 56 | png_error_msg = "`png_create_read_struct` returned null"; 57 | fclose(fp); 58 | return false; 59 | } 60 | 61 | png_infop info = png_create_info_struct(png); 62 | if (info == NULL) { 63 | png_error_msg = "`png_create_info_struct` returned null"; 64 | png_destroy_read_struct(&png, NULL, NULL); 65 | fclose(fp); 66 | return false; 67 | } 68 | 69 | if (setjmp(png_jmpbuf(png))) { 70 | /* Do not overwrite `png_error_msg` set by the handler. */ 71 | png_destroy_read_struct(&png, &info, NULL); 72 | fclose(fp); 73 | return false; 74 | } 75 | 76 | png_init_io(png, fp); 77 | png_read_info(png, info); 78 | 79 | *width = png_get_image_width(png, info); 80 | *height = png_get_image_height(png, info); 81 | png_byte color_type = png_get_color_type(png, info); 82 | png_byte bit_depth = png_get_bit_depth(png, info); 83 | 84 | if (bit_depth == 16) { 85 | png_set_strip_16(png); 86 | } 87 | 88 | if (color_type == PNG_COLOR_TYPE_PALETTE) { 89 | png_set_palette_to_rgb(png); 90 | } 91 | 92 | if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) { 93 | png_set_expand_gray_1_2_4_to_8(png); 94 | } 95 | 96 | if (png_get_valid(png, info, PNG_INFO_tRNS)) { 97 | png_set_tRNS_to_alpha(png); 98 | } 99 | 100 | if (color_type == PNG_COLOR_TYPE_RGB 101 | || color_type == PNG_COLOR_TYPE_GRAY 102 | || color_type == PNG_COLOR_TYPE_PALETTE) { 103 | png_set_filler(png, 0xFF, PNG_FILLER_AFTER); 104 | } 105 | 106 | if (color_type == PNG_COLOR_TYPE_GRAY 107 | || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { 108 | png_set_gray_to_rgb(png); 109 | } 110 | 111 | png_read_update_info(png, info); 112 | 113 | *rgb_img = malloc(sizeof(hicolor_rgb) * *width * *height); 114 | *alpha = malloc(sizeof(uint8_t) * *width * *height); 115 | 116 | if (*rgb_img == NULL || *alpha == NULL) { 117 | png_error_msg = "failed to allocate memory for `rgb_img` or `alpha`"; 118 | png_destroy_read_struct(&png, &info, NULL); 119 | fclose(fp); 120 | return false; 121 | } 122 | 123 | png_bytep row = malloc(png_get_rowbytes(png, info)); 124 | if (row == NULL) { 125 | png_error_msg = "failed to allocate memory for `row`"; 126 | free(*rgb_img); 127 | free(*alpha); 128 | png_destroy_read_struct(&png, &info, NULL); 129 | fclose(fp); 130 | return false; 131 | } 132 | 133 | for (int y = 0; y < *height; y++) { 134 | png_read_row(png, row, NULL); 135 | 136 | for (int x = 0; x < *width; x++) { 137 | png_bytep pixel = &(row[x * 4]); 138 | (*rgb_img)[y * (*width) + x].r = pixel[0]; 139 | (*rgb_img)[y * (*width) + x].g = pixel[1]; 140 | (*rgb_img)[y * (*width) + x].b = pixel[2]; 141 | (*alpha)[y * (*width) + x] = pixel[3]; 142 | } 143 | } 144 | 145 | free(row); 146 | png_destroy_read_struct(&png, &info, NULL); 147 | fclose(fp); 148 | 149 | return true; 150 | } 151 | 152 | bool save_png( 153 | const char* filename, 154 | int width, 155 | int height, 156 | const hicolor_rgb* rgb_img, 157 | const uint8_t* alpha 158 | ) 159 | { 160 | FILE* fp = fopen(filename, "wb"); 161 | if (!fp) { 162 | png_error_msg = "failed to open for writing"; 163 | return false; 164 | } 165 | 166 | png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, libpng_error_handler, NULL); 167 | if (png == NULL) { 168 | png_error_msg = "`png_create_write_struct` returned null"; 169 | fclose(fp); 170 | return false; 171 | } 172 | 173 | png_infop info = png_create_info_struct(png); 174 | if (info == NULL) { 175 | png_error_msg = "`png_create_info_struct` returned null"; 176 | png_destroy_write_struct(&png, NULL); 177 | fclose(fp); 178 | return false; 179 | } 180 | 181 | if (setjmp(png_jmpbuf(png))) { 182 | /* Do not overwrite `png_error_msg` set by the handler. */ 183 | png_destroy_write_struct(&png, &info); 184 | fclose(fp); 185 | return false; 186 | } 187 | 188 | png_init_io(png, fp); 189 | 190 | png_set_IHDR( 191 | png, 192 | info, 193 | width, 194 | height, 195 | 8, 196 | PNG_COLOR_TYPE_RGBA, 197 | PNG_INTERLACE_NONE, 198 | PNG_COMPRESSION_TYPE_DEFAULT, 199 | PNG_FILTER_TYPE_DEFAULT 200 | ); 201 | png_set_compression_level(png, HICOLOR_CLI_LIBPNG_COMPRESSION_LEVEL); 202 | png_write_info(png, info); 203 | 204 | png_bytep row = malloc(png_get_rowbytes(png, info)); 205 | if (row == NULL) { 206 | png_error_msg = "failed to allocate memory for `row`"; 207 | png_destroy_write_struct(&png, &info); 208 | fclose(fp); 209 | return false; 210 | } 211 | 212 | for (int y = 0; y < height; y++) { 213 | for (int x = 0; x < width; x++) { 214 | png_bytep pixel = &(row[x * 4]); 215 | pixel[0] = rgb_img[y * width + x].r; 216 | pixel[1] = rgb_img[y * width + x].g; 217 | pixel[2] = rgb_img[y * width + x].b; 218 | pixel[3] = alpha == NULL ? 255 : alpha[y * width + x]; 219 | } 220 | 221 | png_write_row(png, row); 222 | } 223 | 224 | free(row); 225 | png_write_end(png, NULL); 226 | png_destroy_write_struct(&png, &info); 227 | fclose(fp); 228 | 229 | return true; 230 | } 231 | 232 | bool check_and_report_error( 233 | char* step, 234 | hicolor_result res 235 | ) 236 | { 237 | if (res == HICOLOR_OK) { 238 | return false; 239 | } 240 | 241 | fprintf( 242 | stderr, 243 | HICOLOR_CLI_ERROR "%s: %s\n", 244 | step, 245 | hicolor_error_message(res) 246 | ); 247 | 248 | return true; 249 | } 250 | 251 | bool check_src_exists( 252 | const char* src 253 | ) 254 | { 255 | if (access(src, F_OK) != 0) { 256 | fprintf( 257 | stderr, 258 | HICOLOR_CLI_ERROR "source image \"%s\" doesn't exist\n", 259 | src 260 | ); 261 | return false; 262 | } 263 | 264 | return true; 265 | } 266 | 267 | bool png_to_hicolor( 268 | hicolor_version version, 269 | hicolor_dither dither, 270 | const char* src, 271 | const char* dest 272 | ) 273 | { 274 | hicolor_result res; 275 | 276 | bool exists = check_src_exists(src); 277 | if (!exists) { 278 | return false; 279 | } 280 | 281 | int width, height; 282 | hicolor_rgb* rgb_img = NULL; 283 | uint8_t* alpha = NULL; 284 | if (!load_png(src, &width, &height, &rgb_img, &alpha)) { 285 | fprintf( 286 | stderr, 287 | HICOLOR_CLI_ERROR "can't load PNG file \"%s\": %s\n", 288 | src, 289 | png_error_msg 290 | ); 291 | return false; 292 | } 293 | 294 | FILE* hi_file = fopen(dest, "wb"); 295 | if (hi_file == NULL) { 296 | fprintf( 297 | stderr, 298 | HICOLOR_CLI_ERROR "can't open file \"%s\" for writing\n", 299 | dest 300 | ); 301 | 302 | free(rgb_img); 303 | return false; 304 | } 305 | 306 | hicolor_metadata meta = { 307 | .version = version, 308 | .width = width, 309 | .height = height 310 | }; 311 | res = hicolor_write_header(hi_file, meta); 312 | 313 | bool success = false; 314 | if (check_and_report_error("can't write header", res)) { 315 | goto clean_up_file; 316 | } 317 | 318 | res = hicolor_quantize_rgb_image(meta, dither, rgb_img); 319 | if (check_and_report_error("can't quantize image", res)) { 320 | goto clean_up_images; 321 | } 322 | 323 | res = hicolor_write_rgb_image(hi_file, meta, rgb_img); 324 | if (check_and_report_error("can't write image data", res)) { 325 | goto clean_up_images; 326 | } 327 | 328 | success = true; 329 | 330 | clean_up_images: 331 | free(rgb_img); 332 | 333 | clean_up_file: 334 | fclose(hi_file); 335 | 336 | return success; 337 | } 338 | 339 | bool png_quantize( 340 | hicolor_version version, 341 | hicolor_dither dither, 342 | const char* src, 343 | const char* dest 344 | ) 345 | { 346 | hicolor_result res; 347 | 348 | bool exists = check_src_exists(src); 349 | if (!exists) { 350 | return false; 351 | } 352 | 353 | int width, height; 354 | hicolor_rgb* rgb_img = NULL; 355 | uint8_t* alpha = NULL; 356 | if (!load_png(src, &width, &height, &rgb_img, &alpha)) { 357 | fprintf( 358 | stderr, 359 | HICOLOR_CLI_ERROR "can't load PNG file \"%s\": %s\n", 360 | src, 361 | png_error_msg 362 | ); 363 | return false; 364 | } 365 | 366 | hicolor_metadata meta = { 367 | .version = version, 368 | .width = width, 369 | .height = height 370 | }; 371 | 372 | res = hicolor_quantize_rgb_image(meta, dither, rgb_img); 373 | bool success = false; 374 | if (check_and_report_error("can't quantize image", res)) { 375 | goto clean_up_images; 376 | } 377 | 378 | if (!save_png(dest, width, height, rgb_img, alpha)) { 379 | fprintf( 380 | stderr, 381 | HICOLOR_CLI_ERROR "can't save PNG: %s\n", 382 | png_error_msg 383 | ); 384 | goto clean_up_images; 385 | } 386 | 387 | success = true; 388 | 389 | clean_up_images: 390 | free(rgb_img); 391 | 392 | return success; 393 | } 394 | 395 | bool hicolor_to_png( 396 | const char* src, 397 | const char* dest 398 | ) 399 | { 400 | hicolor_result res; 401 | 402 | bool exists = check_src_exists(src); 403 | if (!exists) { 404 | return false; 405 | } 406 | 407 | FILE* hi_file = fopen(src, "rb"); 408 | if (hi_file == NULL) { 409 | fprintf(stderr, HICOLOR_CLI_ERROR "can't open source image \"%s\" for reading\n", src); 410 | return false; 411 | } 412 | 413 | hicolor_metadata meta; 414 | res = hicolor_read_header(hi_file, &meta); 415 | bool success = false; 416 | if (check_and_report_error("can't read header", res)) { 417 | goto clean_up_file; 418 | } 419 | 420 | hicolor_rgb* rgb_img = malloc(sizeof(hicolor_rgb) * meta.width * meta.height); 421 | if (rgb_img == NULL) { 422 | goto clean_up_file; 423 | } 424 | res = hicolor_read_rgb_image(hi_file, meta, rgb_img); 425 | if (check_and_report_error("can't read image data", res)) { 426 | goto clean_up_rgb_img; 427 | } 428 | 429 | if (!save_png(dest, meta.width, meta.height, rgb_img, NULL)) { 430 | fprintf( 431 | stderr, 432 | HICOLOR_CLI_ERROR "can't save PNG: %s\n", 433 | png_error_msg 434 | ); 435 | goto clean_up_rgb_img; 436 | } 437 | 438 | success = true; 439 | 440 | clean_up_rgb_img: 441 | free(rgb_img); 442 | 443 | clean_up_file: 444 | fclose(hi_file); 445 | 446 | return success; 447 | } 448 | 449 | bool hicolor_print_info( 450 | const char* src 451 | ) 452 | { 453 | hicolor_result res; 454 | 455 | bool exists = check_src_exists(src); 456 | if (!exists) { 457 | return false; 458 | } 459 | 460 | FILE* hi_file = fopen(src, "rb"); 461 | if (hi_file == NULL) { 462 | fprintf( 463 | stderr, 464 | HICOLOR_CLI_ERROR "can't open source image \"%s\" for reading\n", 465 | src 466 | ); 467 | return false; 468 | } 469 | 470 | hicolor_metadata meta; 471 | res = hicolor_read_header(hi_file, &meta); 472 | bool success = false; 473 | if (check_and_report_error("can't read header", res)) { 474 | goto clean_up_file; 475 | } 476 | 477 | uint8_t vch = '\0'; 478 | res = hicolor_version_to_char(meta.version, &vch); 479 | if (check_and_report_error("can't decode version", res)) { 480 | goto clean_up_file; 481 | } 482 | 483 | printf( 484 | "%c %i %i\n", 485 | vch, 486 | meta.width, 487 | meta.height 488 | ); 489 | 490 | success = true; 491 | 492 | clean_up_file: 493 | fclose(hi_file); 494 | 495 | return success; 496 | } 497 | 498 | void usage( 499 | FILE* output 500 | ) 501 | { 502 | fprintf( 503 | output, 504 | "usage:\n" 505 | " hicolor (encode|quantize) [-5|-6] [-a|-b|-n] [--] []\n" 506 | " hicolor decode []\n" 507 | " hicolor info \n" 508 | " hicolor (version|help|-h|--help)\n" 509 | ); 510 | } 511 | 512 | void version( 513 | bool full 514 | ) 515 | { 516 | if (full) { 517 | printf( 518 | HICOLOR_CLI_LIB_NAME_FORMAT, 519 | "HiColor" 520 | ); 521 | } 522 | 523 | uint32_t program_version = HICOLOR_LIBRARY_VERSION; 524 | printf( 525 | "%u.%u.%u\n", 526 | program_version / 10000, 527 | program_version % 10000 / 100, 528 | program_version % 100 529 | ); 530 | 531 | if (!full) { 532 | return; 533 | } 534 | 535 | png_uint_32 libpng_version = png_access_version_number(); 536 | printf( 537 | HICOLOR_CLI_LIB_NAME_FORMAT "%u.%u.%u\n", 538 | "libpng", 539 | libpng_version / 10000, 540 | libpng_version % 10000 / 100, 541 | libpng_version % 100 542 | ); 543 | 544 | printf( 545 | HICOLOR_CLI_LIB_NAME_FORMAT "%s\n", 546 | "zlib", 547 | ZLIB_VERSION 548 | ); 549 | } 550 | 551 | void help() 552 | { 553 | printf( 554 | "HiColor " 555 | ); 556 | version(false); 557 | printf( 558 | "Create 15/16-bit color RGB images.\n\n" 559 | ); 560 | usage(stdout); 561 | printf( 562 | "\ncommands:\n" 563 | " encode convert PNG to HiColor\n" 564 | " decode convert HiColor to PNG\n" 565 | " quantize quantize PNG to PNG\n" 566 | " info print HiColor image version and resolution\n" 567 | " version print version of HiColor, libpng, and zlib\n" 568 | " help print this help message\n" 569 | "\noptions:\n" 570 | " -5, --15-bit 15-bit color\n" 571 | " -6, --16-bit 16-bit color\n" 572 | " -a, --a-dither dither image with \"a dither\"\n" 573 | " -b, --bayer dither image with Bayer algorithm (default)\n" 574 | " -n, --no-dither do not dither image\n" 575 | ); 576 | } 577 | 578 | bool str_prefix( 579 | const char* ref, 580 | const char* str 581 | ) 582 | { 583 | size_t i; 584 | 585 | for (i = 0; str[i] != '\0'; i++) { 586 | if (str[i] != ref[i]) { 587 | return false; 588 | } 589 | } 590 | 591 | if (i == 0) { 592 | return false; 593 | } 594 | 595 | return true; 596 | } 597 | 598 | typedef enum command { 599 | ENCODE, DECODE, QUANTIZE, INFO, VERSION, HELP 600 | } command; 601 | 602 | int main( 603 | int argc, 604 | char** argv 605 | ) 606 | { 607 | command opt_command = ENCODE; 608 | hicolor_dither opt_dither = HICOLOR_BAYER; 609 | hicolor_version opt_version = HICOLOR_VERSION_6; 610 | const char* command_name; 611 | char* arg_src; 612 | char* arg_dest; 613 | bool allow_opts = true; 614 | int min_pos_args = 1; 615 | int max_pos_args = 2; 616 | 617 | if (argc <= 1) { 618 | help(); 619 | return 1; 620 | } 621 | 622 | /* The regular "help" command is handled later with the rest. */ 623 | for (int i = 0; i < argc; i++) { 624 | if (strcmp(argv[i], "-h") == 0 625 | || strcmp(argv[i], "--help") == 0) { 626 | help(); 627 | return 0; 628 | } 629 | } 630 | 631 | int i = 1; 632 | 633 | if (str_prefix(HICOLOR_CLI_CMD_ENCODE, argv[i])) { 634 | command_name = HICOLOR_CLI_CMD_ENCODE; 635 | opt_command = ENCODE; 636 | } else if (str_prefix(HICOLOR_CLI_CMD_DECODE, argv[i])) { 637 | allow_opts = false; 638 | command_name = HICOLOR_CLI_CMD_DECODE; 639 | opt_command = DECODE; 640 | } else if (str_prefix(HICOLOR_CLI_CMD_QUANTIZE, argv[i])) { 641 | command_name = HICOLOR_CLI_CMD_QUANTIZE; 642 | opt_command = QUANTIZE; 643 | } else if (str_prefix(HICOLOR_CLI_CMD_INFO, argv[i])) { 644 | allow_opts = false; 645 | command_name = HICOLOR_CLI_CMD_INFO; 646 | max_pos_args = 1; 647 | opt_command = INFO; 648 | } else if (str_prefix(HICOLOR_CLI_CMD_VERSION, argv[i])) { 649 | allow_opts = false; 650 | command_name = HICOLOR_CLI_CMD_VERSION; 651 | min_pos_args = 0; 652 | max_pos_args = 0; 653 | opt_command = VERSION; 654 | } else if (str_prefix(HICOLOR_CLI_CMD_HELP, argv[i])) { 655 | allow_opts = false; 656 | command_name = HICOLOR_CLI_CMD_HELP; 657 | min_pos_args = 0; 658 | max_pos_args = 0; 659 | opt_command = HELP; 660 | } else { 661 | usage(stderr); 662 | fprintf( 663 | stderr, 664 | "\n" HICOLOR_CLI_ERROR "unknown command \"%s\"\n", 665 | argv[i] 666 | ); 667 | return 1; 668 | } 669 | 670 | i++; 671 | 672 | if (allow_opts) { 673 | while (i < argc && argv[i][0] == '-') { 674 | if (strcmp(argv[i], "--") == 0) { 675 | i++; 676 | break; 677 | } else if (strcmp(argv[i], "-5") == 0 678 | || strcmp(argv[i], "--15-bit") == 0) { 679 | opt_version = HICOLOR_VERSION_5; 680 | } else if (strcmp(argv[i], "-6") == 0 681 | || strcmp(argv[i], "--16-bit") == 0) { 682 | opt_version = HICOLOR_VERSION_6; 683 | } else if (strcmp(argv[i], "-a") == 0 684 | || strcmp(argv[i], "--a-dither") == 0) { 685 | opt_dither = HICOLOR_A_DITHER; 686 | } else if (strcmp(argv[i], "-b") == 0 687 | || strcmp(argv[i], "--bayer") == 0) { 688 | opt_dither = HICOLOR_BAYER; 689 | } else if (strcmp(argv[i], "-n") == 0 690 | || strcmp(argv[i], "--no-dither") == 0) { 691 | opt_dither = HICOLOR_NO_DITHER; 692 | } else { 693 | usage(stderr); 694 | fprintf( 695 | stderr, 696 | "\n" HICOLOR_CLI_ERROR "unknown option \"%s\"\n", 697 | argv[i] 698 | ); 699 | return 1; 700 | } 701 | 702 | i++; 703 | } 704 | } 705 | 706 | int rem_args = argc - i; 707 | 708 | if (rem_args < min_pos_args) { 709 | usage(stderr); 710 | fprintf( 711 | stderr, 712 | "\n" HICOLOR_CLI_ERROR "no source image given to command \"%s\"\n", 713 | command_name 714 | ); 715 | return 1; 716 | } 717 | 718 | if (rem_args > max_pos_args) { 719 | usage(stderr); 720 | fprintf( 721 | stderr, 722 | "\n" HICOLOR_CLI_ERROR "too many arguments to command \"%s\"\n", 723 | command_name 724 | ); 725 | return 1; 726 | } 727 | 728 | arg_src = argv[i]; 729 | i++; 730 | 731 | if (i == argc) { 732 | arg_dest = malloc(strlen(arg_src) + 5); 733 | if (arg_dest == NULL) { 734 | return HICOLOR_CLI_NO_MEMORY_EXIT_CODE; 735 | } 736 | sprintf( 737 | arg_dest, 738 | opt_command == ENCODE ? "%s.hic" : "%s.png", 739 | arg_src 740 | ); 741 | } else { 742 | arg_dest = argv[i]; 743 | } 744 | i++; 745 | 746 | switch (opt_command) { 747 | case ENCODE: 748 | return !png_to_hicolor(opt_version, opt_dither, arg_src, arg_dest); 749 | case DECODE: 750 | return !hicolor_to_png(arg_src, arg_dest); 751 | case QUANTIZE: 752 | return !png_quantize(opt_version, opt_dither, arg_src, arg_dest); 753 | case INFO: 754 | return !hicolor_print_info(arg_src); 755 | case VERSION: 756 | version(true); 757 | return 0; 758 | case HELP: 759 | help(); 760 | return 0; 761 | } 762 | } 763 | -------------------------------------------------------------------------------- /format.md: -------------------------------------------------------------------------------- 1 | # File format 2 | 3 | - Magic: 7 bytes, `HiColor`. 4 | - Version: 1 byte, `5` for 15-bit color, `6` for 16-bit color. 5 | Other versions may be added later. 6 | - Width: 2 bytes: WB1, WB2. 7 | Width = WB1 + 256×WB2. 8 | - Height: 2 bytes: HB1, HB2. 9 | Height = HB1 + 256×HB2. 10 | - Data: Width×Height×2 bytes. 11 | The Data consist of Width×Height Values. 12 | Each Value is 2 bytes: VB1, VB2. 13 | Value = VB1 + 256×VB2. 14 | 15 | The Data part encodes the lines of pixels comprising the image from top to bottom and each line from left to right. 16 | The first Value of the data is the top-left pixel, the next is the one to its right, etc. 17 | 18 | ## Values 19 | 20 | - Version `5`: 21 | - 5 bits red, 5 bits green, 5 bits blue, 0. 22 | - Version `6`: 23 | - 5 bits red, 6 bits green, 5 bits blue. 24 | -------------------------------------------------------------------------------- /hicolor.h: -------------------------------------------------------------------------------- 1 | /* HiColor image file format encoder/decoder library. 2 | * 3 | * Copyright (c) 2021, 2023-2025 D. Bohdan and contributors listed in AUTHORS. 4 | * License: MIT. 5 | * 6 | * This header file contains both the interface and the implementation for 7 | * HiColor. To instantiate the implementation put the line 8 | * #define HICOLOR_IMPLEMENTATION 9 | * in a single source code file of your project above where you include this 10 | * file. 11 | */ 12 | 13 | #ifndef HICOLOR_H 14 | #define HICOLOR_H 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #define HICOLOR_BAYER_SIZE 8 22 | #define HICOLOR_LIBRARY_VERSION 10001 23 | 24 | /* Types. */ 25 | 26 | static const uint8_t hicolor_magic[7] = "HiColor"; 27 | 28 | /* These arrays are generated with `scripts/conversion-tables.tcl`. */ 29 | static const uint8_t hicolor_256_to_32[] = { 30 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 31 | 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 32 | 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 33 | 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 34 | 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 35 | 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 36 | 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 37 | 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 38 | 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 39 | 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 40 | 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 41 | 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 42 | 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 43 | 31, 31 44 | }; 45 | 46 | static const uint8_t hicolor_256_to_64[] = { 47 | 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 48 | 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 49 | 11, 11, 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 50 | 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 51 | 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 52 | 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 53 | 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 32, 32, 32, 32, 33, 33, 33, 33, 54 | 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 55 | 38, 38, 39, 39, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42, 56 | 43, 43, 43, 43, 44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 57 | 47, 47, 48, 48, 48, 48, 49, 49, 49, 49, 50, 50, 50, 50, 51, 51, 51, 51, 58 | 52, 52, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 56, 56, 59 | 56, 56, 57, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59, 60, 60, 60, 60, 60 | 61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 63 61 | }; 62 | 63 | static const uint8_t hicolor_32_to_256[] = { 64 | 0, 8, 16, 24, 33, 41, 49, 57, 66, 74, 82, 90, 99, 107, 115, 123, 132, 65 | 140, 148, 156, 165, 173, 181, 189, 198, 206, 214, 222, 231, 239, 247, 66 | 255 67 | }; 68 | 69 | static const uint8_t hicolor_64_to_256[] = { 70 | 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 65, 69, 73, 71 | 77, 81, 85, 89, 93, 97, 101, 105, 109, 113, 117, 121, 125, 130, 134, 72 | 138, 142, 146, 150, 154, 158, 162, 166, 170, 174, 178, 182, 186, 190, 73 | 195, 199, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 243, 247, 74 | 251, 255 75 | }; 76 | 77 | /* The values in this array are the output of 78 | * `scripts/bayer-matrix.tcl`. 79 | */ 80 | static const double hicolor_bayer[HICOLOR_BAYER_SIZE * HICOLOR_BAYER_SIZE] = { 81 | 0.0/64, 48.0/64, 12.0/64, 60.0/64, 3.0/64, 51.0/64, 15.0/64, 63.0/64, 82 | 32.0/64, 16.0/64, 44.0/64, 28.0/64, 35.0/64, 19.0/64, 47.0/64, 31.0/64, 83 | 8.0/64, 56.0/64, 4.0/64, 52.0/64, 11.0/64, 59.0/64, 7.0/64, 55.0/64, 84 | 40.0/64, 24.0/64, 36.0/64, 20.0/64, 43.0/64, 27.0/64, 39.0/64, 23.0/64, 85 | 2.0/64, 50.0/64, 14.0/64, 62.0/64, 1.0/64, 49.0/64, 13.0/64, 61.0/64, 86 | 34.0/64, 18.0/64, 46.0/64, 30.0/64, 33.0/64, 17.0/64, 45.0/64, 29.0/64, 87 | 10.0/64, 58.0/64, 6.0/64, 54.0/64, 9.0/64, 57.0/64, 5.0/64, 53.0/64, 88 | 42.0/64, 26.0/64, 38.0/64, 22.0/64, 41.0/64, 25.0/64, 37.0/64, 21.0/64 89 | }; 90 | 91 | typedef enum hicolor_version { 92 | HICOLOR_VERSION_5, 93 | HICOLOR_VERSION_6 94 | } hicolor_version; 95 | 96 | typedef struct hicolor_metadata { 97 | hicolor_version version; 98 | uint16_t width; 99 | uint16_t height; 100 | } hicolor_metadata; 101 | 102 | typedef enum hicolor_result { 103 | HICOLOR_OK, 104 | HICOLOR_IO_ERROR, 105 | HICOLOR_UNKNOWN_VERSION, 106 | HICOLOR_INVALID_VALUE, 107 | HICOLOR_INSUFFICIENT_DATA, 108 | HICOLOR_BAD_MAGIC 109 | } hicolor_result; 110 | 111 | typedef enum hicolor_dither { 112 | HICOLOR_A_DITHER, 113 | HICOLOR_BAYER, 114 | HICOLOR_NO_DITHER 115 | } hicolor_dither; 116 | 117 | typedef struct hicolor_rgb { 118 | uint8_t r; 119 | uint8_t g; 120 | uint8_t b; 121 | } hicolor_rgb; 122 | 123 | typedef uint16_t hicolor_value; 124 | 125 | /* Functions. */ 126 | 127 | const char* hicolor_error_message(hicolor_result res); 128 | 129 | hicolor_result hicolor_char_to_version( 130 | const uint8_t ch, 131 | hicolor_version* version 132 | ); 133 | hicolor_result hicolor_version_to_char( 134 | const hicolor_version version, 135 | uint8_t* ch 136 | ); 137 | 138 | hicolor_result hicolor_value_to_rgb( 139 | const hicolor_version version, 140 | const hicolor_value value, 141 | hicolor_rgb* rgb 142 | ); 143 | hicolor_result hicolor_rgb_to_value( 144 | const hicolor_version version, 145 | const hicolor_rgb rgb, 146 | hicolor_value* value 147 | ); 148 | 149 | hicolor_result hicolor_read_header( 150 | FILE* stream, 151 | hicolor_metadata* meta 152 | ); 153 | hicolor_result hicolor_write_header( 154 | FILE* stream, 155 | const hicolor_metadata meta 156 | ); 157 | 158 | /* Quantize using optional dithering. */ 159 | hicolor_result hicolor_quantize_rgb_image( 160 | const hicolor_metadata meta, 161 | hicolor_dither dither, 162 | hicolor_rgb* image 163 | ); 164 | 165 | hicolor_result hicolor_read_rgb_image( 166 | FILE* stream, 167 | const hicolor_metadata meta, 168 | hicolor_rgb* image 169 | ); 170 | hicolor_result hicolor_write_rgb_image( 171 | FILE* stream, 172 | const hicolor_metadata meta, 173 | const hicolor_rgb* image 174 | ); 175 | 176 | #endif /* HICOLOR_H */ 177 | 178 | /* -------------------------------------------------------------------------- */ 179 | 180 | #ifdef HICOLOR_IMPLEMENTATION 181 | 182 | const char* hicolor_error_message(hicolor_result res) 183 | { 184 | switch (res) { 185 | case HICOLOR_OK: 186 | return "OK"; 187 | case HICOLOR_IO_ERROR: 188 | return "I/O error"; 189 | case HICOLOR_UNKNOWN_VERSION: 190 | return "unknown version"; 191 | case HICOLOR_INVALID_VALUE: 192 | return "invalid value"; 193 | case HICOLOR_INSUFFICIENT_DATA: 194 | return "insufficient data"; 195 | case HICOLOR_BAD_MAGIC: 196 | return "bad magic value"; 197 | default: 198 | return ""; 199 | } 200 | } 201 | 202 | hicolor_result hicolor_char_to_version( 203 | const uint8_t ch, 204 | hicolor_version* version 205 | ) 206 | { 207 | switch (ch) { 208 | case '5': 209 | *version = HICOLOR_VERSION_5; 210 | return HICOLOR_OK; 211 | case '6': 212 | *version = HICOLOR_VERSION_6; 213 | return HICOLOR_OK; 214 | default: 215 | return HICOLOR_UNKNOWN_VERSION; 216 | }; 217 | } 218 | 219 | 220 | hicolor_result hicolor_version_to_char( 221 | const hicolor_version version, 222 | uint8_t* ch 223 | ) 224 | { 225 | switch (version) { 226 | case HICOLOR_VERSION_5: 227 | *ch = '5'; 228 | return HICOLOR_OK; 229 | case HICOLOR_VERSION_6: 230 | *ch = '6'; 231 | return HICOLOR_OK; 232 | default: 233 | return HICOLOR_UNKNOWN_VERSION; 234 | }; 235 | } 236 | 237 | hicolor_result hicolor_value_to_rgb( 238 | const hicolor_version version, 239 | const hicolor_value value, 240 | hicolor_rgb* rgb 241 | ) 242 | { 243 | switch (version) { 244 | case HICOLOR_VERSION_5: 245 | if (value & 0x8000) return HICOLOR_INVALID_VALUE; 246 | rgb->r = hicolor_32_to_256[value & 0x1f]; 247 | rgb->g = hicolor_32_to_256[(value & 0x3ff) >> 5]; 248 | rgb->b = hicolor_32_to_256[(value & 0x7fff) >> 10]; 249 | return HICOLOR_OK; 250 | case HICOLOR_VERSION_6: 251 | rgb->r = hicolor_32_to_256[value & 0x1f]; 252 | rgb->g = hicolor_64_to_256[(value & 0x7ff) >> 5]; 253 | rgb->b = hicolor_32_to_256[value >> 11]; 254 | return HICOLOR_OK; 255 | default: 256 | return HICOLOR_UNKNOWN_VERSION; 257 | }; 258 | } 259 | 260 | hicolor_result hicolor_rgb_to_value( 261 | const hicolor_version version, 262 | const hicolor_rgb rgb, 263 | hicolor_value* value 264 | ) 265 | { 266 | switch (version) { 267 | case HICOLOR_VERSION_5: 268 | *value = hicolor_256_to_32[rgb.r] 269 | | hicolor_256_to_32[rgb.g] << 5 270 | | hicolor_256_to_32[rgb.b] << 10; 271 | return HICOLOR_OK; 272 | case HICOLOR_VERSION_6: 273 | *value = hicolor_256_to_32[rgb.r] 274 | | hicolor_256_to_64[rgb.g] << 5 275 | | hicolor_256_to_32[rgb.b] << 11; 276 | return HICOLOR_OK; 277 | default: 278 | return HICOLOR_UNKNOWN_VERSION; 279 | }; 280 | } 281 | 282 | hicolor_result hicolor_read_header( 283 | FILE* stream, 284 | hicolor_metadata* meta 285 | ) 286 | { 287 | size_t total = 0; 288 | hicolor_result res; 289 | 290 | uint8_t magic[7]; 291 | total += fread(magic, 1, sizeof(magic), stream); 292 | if (memcmp(magic, hicolor_magic, sizeof(magic)) != 0) { 293 | return HICOLOR_BAD_MAGIC; 294 | } 295 | 296 | uint8_t vch; 297 | total += fread(&vch, 1, sizeof(vch), stream); 298 | res = hicolor_char_to_version(vch, &meta->version); 299 | if (res != HICOLOR_OK) { 300 | return res; 301 | } 302 | 303 | uint8_t b[2]; 304 | total += fread(&b, 1, sizeof(b), stream); 305 | meta->width = b[0] + (b[1] << 8); 306 | total += fread(&b, 1, sizeof(b), stream); 307 | meta->height = b[0] + (b[1] << 8); 308 | 309 | if (total == 12) return HICOLOR_OK; 310 | 311 | return HICOLOR_INSUFFICIENT_DATA; 312 | } 313 | 314 | hicolor_result hicolor_write_header( 315 | FILE* stream, 316 | const hicolor_metadata meta 317 | ) 318 | { 319 | size_t total = 0; 320 | 321 | total += fwrite(hicolor_magic, 1, sizeof(hicolor_magic), stream); 322 | 323 | uint8_t vch; 324 | hicolor_result res = hicolor_version_to_char(meta.version, &vch); 325 | if (res != HICOLOR_OK) return res; 326 | total += fwrite(&vch, 1, sizeof(vch), stream); 327 | 328 | uint8_t wb1 = meta.width & 0xff; 329 | uint8_t wb2 = (meta.width >> 8) & 0xff; 330 | total += fwrite(&wb1, 1, sizeof(wb1), stream); 331 | total += fwrite(&wb2, 1, sizeof(wb2), stream); 332 | 333 | uint8_t hb1 = meta.height & 0xff; 334 | uint8_t hb2 = (meta.height >> 8) & 0xff; 335 | total += fwrite(&hb1, 1, sizeof(hb1), stream); 336 | total += fwrite(&hb2, 1, sizeof(hb2), stream); 337 | 338 | if (total == 12) return HICOLOR_OK; 339 | 340 | return HICOLOR_IO_ERROR; 341 | } 342 | 343 | /* "a dither" is a public-domain dithering algorithm by Øyvind Kolås. 344 | * This function implements pattern 3. 345 | * https://pippin.gimp.org/a_dither/ 346 | */ 347 | uint8_t hicolor_a_dither_channel( 348 | uint8_t intensity, 349 | uint16_t x, 350 | uint16_t y, 351 | double levels 352 | ) 353 | { 354 | double mask = (double) ((x + y * 237) * 119 & 255) / 255.0; 355 | double normalized = (double) intensity / 255.0; 356 | double dithered_normalized = floor(levels * normalized + mask) / levels; 357 | if (dithered_normalized > 1) { 358 | dithered_normalized = 1; 359 | } 360 | 361 | uint8_t result = dithered_normalized * 255; 362 | return result; 363 | } 364 | 365 | void hicolor_a_dither_rgb( 366 | hicolor_version version, 367 | uint16_t x, 368 | uint16_t y, 369 | const hicolor_rgb rgb, 370 | hicolor_rgb* output 371 | ) 372 | { 373 | double levels = 32.0; 374 | double levels_g = version == HICOLOR_VERSION_5 ? levels : 64.0; 375 | 376 | output->r = hicolor_a_dither_channel(rgb.r, x, y, levels); 377 | output->g = hicolor_a_dither_channel(rgb.g, x, y, levels_g); 378 | output->b = hicolor_a_dither_channel(rgb.b, x, y, levels); 379 | } 380 | 381 | /* Ordered (Bayer) dithering. */ 382 | uint8_t hicolor_bayerize_channel( 383 | uint8_t intensity, 384 | double factor, 385 | double step 386 | ) 387 | { 388 | double dithered = ((double) intensity) / 255 + step / 256 * factor; 389 | 390 | double levels = 128.0 / step; 391 | return (uint8_t) (round(dithered * levels) / levels * 255); 392 | } 393 | 394 | void hicolor_bayerize_rgb( 395 | hicolor_version version, 396 | uint16_t x, 397 | uint16_t y, 398 | const hicolor_rgb rgb, 399 | hicolor_rgb* output 400 | ) 401 | { 402 | uint8_t bayer_coord = 403 | (y % HICOLOR_BAYER_SIZE) * HICOLOR_BAYER_SIZE + 404 | x % HICOLOR_BAYER_SIZE; 405 | double factor = hicolor_bayer[bayer_coord]; 406 | 407 | double step = 8.0; 408 | double step_g = version == HICOLOR_VERSION_5 ? step : 4.0; 409 | 410 | output->r = hicolor_bayerize_channel(rgb.r, factor, step); 411 | output->g = hicolor_bayerize_channel(rgb.g, factor, step_g); 412 | output->b = hicolor_bayerize_channel(rgb.b, factor, step); 413 | } 414 | 415 | hicolor_result hicolor_quantize_rgb_image( 416 | const hicolor_metadata meta, 417 | hicolor_dither dither, 418 | hicolor_rgb* image 419 | ) 420 | { 421 | hicolor_rgb rgb; 422 | hicolor_value value; 423 | 424 | for (uint16_t y = 0; y < meta.height; y++) { 425 | for (uint16_t x = 0; x < meta.width; x++) { 426 | rgb = image[y * meta.width + x]; 427 | 428 | hicolor_rgb quant_rgb = rgb; 429 | if (dither == HICOLOR_A_DITHER) { 430 | hicolor_a_dither_rgb(meta.version, x, y, rgb, &quant_rgb); 431 | } else if (dither == HICOLOR_BAYER) { 432 | hicolor_bayerize_rgb(meta.version, x, y, rgb, &quant_rgb); 433 | } 434 | 435 | hicolor_result res = hicolor_rgb_to_value( 436 | meta.version, 437 | quant_rgb, 438 | &value 439 | ); 440 | if (res != HICOLOR_OK) { 441 | return res; 442 | } 443 | 444 | res = hicolor_value_to_rgb( 445 | meta.version, 446 | value, 447 | &image[y * meta.width + x] 448 | ); 449 | if (res != HICOLOR_OK) { 450 | return res; 451 | } 452 | } 453 | } 454 | 455 | return HICOLOR_OK; 456 | }; 457 | 458 | hicolor_result hicolor_read_rgb_image( 459 | FILE* stream, 460 | const hicolor_metadata meta, 461 | hicolor_rgb* image 462 | ) 463 | { 464 | size_t total = 0; 465 | 466 | for (int i = 0; i < meta.width * meta.height; i++) { 467 | hicolor_value value; 468 | total += fread(&value, 1, sizeof(value), stream); 469 | 470 | hicolor_result res = 471 | hicolor_value_to_rgb(meta.version, value, &image[i]); 472 | if (res != HICOLOR_OK) return res; 473 | } 474 | 475 | if (total == 2 * meta.width * meta.height) return HICOLOR_OK; 476 | 477 | return HICOLOR_INSUFFICIENT_DATA; 478 | } 479 | 480 | hicolor_result hicolor_write_rgb_image( 481 | FILE* stream, 482 | const hicolor_metadata meta, 483 | const hicolor_rgb* image 484 | ) 485 | { 486 | size_t total = 0; 487 | 488 | for (int i = 0; i < meta.width * meta.height; i++) { 489 | hicolor_value value; 490 | hicolor_result res = 491 | hicolor_rgb_to_value(meta.version, image[i], &value); 492 | if (res != HICOLOR_OK) return res; 493 | 494 | total += fwrite(&value, 1, sizeof(value), stream); 495 | } 496 | 497 | if (total == 2 * meta.width * meta.height) return HICOLOR_OK; 498 | 499 | return HICOLOR_IO_ERROR; 500 | } 501 | 502 | #endif /* HICOLOR_IMPLEMENTATION */ 503 | -------------------------------------------------------------------------------- /scripts/bayer-matrix.tcl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh 2 | 3 | set bm8 { 4 | 0 48 12 60 3 51 15 63 5 | 32 16 44 28 35 19 47 31 6 | 8 56 4 52 11 59 7 55 7 | 40 24 36 20 43 27 39 23 8 | 2 50 14 62 1 49 13 61 9 | 34 18 46 30 33 17 45 29 10 | 10 58 6 54 9 57 5 53 11 | 42 26 38 22 41 25 37 21 12 | } 13 | 14 | set n 8 15 | set size [expr { $n * $n }] 16 | 17 | set fmt [lmap x $bm8 { 18 | format %2i.0/%u $x $size 19 | }] 20 | 21 | for {set i 0} {$i < $size} {incr i $n} { 22 | lappend lines [join [lrange $fmt $i [expr { $i + $n - 1 }]] {, }] 23 | } 24 | 25 | puts "\n [join $lines ",\n "]" 26 | -------------------------------------------------------------------------------- /scripts/conversion-tables.tcl: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh 2 | 3 | package require textutil 4 | 5 | set varDeclTemplate {static const uint8_t %s[] = {%s};} 6 | 7 | proc table {from to} { 8 | set ratio [expr { $to * 1.0 / $from }] 9 | 10 | for {set i 0} {$i < $from} {incr i} { 11 | lappend table [expr { int($i * 1.0 / $from * ($to + $ratio) ) }] 12 | } 13 | 14 | return $table 15 | } 16 | 17 | proc format-table {name table} { 18 | set lines [textutil::adjust [join $table {, }]] 19 | set indented [join [split $lines \n] "\n "] 20 | 21 | return [format $::varDeclTemplate $name "\n $indented\n"] 22 | } 23 | 24 | foreach {from to} {256 32 256 64 32 256 64 256} { 25 | dict set tables hicolor_${from}_to_$to [table $from $to] 26 | } 27 | 28 | puts [join [lmap {key value} $tables { 29 | format-table $key $value 30 | }] \n\n] 31 | -------------------------------------------------------------------------------- /tests/alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/hicolor/1c437f80229a4d7a49e2804b3d79c1896e8c46ad/tests/alpha.png -------------------------------------------------------------------------------- /tests/hicolor.test: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env tclsh 2 | 3 | package require Tcl 8.6 9 4 | package require tcltest 5 | 6 | cd [file dirname [info script]] 7 | 8 | set hicolorCommand ../hicolor 9 | if {[info exists env(HICOLOR_COMMAND)]} { 10 | set hicolorCommand $env(HICOLOR_COMMAND) 11 | } 12 | 13 | try { 14 | exec gm version 15 | } on ok _ { 16 | tcltest::testConstraint gm true 17 | } on error _ {} 18 | 19 | 20 | proc hicolor args { 21 | exec {*}$::hicolorCommand {*}$args 22 | } 23 | 24 | proc read-file path { 25 | try { 26 | set ch [open $path rb] 27 | read $ch 28 | } finally { 29 | close $ch 30 | } 31 | } 32 | 33 | proc prefixes s { 34 | for {set i 0} {$i < [string length $s]} {incr i} { 35 | lappend prefixes [string range $s 0 $i] 36 | } 37 | 38 | return $prefixes 39 | } 40 | 41 | 42 | tcltest::test version-1.1 {} -body { 43 | hicolor version 44 | } -match regexp -result {\d+\.\d+\.\d+} 45 | 46 | tcltest::test version-1.2 {} -body { 47 | hicolor version file.hi5 48 | } -returnCodes error -match glob -result {*too many arg*} 49 | 50 | tcltest::test version-2.1 {} -body { 51 | set action version 52 | set output [hicolor $action] 53 | 54 | lmap prefix [prefixes $action] { 55 | expr { 56 | [hicolor $prefix] eq $output 57 | } 58 | } 59 | } -match regexp -result {1(?: 1)+} 60 | 61 | 62 | tcltest::test help-1.1 {} -body { 63 | hicolor help 64 | } -match glob -result *options:* 65 | 66 | 67 | tcltest::test help-1.2 {} -body { 68 | hicolor -h 69 | } -match glob -result *options:* 70 | 71 | 72 | tcltest::test help-1.3 {} -body { 73 | hicolor --help 74 | } -match glob -result *options:* 75 | 76 | 77 | tcltest::test encode-1.1 {} -body { 78 | hicolor encode 79 | } -returnCodes error -match glob -result {*no source image given to command\ 80 | "encode"*} 81 | 82 | tcltest::test encode-1.2 {} -body { 83 | hicolor encode photo.png 84 | } -result {} 85 | 86 | tcltest::test encode-1.3 {} -body { 87 | hicolor encode photo.png photo.png.hic 88 | } -result {} 89 | 90 | tcltest::test encode-1.4 {} -body { 91 | hicolor encode photo.png photo.png.hic ascii.txt 2>@1 92 | } -returnCodes error -match glob -result {*too many arg*} 93 | 94 | tcltest::test encode-1.5 {} -body { 95 | hicolor encode photo.png photo.png.hic ascii.txt foo bar baz 2>@1 96 | } -returnCodes error -match glob -result {*too many arg*} 97 | 98 | tcltest::test encode-1.6 {} -body { 99 | hicolor encode [file tail [info script]] 100 | } -returnCodes error -match glob -result {*Not a PNG file*} 101 | 102 | 103 | tcltest::test encode-2.1 {encode flags} -body { 104 | hicolor encode -5 photo.png photo.png.hic 105 | hicolor info photo.png.hic 106 | } -result {5 640 427} 107 | 108 | tcltest::test encode-2.2 {encode flags} -body { 109 | hicolor encode --15-bit photo.png photo.png.hic 110 | hicolor info photo.png.hic 111 | } -result {5 640 427} 112 | 113 | tcltest::test encode-2.3 {encode flags} -body { 114 | hicolor encode -6 photo.png photo.png.hic 115 | hicolor info photo.png.hic 116 | } -result {6 640 427} 117 | 118 | tcltest::test encode-2.4 {encode flags} -body { 119 | hicolor encode --16-bit photo.png photo.png.hic 120 | hicolor info photo.png.hic 121 | } -result {6 640 427} 122 | 123 | tcltest::test encode-2.5 {encode flags} -body { 124 | hicolor encode --16-bit photo.png 125 | hicolor info photo.png.hic 126 | } -result {6 640 427} 127 | 128 | tcltest::test encode-2.6 {encode flags} -body { 129 | hicolor encode -5 -6 -5 photo.png 130 | hicolor info photo.png.hic 131 | } -result {5 640 427} 132 | 133 | tcltest::test encode-2.7 {encode flags} -body { 134 | hicolor encode -n -n -n -n -n photo.png 135 | } -result {} 136 | 137 | tcltest::test encode-2.8 {encode flags} -body { 138 | hicolor encode -6 -n -5 photo.png 139 | hicolor info photo.png.hic 140 | } -result {5 640 427} 141 | 142 | tcltest::test encode-2.9 {} -body { 143 | hicolor encode -b -a -n -a -n -a photo.png 144 | } -result {} 145 | 146 | 147 | tcltest::test encode-3.1 {bad input} -body { 148 | hicolor encode truncated.png 149 | } -returnCodes error -result {error: can't load PNG file "truncated.png":\ 150 | Read Error} 151 | 152 | tcltest::test encode-3.2 {bad input} -body { 153 | hicolor encode wrong-size.png 154 | } -returnCodes error -match glob -result {error: can't load PNG file*} 155 | 156 | 157 | hicolor encode --15-bit photo.png photo.hi5 158 | hicolor encode --15-bit --a-dither photo.png photo-a-dither.hi5 159 | hicolor encode --16-bit photo.png photo.hi6 160 | hicolor encode --16-bit --a-dither photo.png photo-a-dither.hi6 161 | 162 | 163 | tcltest::test decode-1.1 {15-bit} -body { 164 | hicolor decode photo.hi5 165 | file exists photo.hi5.png 166 | } -result 1 167 | 168 | tcltest::test decode-1.2 {16-bit} -body { 169 | hicolor decode photo.hi6 170 | file exists photo.hi6.png 171 | } -result 1 172 | 173 | tcltest::test decode-1.3 {bad input} -body { 174 | hicolor decode [file tail [info script]] 175 | } -returnCodes error -match glob -result {*bad magic*} 176 | 177 | tcltest::test decode-1.4 {bad input} -body { 178 | hicolor decode -5 photo.hi5 179 | } -returnCodes error -match glob -result *error:* 180 | 181 | 182 | tcltest::test quantize-1.1 {} -body { 183 | hicolor quantize photo.png photo.16-bit.png 184 | } -result {} 185 | 186 | tcltest::test quantize-1.2 {} -body { 187 | hicolor quantize -5 photo.png photo.15-bit.png 188 | } -result {} 189 | 190 | 191 | tcltest::test quantize-2.1 {bad input} -body { 192 | hicolor encode [file tail [info script]] 193 | } -returnCodes error -match glob -result {*Not a PNG file*} 194 | 195 | tcltest::test quantize-2.2 {bad input} -body { 196 | hicolor quantize truncated.png 197 | } -returnCodes error -result {error: can't load PNG file "truncated.png":\ 198 | Read Error} 199 | 200 | tcltest::test quantize-2.3 {bad input} -body { 201 | hicolor quantize wrong-size.png 202 | } -returnCodes error -match glob -result {error: can't load PNG file*} 203 | 204 | 205 | tcltest::test unknown-command-1.1 {} -body { 206 | hicolor -5 src.png 207 | } -returnCodes error -match glob -result {usage:*error: unknown command "-5"} 208 | 209 | tcltest::test unknown-command-1.2 {} -body { 210 | hicolor encoder 211 | } -returnCodes error -match glob -result {usage:*error: unknown command "encoder"} 212 | 213 | 214 | tcltest::test unknown-option-1.1 {} -body { 215 | hicolor encode --wrong fo bar 216 | } -returnCodes error -match glob -result {usage:*error: unknown option "--wrong"} 217 | 218 | 219 | tcltest::test no-arguments-1.1 {} -body { 220 | hicolor 221 | } -returnCodes error -match glob -result *options:* 222 | 223 | 224 | tcltest::test dash-dash-1.1 {} -body { 225 | hicolor encode -- --no-such-file 226 | } -returnCodes error -match glob -result {error: source image "--no-such-file"\ 227 | doesn't exist} 228 | 229 | 230 | tcltest::test data-integrity-1.1 {roundtrip} -constraints gm -body { 231 | hicolor decode photo.hi5 temp.png 232 | exec gm compare -metric rmse photo.png temp.png 233 | } -match regexp -result {Total: 0.0[12]} 234 | 235 | tcltest::test data-integrity-1.2 {roundtrip} -constraints gm -body { 236 | hicolor decode photo.hi6 temp.png 237 | exec gm compare -metric rmse photo.png temp.png 238 | } -match regexp -result {Total: 0.0[12]} 239 | 240 | tcltest::test data-integrity-2.1 {roundtrip with "a dither"} -constraints gm -body { 241 | hicolor decode photo-a-dither.hi5 temp.png 242 | exec gm compare -metric rmse photo.png temp.png 243 | } -match regexp -result {Total: 0.0[12]} 244 | 245 | tcltest::test data-integrity-2.2 {roundtrip with "a dither"} -constraints gm -body { 246 | hicolor decode photo-a-dither.hi6 temp.png 247 | exec gm compare -metric rmse photo.png temp.png 248 | } -match regexp -result {Total: 0.0[12]} 249 | 250 | tcltest::test data-integrity-3.1 {alpha roundtrip} -constraints gm -body { 251 | hicolor quant alpha.png alpha-q.png 252 | exec gm compare -metric rmse alpha.png alpha-q.png 253 | } -match regexp -result {Total: 0.0+ } 254 | 255 | 256 | incr failed [expr {$tcltest::numTests(Failed) > 0}] 257 | tcltest::cleanupTests 258 | 259 | if {$failed > 0} { 260 | exit 1 261 | } 262 | -------------------------------------------------------------------------------- /tests/photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/hicolor/1c437f80229a4d7a49e2804b3d79c1896e8c46ad/tests/photo.png -------------------------------------------------------------------------------- /tests/truncated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/hicolor/1c437f80229a4d7a49e2804b3d79c1896e8c46ad/tests/truncated.png -------------------------------------------------------------------------------- /tests/wrong-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/hicolor/1c437f80229a4d7a49e2804b3d79c1896e8c46ad/tests/wrong-size.png --------------------------------------------------------------------------------