joeylib2/tools/iconBake/iconBake.c

2004 lines
73 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// iconBake.c - a program's icon, from one picture, for each system that
// shows programs by an icon.
//
// The picture is a PNG, best square, with transparency; it is fitted into
// each icon's size keeping its shape, and centred. A picture of 64 pixels or
// less is taken for pixel art and enlarged by whole pixels; anything else is
// resampled smoothly (Lanczos, three lobes, on colour weighted by its
// opacity, in fixed point as Pillow does it). Where an icon has few colours
// it takes the nearest (the 8-bit Windows icon dithers to its own 256, cut
// from the picture's by median cut), and a pixel at least half transparent
// is transparent. The formats each system takes are told at their bake
// functions; usage() lists the command lines.
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "bePalette.h"
#include "toolBuf.h"
#include "toolFile.h"
#include "toolPng.h"
#define TOOL_NAME "iconBake"
#define USAGE_EXIT 2
#define RGB_BYTES 3u
#define RGBA_BYTES 4u
#define ALPHA 3u
#define SAMPLE_MAX 255u
#define PALETTE_SIZE 256u
#define BYTE_BITS 8u
#define NIBBLE_BITS 4u
// A picture this small or smaller is pixel art: enlarged by whole pixels.
#define PIXEL_ART_MAX 64u
#define ALPHA_OPAQUE_MIN 128u
// The biggest icon side asked for on the command line (a sanity bound).
#define ICON_SIDE_MAX 4096u
// Resampling: Lanczos with three lobes, its taps in fixed point with 22
// fraction bits (32 less a sample's 8 and 2 of headroom), as Pillow's
// resize does it.
#define LANCZOS_LOBES 3.0
#define PI 3.14159265358979323846
#define TAP_BITS 22
#define TAP_ONE (1 << TAP_BITS)
#define TAP_HALF (1 << (TAP_BITS - 1))
// Compositing over a background, as Pillow's alpha_composite: 7 fraction
// bits, a division by 255 done as two shifts.
#define BLEND_BITS 7u
#define BLEND_HALF 0x80u
#define DIV255_SHIFT 8u
// The nearest colour is looked up by the RGB cell a colour falls in, 6 bits
// a channel, measured from the cell's low corner (as Pillow's palette cache
// does it, so the same colours are chosen).
#define CELL_SHIFT 2u
#define CELL_SIDE 64u
#define CELL_LOW_MASK 0xFCu
#define CELL_COUNT (CELL_SIDE * CELL_SIDE * CELL_SIDE)
#define CELL_EMPTY (-1)
// Floyd-Steinberg error diffusion: 7/16 right, 3/16 below left, 5/16 below,
// 1/16 below right.
#define FS_DIVISOR 16
// Median cut: the 8-bit Windows icon's 256 colours are black (behind the
// AND mask) and up to 255 of the picture's own.
#define MEDIAN_COLOURS 255u
#define RGB_RED_SHIFT 16u
#define RGB_GREEN_SHIFT 8u
#define KEY_SHIFT 24u
// Windows: the sizes Win32 icons come in, and the 16 colours of a Windows
// 3.x icon (the VGA's, in Windows' order).
#define ICO_SIZE_COUNT 3u
#define ICO16_SIZE 32u
#define ICO16_COLOURS 16u
#define ICO_TYPE 1u
#define DIB_HEADER_BYTES 40u
#define ICONDIR_BYTES 6u
#define ICONDIRENTRY_BYTES 16u
#define ICO_TRUE_COLOUR_BITS 32u
#define ICO_256_BITS 8u
#define ICO_16_BITS 4u
#define DIB_ROW_ALIGN 4u
#define ENTRY_SIZE_WRAP 256u
// DVX: its icons' size and the grey they sit on (its own apps'); the
// 24-bit BMP DVX reads, at 96 dots an inch.
#define DVX_SIZE 32u
#define BMP_FILE_HEADER_BYTES 14u
#define BMP_BITS 24u
#define BMP_PIXELS_PER_METRE 3780u
// The Amiga: a Workbench tool icon (struct DiskObject, workbench.h) of
// AMIGA_W x AMIGA_H Workbench pixels, which are twice as tall as wide, in
// the four colours Workbench 2.0 on has (grey, black, white, blue).
#define AMIGA_W 48u
#define AMIGA_H 24u
#define AMIGA_DEPTH 2u
#define AMIGA_COLOURS 4u
#define AMIGA_WORD_PIXELS 16u
#define AMIGA_WORD_LEFT_BIT 0x8000u
#define WB_DISKMAGIC 0xE310u
#define WB_DISKVERSION 1u
#define WB_DISKREVISION 1u
#define WBTOOL 3u
#define NO_ICON_POSITION 0x80000000u
#define GFLG_GADGIMAGE 0x0004u
#define GACT_RELVERIFY_IMMEDIATE 0x0003u
#define GTYP_BOOLGADGET 0x0001u
// Any non-zero pointer: on disk it says the structure follows.
#define PRESENT 1u
// BeOS and Haiku: 32x32 and 16x16 icons in the system palette (bePalette.h;
// index 255 is transparent, B_TRANSPARENT_MAGIC_CMAP8).
#define BE_LARGE 32u
#define BE_SMALL 16u
#define BE_TRANSPARENT 255u
// Haiku's vector icon (HVIF, src/libs/icon/flat_icon in Haiku): the 16x16
// and 32x32 icons' pixels as outlines on its 64-unit grid, a shape a colour,
// hinted so every edge falls on a pixel. The 16x16 shapes show below 32
// pixels and the 32x32 ones from there up (scale 0.5 is 32 pixels; a level
// of detail byte is 63.75 to 1), so each size draws exactly its own pixels.
// A section holds at most 255 of anything; an icon needing more gets no
// vector icon.
#define HVIF_MAGIC "ncif"
#define HVIF_MAGIC_BYTES 4u
#define HVIF_GRID 64u
#define HVIF_STYLE_SOLID_COLOR 1u
#define HVIF_PATH_CLOSED 0x02u
#define HVIF_PATH_NO_CURVES 0x08u
#define HVIF_SHAPE_PATH_SOURCE 10u
#define HVIF_SHAPE_HINTING 0x04u
#define HVIF_SHAPE_LOD_SCALE 0x08u
#define HVIF_LOD_SMALL_MIN 0u
#define HVIF_LOD_SMALL_MAX 31u
#define HVIF_LOD_LARGE_MIN 31u
#define HVIF_LOD_LARGE_MAX 255u
#define HVIF_COORD_BIAS 32u
#define HVIF_MAX_ITEMS 255u
#define HVIF_OPAQUE 255u
#define HVIF_EDGES_PER_POINT 2u
#define HVIF_SIDES_PER_PIXEL 4u
// macOS: an app's icon file (.icns) is "icns" and the file's length, then
// an element for each size: its type, its length (these 8 bytes included)
// and the picture as a PNG. The @2x types are for Retina screens, twice the
// pixels of the size they stand for. Sizes past the first at least as big
// as the picture add nothing macOS's own enlarging would not, so are left
// out, unless it is pixel art, which fit() enlarges sharply.
#define ICNS_MAGIC "icns"
#define ICNS_TYPE_BYTES 4u
#define ICNS_HEADER_BYTES 8u
#define ICNS_ELEMENT_COUNT 9u
#define ICNS_LARGEST 512u
// The Apple IIgs Finder: an icon file (file type $CA; Apple's File Type
// Note $CA, "Finder Icons File") in a volume's Icons folder, holding one
// icon data record that gives a GS/OS application (file type $B3) of the
// program's name its icons, normal and small. Each is a QuickDraw II icon
// (Toolbox Reference volume 2, QuickDraw II Auxiliary): iconType, iconSize
// (the image's bytes), iconHeight, iconWidth, the image at 4 bits a pixel
// (rows of whole bytes, the even pixel in the high nibble), then a mask of
// the same shape, non-zero where the icon is opaque. They are colour icons
// (iconType bit 15), whose colours the Finder leaves alone. The Finder runs
// in 640 mode, where QuickDraw draws each 4-bit icon pixel as two screen
// pixels, its high two bits then its low two, and in the Finder's palette
// the first of the pair is black, red, green, or white and the second
// black, blue, yellow, or white (kIigsFirst, kIigsSecond: measured in MAME,
// an icon at an even screen x), so an icon pixel is one of 16 colours a
// pair makes, seen as their mean. An icon pixel is some 1.2 times as tall
// as wide; the big icon is the size of the Finder's own (its folder is 24
// pixels wide).
#define IIGS_BIG_W 24u
#define IIGS_BIG_H 20u
#define IIGS_SMALL_W 12u
#define IIGS_SMALL_H 10u
#define IIGS_PIXEL_ASPECT 1.2
#define IIGS_VALUES 16u
#define IIGS_HALVES 4u
#define IIGS_HALF_BITS 2u
#define IIGS_HALF_MASK 3u
#define IIGS_BLACK 0x0u
#define IIGS_WHITE 0xFu
#define IIGS_OPAQUE 0xFu
#define IIGS_ICON_TYPE_COLOUR 0x8000u
#define IIGS_ICON_FILE_ID 0x0001u
#define IIGS_APPLICATION_TYPE 0x00B3u
#define IIGS_FILE_NAME_BYTES 16u
#define IIGS_BOSS_BYTES 64u
#define IIGS_NAME_BYTES 16u
#define IIGS_LENGTH_WORD_BYTES 2u
#define ASCII_MAX 0x7Fu
// A pixel with less colour than this (CIELAB chroma) may be shown in a
// grey; a more colourful one keeps a colour, or black or white: brown fur
// nearest a grey by lightness reads better in the nearest colour.
#define IIGS_GREY_CHROMA 12.0
// CIE94 (graphic arts): how much a difference in chroma and in hue counts,
// against the colour's own chroma.
#define CIE94_CHROMA 0.045
#define CIE94_HUE 0.015
// sRGB's transfer curve, and CIELAB's white point (D65) and curve.
#define SRGB_LINEAR_MAX 0.04045
#define SRGB_LINEAR_SLOPE 12.92
#define SRGB_OFFSET 0.055
#define SRGB_GAMMA 2.4
#define LAB_EPSILON 0.008856
#define LAB_KAPPA 7.787
#define LAB_L_SCALE 116.0
#define LAB_L_OFFSET 16.0
#define LAB_A_SCALE 500.0
#define LAB_B_SCALE 200.0
// The SDL3 backend's window icon as C: this many bytes a line.
#define C_BYTES_PER_LINE 16u
#define COUNT_OF(a) (sizeof(a) / sizeof((a)[0]))
// The taps one resampling pass uses: for each output sample its first input
// sample, how many it takes, and their weights (size a sample).
typedef struct TapsT {
uint32_t size;
uint32_t *first;
uint32_t *count;
int32_t *weights;
} TapsT;
// A palette of 256 colours and, by RGB cell, the nearest entry found yet.
typedef struct PaletteT {
uint8_t rgb[PALETTE_SIZE][RGB_BYTES];
int16_t *cache;
} PaletteT;
// One of a picture's colours (0xRRGGBB), how many pixels have it, and what
// a median cut box sorts it by.
typedef struct ShadeT {
uint32_t key;
uint32_t rgb;
uint32_t pixels;
} ShadeT;
// A median cut box: a run of shades, their pixels, and its widest channel.
typedef struct BoxT {
uint32_t first;
uint32_t count;
uint32_t pixels;
uint32_t channel;
uint32_t range;
} BoxT;
// A colour in CIELAB.
typedef struct LabT {
double l;
double a;
double b;
} LabT;
// One palette picture for the vector icon: its side, its pixels (palette
// indices, BE_TRANSPARENT clear), and the scales it shows at.
typedef struct HvifSizeT {
uint32_t size;
const uint8_t *pixels;
uint32_t lodMin;
uint32_t lodMax;
} HvifSizeT;
typedef void (*BakeFnT)(const ToolImageT *picture, char **args);
typedef struct BakeT {
const char *name;
BakeFnT bake;
uint32_t args;
} BakeT;
typedef struct IcnsElementT {
const char *type;
uint32_t size;
} IcnsElementT;
static void alphaComposite(ToolImageT *dst, const ToolImageT *src);
static void andMask(const ToolImageT *image, ToolBufT *out);
static void bakeBe(const ToolImageT *picture, char **args);
static void bakeBmp(const ToolImageT *picture, char **args);
static void bakeC(const ToolImageT *picture, char **args);
static void bakeIcns(const ToolImageT *picture, char **args);
static void bakeIco(const ToolImageT *picture, char **args);
static void bakeIco16(const ToolImageT *picture, char **args);
static void bakeIigs(const ToolImageT *picture, char **args);
static void bakeInfo(const ToolImageT *picture, char **args);
static void bakePng(const ToolImageT *picture, char **args);
static void boxMeasure(BoxT *box, const ShadeT *shades);
static double chroma(LabT colour);
static double cie94(LabT reference, LabT sample);
static uint8_t clampSample(int32_t value);
static void ditherFloydSteinberg(const ToolImageT *flat, PaletteT *palette, uint8_t *indices);
static void enlargeWhole(const ToolImageT *src, uint32_t scale, ToolImageT *dst);
static void fit(const ToolImageT *picture, uint32_t w, uint32_t h, ToolImageT *out);
static void flatten(const ToolImageT *image, const uint8_t background[RGB_BYTES], ToolImageT *flat);
static bool hvif(const HvifSizeT *sizes, uint32_t sizeCount, ToolBufT *out);
static bool hvifContours(const uint8_t *pixels, uint32_t size, uint8_t colour, uint32_t units, ToolBufT *paths, uint32_t *pathCount);
static void icoFile(const uint32_t *sides, const uint32_t *colourCounts, const uint32_t *bitCounts, const ToolBufT *images, uint32_t count, const char *path);
static void icoImage(const ToolImageT *image, uint32_t bitCount, const uint8_t (*colours)[RGB_BYTES], uint32_t colourCount, ToolBufT *out);
static void iigsColours(LabT colours[IIGS_VALUES], uint32_t keeps[IIGS_VALUES], uint32_t *keepCount);
static void iigsIcon(const ToolImageT *picture, uint32_t w, uint32_t h, ToolBufT *out);
static void imageCopy(const ToolImageT *src, ToolImageT *dst);
static void imageNew(ToolImageT *img, uint32_t w, uint32_t h);
static LabT lab(const double rgb[RGB_BYTES]);
static double lanczos(double x);
static double linear(double value);
static void medianCut(const ToolImageT *image, uint8_t palette[PALETTE_SIZE][RGB_BYTES]);
static uint8_t nearestColour(PaletteT *palette, uint32_t r, uint32_t g, uint32_t b);
static bool opaque(const ToolImageT *image, size_t pixel);
static int packedCompare(const void *a, const void *b);
static void paletteFree(PaletteT *palette);
static void paletteInit(PaletteT *palette, const uint8_t (*colours)[RGB_BYTES], uint32_t count);
static uint32_t parseSide(const char *text, size_t len, const char *what);
static void pascal(ToolBufT *out, const char *text, uint32_t size);
static void premultiply(const ToolImageT *src, ToolImageT *dst);
static void resamplePass(const uint8_t *in, uint8_t *out, const TapsT *taps, uint32_t outCount, uint32_t lines, size_t inStep, size_t inLineStep, size_t outStep, size_t outLineStep);
static void resizeLanczos(const ToolImageT *src, uint32_t w, uint32_t h, ToolImageT *dst);
static int shadeCompare(const void *a, const void *b);
static double sinc(double x);
static void tapsBuild(TapsT *taps, uint32_t inSize, uint32_t outSize);
static void tapsFree(TapsT *taps);
static void toColours(const ToolImageT *image, const uint8_t (*colours)[RGB_BYTES], uint32_t count, bool dither, const uint8_t background[RGB_BYTES], uint8_t *indices);
static void unpremultiply(const ToolImageT *src, ToolImageT *dst);
static void usage(void);
static void writeOut(const char *path, const uint8_t *data, size_t len);
static const BakeT kBakes[] = {
{ "ico", bakeIco, 0u },
{ "ico16", bakeIco16, 0u },
{ "bmp", bakeBmp, 0u },
{ "png", bakePng, 1u },
{ "info", bakeInfo, 1u },
{ "be", bakeBe, 2u },
{ "c", bakeC, 1u },
{ "icns", bakeIcns, 0u },
{ "iigs", bakeIigs, 1u }
};
static const uint32_t kIcoSizes[ICO_SIZE_COUNT] = { 16u, 32u, 48u };
static const uint8_t kVga16[ICO16_COLOURS][RGB_BYTES] = {
{ 0, 0, 0 }, { 128, 0, 0 }, { 0, 128, 0 }, { 128, 128, 0 },
{ 0, 0, 128 }, { 128, 0, 128 }, { 0, 128, 128 }, { 192, 192, 192 },
{ 128, 128, 128 }, { 255, 0, 0 }, { 0, 255, 0 }, { 255, 255, 0 },
{ 0, 0, 255 }, { 255, 0, 255 }, { 0, 255, 255 }, { 255, 255, 255 }
};
static const uint8_t kBlack[RGB_BYTES] = { 0, 0, 0 };
static const uint8_t kWhite[RGB_BYTES] = { 255, 255, 255 };
static const uint8_t kDvxGrey[RGB_BYTES] = { 192, 192, 192 };
static const uint8_t kAmigaColours[AMIGA_COLOURS][RGB_BYTES] = {
{ 170, 170, 170 }, { 0, 0, 0 }, { 255, 255, 255 }, { 102, 136, 187 }
};
static const IcnsElementT kIcnsElements[ICNS_ELEMENT_COUNT] = {
{ "icp4", 16u }, { "icp5", 32u }, { "ic11", 32u }, { "ic12", 64u }, { "ic07", 128u },
{ "ic13", 256u }, { "ic08", 256u }, { "ic14", 512u }, { "ic09", 512u }
};
static const uint8_t kIigsFirst[IIGS_HALVES][RGB_BYTES] = { { 0, 0, 0 }, { 255, 0, 0 }, { 0, 255, 0 }, { 255, 255, 255 } };
static const uint8_t kIigsSecond[IIGS_HALVES][RGB_BYTES] = { { 0, 0, 0 }, { 0, 0, 255 }, { 255, 255, 0 }, { 255, 255, 255 } };
// sRGB to CIE XYZ (D65), and CIELAB's white point.
static const double kXyzFromRgb[RGB_BYTES][RGB_BYTES] = { { 0.4124, 0.3576, 0.1805 }, { 0.2126, 0.7152, 0.0722 }, { 0.0193, 0.1192, 0.9505 } };
static const double kLabWhite[RGB_BYTES] = { 0.95047, 1.0, 1.08883 };
static const char *const kUsage[] = {
"usage:",
" iconBake ico PICTURE OUT.ico Windows 95 on (Win32): 16, 32 and",
" 48 pixels, each in 256 colours and",
" in 32-bit",
" iconBake ico16 PICTURE OUT.ico Windows 3.x (Win16): 32x32 in the 16",
" VGA colours",
" iconBake bmp PICTURE OUT.bmp DVX: 32x32, 24-bit, on DVX's grey",
" iconBake png PICTURE OUT.png WxH a picture WxH (the PSP's 144x80",
" ICON0, a web page's icon)",
" iconBake info PICTURE OUT.info STACK the Amiga's Workbench: a tool's icon",
" in its four colours, STACK bytes of",
" stack when started from it",
" iconBake be PICTURE OUT32 OUT16 OUT.hvif",
" BeOS and Haiku: 32x32 and 16x16 in",
" the BeOS palette, raw, for xres",
" (ICON, MICN), and the 32x32 one as",
" Haiku's vector icon (VICN)",
" iconBake c PICTURE OUT.c SIZE the SDL3 backend's window icon: SIZE",
" x SIZE RGBA as C",
" iconBake icns PICTURE OUT.icns macOS: an app's icon, 16 pixels up",
" to 512 (or the picture's size), and",
" twice that for Retina screens",
" iconBake iigs PICTURE OUT NAME the Apple IIgs Finder: an icon file",
" ($CA) giving the program NAME its",
" icons, in the Finder's 16 colours",
"",
"PICTURE is a PNG, best square, with transparency. One of 64 pixels or less",
"is pixel art, enlarged by whole pixels; anything else is resampled smoothly."
};
// src composited over dst (Porter-Duff over), in place, as Pillow's
// alpha_composite rounds it.
static void alphaComposite(ToolImageT *dst, const ToolImageT *src) {
size_t i;
size_t pixels;
uint8_t *d;
const uint8_t *s;
uint32_t blend;
uint32_t outA255;
uint32_t coef1;
uint32_t coef2;
uint32_t c;
uint32_t mix;
pixels = (size_t)dst->width * dst->height;
for (i = 0; i < pixels; i++) {
d = dst->pixels + i * RGBA_BYTES;
s = src->pixels + i * RGBA_BYTES;
if (s[ALPHA] == 0u) {
continue;
}
blend = (uint32_t)d[ALPHA] * (SAMPLE_MAX - s[ALPHA]);
outA255 = (uint32_t)s[ALPHA] * SAMPLE_MAX + blend;
coef1 = (uint32_t)s[ALPHA] * SAMPLE_MAX * SAMPLE_MAX * (1u << BLEND_BITS) / outA255;
coef2 = SAMPLE_MAX * (1u << BLEND_BITS) - coef1;
for (c = 0; c < RGB_BYTES; c++) {
mix = s[c] * coef1 + d[c] * coef2 + (BLEND_HALF << BLEND_BITS);
d[c] = (uint8_t)((((mix >> DIV255_SHIFT) + mix) >> DIV255_SHIFT) >> BLEND_BITS);
}
mix = outA255 + BLEND_HALF;
d[ALPHA] = (uint8_t)(((mix >> DIV255_SHIFT) + mix) >> DIV255_SHIFT);
}
}
// A Windows icon's AND mask: 1 where transparent, bottom-up rows of 1 bit a
// pixel padded to DWORDs.
static void andMask(const ToolImageT *image, ToolBufT *out) {
uint32_t x;
uint32_t y;
uint32_t bits;
size_t rowStart;
for (y = image->height; y-- > 0u;) {
rowStart = out->len;
bits = 0u;
for (x = 0; x < image->width; x++) {
bits = (bits << 1) | (opaque(image, (size_t)y * image->width + x) ? 0u : 1u);
if (x % BYTE_BITS == BYTE_BITS - 1u) {
toolBufByte(out, bits);
bits = 0u;
}
}
if (image->width % BYTE_BITS != 0u) {
toolBufByte(out, bits << (BYTE_BITS - image->width % BYTE_BITS));
}
toolBufZeros(out, (DIB_ROW_ALIGN - (out->len - rowStart) % DIB_ROW_ALIGN) % DIB_ROW_ALIGN);
}
}
// BeOS and Haiku: the 32x32 and 16x16 icons in the BeOS palette, raw, and
// both as Haiku's vector icon (empty when HVIF cannot hold it).
static void bakeBe(const ToolImageT *picture, char **args) {
static const uint32_t sides[] = { BE_LARGE, BE_SMALL };
static const uint32_t lodMin[] = { HVIF_LOD_LARGE_MIN, HVIF_LOD_SMALL_MIN };
static const uint32_t lodMax[] = { HVIF_LOD_LARGE_MAX, HVIF_LOD_SMALL_MAX };
ToolImageT image;
uint8_t *pixels[COUNT_OF(sides)];
HvifSizeT sizes[COUNT_OF(sides)];
ToolBufT vector;
size_t count;
size_t i;
uint32_t s;
for (s = 0; s < COUNT_OF(sides); s++) {
fit(picture, sides[s], sides[s], &image);
count = (size_t)sides[s] * sides[s];
pixels[s] = (uint8_t *)malloc(count);
if (pixels[s] == NULL) {
toolFail("out of memory");
}
toColours(&image, kBePalette, BE_TRANSPARENT, false, kBlack, pixels[s]);
for (i = 0; i < count; i++) {
if (!opaque(&image, i)) {
pixels[s][i] = BE_TRANSPARENT;
}
}
writeOut(args[s], pixels[s], count);
toolImageFree(&image);
sizes[s].size = sides[s];
sizes[s].pixels = pixels[s];
sizes[s].lodMin = lodMin[s];
sizes[s].lodMax = lodMax[s];
}
memset(&vector, 0, sizeof(vector));
if (!hvif(sizes, COUNT_OF(sides), &vector)) {
fprintf(stderr, "%s: the icon needs more than %u paths or shapes; no vector icon\n", TOOL_NAME, HVIF_MAX_ITEMS);
}
writeOut(args[COUNT_OF(sides)], vector.data, vector.len);
toolBufFree(&vector);
for (s = 0; s < COUNT_OF(sides); s++) {
free(pixels[s]);
}
}
// DVX: 32x32, 24-bit, on DVX's grey, as a BMP.
static void bakeBmp(const ToolImageT *picture, char **args) {
ToolImageT image;
ToolImageT flat;
ToolBufT out;
uint32_t stride;
uint32_t x;
uint32_t y;
const uint8_t *p;
fit(picture, DVX_SIZE, DVX_SIZE, &image);
flatten(&image, kDvxGrey, &flat);
stride = (DVX_SIZE * RGB_BYTES + DIB_ROW_ALIGN - 1u) / DIB_ROW_ALIGN * DIB_ROW_ALIGN;
memset(&out, 0, sizeof(out));
toolBufAdd(&out, "BM", 2u);
toolBufLe32(&out, BMP_FILE_HEADER_BYTES + DIB_HEADER_BYTES + stride * DVX_SIZE);
toolBufLe32(&out, 0u);
toolBufLe32(&out, BMP_FILE_HEADER_BYTES + DIB_HEADER_BYTES);
toolBufLe32(&out, DIB_HEADER_BYTES);
toolBufLe32(&out, DVX_SIZE);
toolBufLe32(&out, DVX_SIZE);
toolBufLe16(&out, 1u);
toolBufLe16(&out, BMP_BITS);
toolBufLe32(&out, 0u);
toolBufLe32(&out, stride * DVX_SIZE);
toolBufLe32(&out, BMP_PIXELS_PER_METRE);
toolBufLe32(&out, BMP_PIXELS_PER_METRE);
toolBufLe32(&out, 0u);
toolBufLe32(&out, 0u);
for (y = DVX_SIZE; y-- > 0u;) {
for (x = 0; x < DVX_SIZE; x++) {
p = flat.pixels + ((size_t)y * DVX_SIZE + x) * RGBA_BYTES;
toolBufByte(&out, p[2]);
toolBufByte(&out, p[1]);
toolBufByte(&out, p[0]);
}
toolBufZeros(&out, stride - DVX_SIZE * RGB_BYTES);
}
writeOut(args[0], out.data, out.len);
toolBufFree(&out);
toolImageFree(&flat);
toolImageFree(&image);
}
// The SDL3 backend's window icon: SIZE x SIZE RGBA pixels as C.
static void bakeC(const ToolImageT *picture, char **args) {
ToolImageT image;
uint32_t size;
size_t bytes;
size_t i;
FILE *f;
bool ok;
size = parseSide(args[1], strlen(args[1]), "SIZE");
fit(picture, size, size, &image);
bytes = (size_t)size * size * RGBA_BYTES;
f = fopen(args[0], "w");
if (f == NULL) {
toolFail("cannot write %s", args[0]);
}
fprintf(f, "// Generated by tools/iconBake: the program's window icon, %u x %u\n", size, size);
fprintf(f, "// RGBA pixels, row after row (src/hosted/sdl3/sdl3Backend.c).\n\n");
fprintf(f, "#include <stdint.h>\n\n");
fprintf(f, "const uint16_t jlgHostedIconSize = %uu;\n", size);
fprintf(f, "const uint8_t jlgHostedIconRgba[] = {\n");
for (i = 0; i < bytes; i++) {
fprintf(f, "%s0x%02X%s", i % C_BYTES_PER_LINE == 0u ? " " : " ", image.pixels[i], (i % C_BYTES_PER_LINE == C_BYTES_PER_LINE - 1u || i == bytes - 1u) ? ",\n" : ",");
}
fprintf(f, "};\n");
ok = !ferror(f);
ok = fclose(f) == 0 && ok;
if (!ok) {
toolFail("cannot write %s", args[0]);
}
toolImageFree(&image);
}
// macOS: each element's size as a PNG, up to the first size at least as big
// as the picture (all of them for pixel art). The elements go up in size,
// so an element the size of the one before shares its PNG.
static void bakeIcns(const ToolImageT *picture, char **args) {
uint8_t *png;
size_t pngLen;
uint32_t pngSize;
uint32_t largest;
uint32_t longest;
uint32_t e;
ToolImageT image;
ToolBufT elements;
ToolBufT out;
longest = picture->width > picture->height ? picture->width : picture->height;
largest = ICNS_LARGEST;
if (longest > PIXEL_ART_MAX) {
for (e = 0; e < ICNS_ELEMENT_COUNT; e++) {
if (kIcnsElements[e].size >= longest) {
largest = kIcnsElements[e].size;
break;
}
}
}
memset(&elements, 0, sizeof(elements));
png = NULL;
pngLen = 0u;
pngSize = 0u;
for (e = 0; e < ICNS_ELEMENT_COUNT && kIcnsElements[e].size <= largest; e++) {
if (kIcnsElements[e].size != pngSize) {
free(png);
fit(picture, kIcnsElements[e].size, kIcnsElements[e].size, &image);
if (!toolPngEncode(&image, &png, &pngLen)) {
toolFail("out of memory");
}
toolImageFree(&image);
pngSize = kIcnsElements[e].size;
}
toolBufAdd(&elements, kIcnsElements[e].type, ICNS_TYPE_BYTES);
toolBufBe32(&elements, (uint32_t)(ICNS_HEADER_BYTES + pngLen));
toolBufAdd(&elements, png, pngLen);
}
free(png);
memset(&out, 0, sizeof(out));
toolBufAdd(&out, ICNS_MAGIC, ICNS_TYPE_BYTES);
toolBufBe32(&out, (uint32_t)(ICNS_HEADER_BYTES + elements.len));
toolBufAdd(&out, elements.data, elements.len);
writeOut(args[0], out.data, out.len);
toolBufFree(&out);
toolBufFree(&elements);
}
// Windows 95 on: 16, 32 and 48 pixels, each in 256 colours (its own, by
// median cut) and in 32-bit.
static void bakeIco(const ToolImageT *picture, char **args) {
ToolImageT image;
ToolBufT images[ICO_SIZE_COUNT * 2u];
uint32_t sides[ICO_SIZE_COUNT * 2u];
uint32_t colourCounts[ICO_SIZE_COUNT * 2u];
uint32_t bitCounts[ICO_SIZE_COUNT * 2u];
uint8_t palette[PALETTE_SIZE][RGB_BYTES];
uint32_t s;
uint32_t i;
memset(images, 0, sizeof(images));
for (s = 0; s < ICO_SIZE_COUNT; s++) {
fit(picture, kIcoSizes[s], kIcoSizes[s], &image);
medianCut(&image, palette);
for (i = s * 2u; i < s * 2u + 2u; i++) {
sides[i] = kIcoSizes[s];
colourCounts[i] = 0u;
}
bitCounts[s * 2u] = ICO_256_BITS;
bitCounts[s * 2u + 1u] = ICO_TRUE_COLOUR_BITS;
icoImage(&image, ICO_256_BITS, (const uint8_t (*)[RGB_BYTES])palette, PALETTE_SIZE, &images[s * 2u]);
icoImage(&image, ICO_TRUE_COLOUR_BITS, NULL, 0u, &images[s * 2u + 1u]);
toolImageFree(&image);
}
icoFile(sides, colourCounts, bitCounts, images, ICO_SIZE_COUNT * 2u, args[0]);
for (i = 0; i < ICO_SIZE_COUNT * 2u; i++) {
toolBufFree(&images[i]);
}
}
// Windows 3.x: 32x32 in the 16 VGA colours.
static void bakeIco16(const ToolImageT *picture, char **args) {
ToolImageT image;
ToolBufT data;
const uint32_t side = ICO16_SIZE;
const uint32_t colourCount = ICO16_COLOURS;
const uint32_t bitCount = ICO_16_BITS;
fit(picture, ICO16_SIZE, ICO16_SIZE, &image);
memset(&data, 0, sizeof(data));
icoImage(&image, ICO_16_BITS, kVga16, ICO16_COLOURS, &data);
icoFile(&side, &colourCount, &bitCount, &data, 1u, args[0]);
toolBufFree(&data);
toolImageFree(&image);
}
// The Apple IIgs Finder: an icon file whose one record gives the
// application NAME its normal and small icons. The record's length counts
// its own length word; a zero length ends the list.
static void bakeIigs(const ToolImageT *picture, char **args) {
const char *name;
ToolBufT record;
ToolBufT out;
size_t i;
name = args[1];
for (i = 0; name[i] != '\0'; i++) {
if ((unsigned char)name[i] > ASCII_MAX) {
toolFail("NAME '%s' is not ASCII", name);
}
}
memset(&record, 0, sizeof(record));
pascal(&record, "", IIGS_BOSS_BYTES);
pascal(&record, name, IIGS_NAME_BYTES);
toolBufLe16(&record, IIGS_APPLICATION_TYPE);
toolBufLe16(&record, 0u);
iigsIcon(picture, IIGS_BIG_W, IIGS_BIG_H, &record);
iigsIcon(picture, IIGS_SMALL_W, IIGS_SMALL_H, &record);
memset(&out, 0, sizeof(out));
toolBufLe32(&out, 0u);
toolBufLe16(&out, IIGS_ICON_FILE_ID);
toolBufLe32(&out, 0u);
pascal(&out, name, IIGS_FILE_NAME_BYTES);
toolBufLe16(&out, (uint32_t)(IIGS_LENGTH_WORD_BYTES + record.len));
toolBufAdd(&out, record.data, record.len);
toolBufLe16(&out, 0u);
writeOut(args[0], out.data, out.len);
toolBufFree(&out);
toolBufFree(&record);
}
// The Amiga: a WBTOOL DiskObject: the header, its gadget, the image (struct
// Image and its planes, colour 0 where transparent) and nothing more.
// Workbench's grey is the background alone: the picture is drawn in the
// other three.
static void bakeInfo(const ToolImageT *picture, char **args) {
ToolImageT tall;
ToolImageT image;
ToolBufT out;
uint8_t indices[AMIGA_W * AMIGA_H];
uint32_t rowWords;
uint32_t plane;
uint32_t x;
uint32_t y;
uint32_t word;
uint32_t bits;
long stack;
size_t i;
if (!toolParseInt(args[1], strlen(args[1]), &stack) || stack < 0 || (unsigned long)stack > UINT32_MAX) {
toolFail("bad STACK '%s'", args[1]);
}
fit(picture, AMIGA_W, AMIGA_H * 2u, &tall);
resizeLanczos(&tall, AMIGA_W, AMIGA_H, &image);
toColours(&image, kAmigaColours + 1, AMIGA_COLOURS - 1u, false, kWhite, indices);
for (i = 0; i < (size_t)AMIGA_W * AMIGA_H; i++) {
indices[i] = opaque(&image, i) ? (uint8_t)(indices[i] + 1u) : 0u;
}
memset(&out, 0, sizeof(out));
toolBufBe16(&out, WB_DISKMAGIC);
toolBufBe16(&out, WB_DISKVERSION);
// struct Gadget do_Gadget: NextGadget, LeftEdge, TopEdge, Width,
// Height, Flags, Activation, GadgetType, GadgetRender, SelectRender,
// GadgetText, MutualExclude, SpecialInfo, GadgetID, UserData (the
// revision).
toolBufBe32(&out, 0u);
toolBufBe16(&out, 0u);
toolBufBe16(&out, 0u);
toolBufBe16(&out, AMIGA_W);
toolBufBe16(&out, AMIGA_H + 1u);
toolBufBe16(&out, GFLG_GADGIMAGE);
toolBufBe16(&out, GACT_RELVERIFY_IMMEDIATE);
toolBufBe16(&out, GTYP_BOOLGADGET);
toolBufBe32(&out, PRESENT);
toolBufBe32(&out, 0u);
toolBufBe32(&out, 0u);
toolBufBe32(&out, 0u);
toolBufBe32(&out, 0u);
toolBufBe16(&out, 0u);
toolBufBe32(&out, WB_DISKREVISION);
// do_Type, a pad byte, do_DefaultTool, do_ToolTypes, do_CurrentX,
// do_CurrentY, do_DrawerData, do_ToolWindow, do_StackSize.
toolBufByte(&out, WBTOOL);
toolBufByte(&out, 0u);
toolBufBe32(&out, 0u);
toolBufBe32(&out, 0u);
toolBufBe32(&out, NO_ICON_POSITION);
toolBufBe32(&out, NO_ICON_POSITION);
toolBufBe32(&out, 0u);
toolBufBe32(&out, 0u);
toolBufBe32(&out, (uint32_t)stack);
// struct Image: LeftEdge, TopEdge, Width, Height, Depth, ImageData,
// PlanePick, PlaneOnOff, NextImage.
toolBufBe16(&out, 0u);
toolBufBe16(&out, 0u);
toolBufBe16(&out, AMIGA_W);
toolBufBe16(&out, AMIGA_H);
toolBufBe16(&out, AMIGA_DEPTH);
toolBufBe32(&out, PRESENT);
toolBufByte(&out, (1u << AMIGA_DEPTH) - 1u);
toolBufByte(&out, 0u);
toolBufBe32(&out, 0u);
rowWords = (AMIGA_W + AMIGA_WORD_PIXELS - 1u) / AMIGA_WORD_PIXELS;
for (plane = 0; plane < AMIGA_DEPTH; plane++) {
for (y = 0; y < AMIGA_H; y++) {
for (word = 0; word < rowWords; word++) {
bits = 0u;
for (x = word * AMIGA_WORD_PIXELS; x < (word + 1u) * AMIGA_WORD_PIXELS && x < AMIGA_W; x++) {
if ((indices[y * AMIGA_W + x] & (1u << plane)) != 0u) {
bits |= AMIGA_WORD_LEFT_BIT >> (x % AMIGA_WORD_PIXELS);
}
}
toolBufBe16(&out, bits);
}
}
}
writeOut(args[0], out.data, out.len);
toolBufFree(&out);
toolImageFree(&image);
toolImageFree(&tall);
}
// A picture WxH (the PSP's 144x80 ICON0, a web page's icon).
static void bakePng(const ToolImageT *picture, char **args) {
const char *spec;
const char *x;
uint32_t w;
uint32_t h;
ToolImageT image;
spec = args[1];
x = strpbrk(spec, "xX");
if (x == NULL || strpbrk(x + 1, "xX") != NULL) {
toolFail("bad size '%s', need 'WxH'", spec);
}
w = parseSide(spec, (size_t)(x - spec), "width");
h = parseSide(x + 1, strlen(x + 1), "height");
fit(picture, w, h, &image);
if (!toolPngSave(args[0], &image)) {
toolFail("cannot write %s", args[0]);
}
toolImageFree(&image);
}
// A median cut box's pixels and widest channel (the first of equals).
static void boxMeasure(BoxT *box, const ShadeT *shades) {
uint32_t lo[RGB_BYTES];
uint32_t hi[RGB_BYTES];
uint32_t i;
uint32_t c;
uint32_t v;
for (c = 0; c < RGB_BYTES; c++) {
lo[c] = SAMPLE_MAX;
hi[c] = 0u;
}
box->pixels = 0u;
for (i = box->first; i < box->first + box->count; i++) {
box->pixels += shades[i].pixels;
for (c = 0; c < RGB_BYTES; c++) {
v = (shades[i].rgb >> (RGB_RED_SHIFT - c * BYTE_BITS)) & SAMPLE_MAX;
if (v < lo[c]) {
lo[c] = v;
}
if (v > hi[c]) {
hi[c] = v;
}
}
}
box->channel = 0u;
box->range = hi[0] - lo[0];
for (c = 1; c < RGB_BYTES; c++) {
if (hi[c] - lo[c] > box->range) {
box->channel = c;
box->range = hi[c] - lo[c];
}
}
}
static double chroma(LabT colour) {
return hypot(colour.a, colour.b);
}
// CIE94's colour difference of sample from reference.
static double cie94(LabT reference, LabT sample) {
double c1;
double dc;
double dh2;
double dl;
double da;
double db;
double sc;
double sh;
c1 = chroma(reference);
dc = c1 - chroma(sample);
dl = reference.l - sample.l;
da = reference.a - sample.a;
db = reference.b - sample.b;
dh2 = da * da + db * db - dc * dc;
if (dh2 < 0.0) {
dh2 = 0.0;
}
sc = dc / (1.0 + CIE94_CHROMA * c1);
sh = 1.0 + CIE94_HUE * c1;
return sqrt(dl * dl + sc * sc + dh2 / (sh * sh));
}
// A fixed-point resampling sum (TAP_BITS of fraction) as a sample.
static uint8_t clampSample(int32_t value) {
if (value < 0) {
return 0u;
}
value >>= TAP_BITS;
return value > (int32_t)SAMPLE_MAX ? (uint8_t)SAMPLE_MAX : (uint8_t)value;
}
// flat's pixels as palette indices, dithered Floyd-Steinberg in integers:
// each pixel takes the error carried to it (sixteenths, truncated),
// clipped, and passes on its own. (Pillow's carries blue's error below the
// last pixel of a row in all three channels; this carries each its own.)
static void ditherFloydSteinberg(const ToolImageT *flat, PaletteT *palette, uint8_t *indices) {
int32_t *errors;
int32_t *e;
int32_t err[RGB_BYTES];
int32_t below[RGB_BYTES];
int32_t belowRight[RGB_BYTES];
int32_t value[RGB_BYTES];
int32_t d2;
int32_t self;
uint32_t x;
uint32_t y;
uint32_t c;
uint8_t index;
const uint8_t *in;
errors = (int32_t *)calloc(((size_t)flat->width + 1u) * RGB_BYTES, sizeof(int32_t));
if (errors == NULL) {
toolFail("out of memory");
}
for (y = 0; y < flat->height; y++) {
e = errors;
for (c = 0; c < RGB_BYTES; c++) {
err[c] = 0;
below[c] = 0;
belowRight[c] = 0;
}
for (x = 0; x < flat->width; x++) {
in = flat->pixels + ((size_t)y * flat->width + x) * RGBA_BYTES;
for (c = 0; c < RGB_BYTES; c++) {
value[c] = (int32_t)in[c] + (err[c] + e[RGB_BYTES + c]) / FS_DIVISOR;
if (value[c] < 0) {
value[c] = 0;
} else if (value[c] > (int32_t)SAMPLE_MAX) {
value[c] = (int32_t)SAMPLE_MAX;
}
}
index = nearestColour(palette, (uint32_t)value[0], (uint32_t)value[1], (uint32_t)value[2]);
indices[(size_t)y * flat->width + x] = index;
for (c = 0; c < RGB_BYTES; c++) {
self = value[c] - (int32_t)palette->rgb[index][c];
d2 = self + self;
e[c] = self + d2 + below[c];
below[c] = self + d2 + d2 + belowRight[c];
belowRight[c] = self;
err[c] = self + d2 + d2 + d2;
}
e += RGB_BYTES;
}
for (c = 0; c < RGB_BYTES; c++) {
e[c] = below[c];
}
}
free(errors);
}
// src enlarged scale whole pixels a pixel.
static void enlargeWhole(const ToolImageT *src, uint32_t scale, ToolImageT *dst) {
uint32_t x;
uint32_t y;
imageNew(dst, src->width * scale, src->height * scale);
for (y = 0; y < dst->height; y++) {
for (x = 0; x < dst->width; x++) {
memcpy(dst->pixels + ((size_t)y * dst->width + x) * RGBA_BYTES, src->pixels + ((size_t)(y / scale) * src->width + x / scale) * RGBA_BYTES, RGBA_BYTES);
}
}
}
// picture fitted into w x h, its shape kept, centred on transparency. Its
// sized copy never outgrows w x h (the scale is the smaller ratio, rounded
// to the nearest pixel), so the centring offsets are not negative.
static void fit(const ToolImageT *picture, uint32_t w, uint32_t h, ToolImageT *out) {
double scale;
double sw;
double sh;
uint32_t longest;
uint32_t y;
uint32_t ox;
uint32_t oy;
ToolImageT sized;
scale = fmin((double)w / (double)picture->width, (double)h / (double)picture->height);
longest = picture->width > picture->height ? picture->width : picture->height;
if (longest <= PIXEL_ART_MAX && scale >= 2.0) {
enlargeWhole(picture, (uint32_t)scale, &sized);
} else {
sw = nearbyint((double)picture->width * scale);
sh = nearbyint((double)picture->height * scale);
resizeLanczos(picture, sw < 1.0 ? 1u : (uint32_t)sw, sh < 1.0 ? 1u : (uint32_t)sh, &sized);
}
imageNew(out, w, h);
ox = (w - sized.width) / 2u;
oy = (h - sized.height) / 2u;
for (y = 0; y < sized.height; y++) {
memcpy(out->pixels + ((size_t)(oy + y) * w + ox) * RGBA_BYTES, sized.pixels + (size_t)y * sized.width * RGBA_BYTES, (size_t)sized.width * RGBA_BYTES);
}
toolImageFree(&sized);
}
// image over an opaque background colour.
static void flatten(const ToolImageT *image, const uint8_t background[RGB_BYTES], ToolImageT *flat) {
size_t i;
size_t pixels;
imageNew(flat, image->width, image->height);
pixels = (size_t)image->width * image->height;
for (i = 0; i < pixels; i++) {
memcpy(flat->pixels + i * RGBA_BYTES, background, RGB_BYTES);
flat->pixels[i * RGBA_BYTES + ALPHA] = (uint8_t)SAMPLE_MAX;
}
alphaComposite(flat, image);
}
// An HVIF of the palette pictures in sizes: each colour's outlines a closed
// path each, a shape a colour and size, hinted, and shown only at the
// scales [lodMin, lodMax]. False, out untouched, when that is more of
// anything than HVIF holds.
static bool hvif(const HvifSizeT *sizes, uint32_t sizeCount, ToolBufT *out) {
uint32_t styles[HVIF_MAX_ITEMS];
uint32_t styleCount;
uint32_t pathCount;
uint32_t shapeCount;
uint32_t style;
uint32_t first;
uint32_t rgb;
uint32_t s;
uint32_t colour;
uint32_t i;
bool present[PALETTE_SIZE];
bool ok;
ToolBufT paths;
ToolBufT shapes;
memset(&paths, 0, sizeof(paths));
memset(&shapes, 0, sizeof(shapes));
styleCount = 0u;
pathCount = 0u;
shapeCount = 0u;
ok = true;
for (s = 0; s < sizeCount && ok; s++) {
memset(present, 0, sizeof(present));
for (i = 0; i < sizes[s].size * sizes[s].size; i++) {
present[sizes[s].pixels[i]] = true;
}
for (colour = 0; colour < BE_TRANSPARENT && ok; colour++) {
if (!present[colour]) {
continue;
}
rgb = ((uint32_t)kBePalette[colour][0] << RGB_RED_SHIFT) | ((uint32_t)kBePalette[colour][1] << RGB_GREEN_SHIFT) | kBePalette[colour][2];
for (style = 0; style < styleCount && styles[style] != rgb; style++) {
}
if (style == styleCount) {
if (styleCount == HVIF_MAX_ITEMS) {
ok = false;
break;
}
styles[styleCount++] = rgb;
}
first = pathCount;
ok = hvifContours(sizes[s].pixels, sizes[s].size, (uint8_t)colour, HVIF_GRID / sizes[s].size, &paths, &pathCount) && pathCount <= HVIF_MAX_ITEMS && shapeCount < HVIF_MAX_ITEMS;
if (!ok) {
break;
}
toolBufByte(&shapes, HVIF_SHAPE_PATH_SOURCE);
toolBufByte(&shapes, style);
toolBufByte(&shapes, pathCount - first);
for (i = first; i < pathCount; i++) {
toolBufByte(&shapes, i);
}
toolBufByte(&shapes, HVIF_SHAPE_HINTING | HVIF_SHAPE_LOD_SCALE);
toolBufByte(&shapes, sizes[s].lodMin);
toolBufByte(&shapes, sizes[s].lodMax);
shapeCount++;
}
}
if (ok) {
toolBufAdd(out, HVIF_MAGIC, HVIF_MAGIC_BYTES);
toolBufByte(out, styleCount);
for (style = 0; style < styleCount; style++) {
toolBufByte(out, HVIF_STYLE_SOLID_COLOR);
toolBufByte(out, styles[style] >> RGB_RED_SHIFT);
toolBufByte(out, (styles[style] >> RGB_GREEN_SHIFT) & SAMPLE_MAX);
toolBufByte(out, styles[style] & SAMPLE_MAX);
toolBufByte(out, HVIF_OPAQUE);
}
toolBufByte(out, pathCount);
toolBufAdd(out, paths.data, paths.len);
toolBufByte(out, shapeCount);
toolBufAdd(out, shapes.data, shapes.len);
}
toolBufFree(&paths);
toolBufFree(&shapes);
return ok;
}
// The outlines of colour's pixels in the size x size palette pixels, each
// appended to paths as a closed HVIF path of its corner points: every pixel
// side with another colour (or the edge) beyond it, directed so the colour
// is on its right, chained head to tail. Outer edges come out clockwise and
// holes the other way, so a fill under either rule leaves the holes empty.
// Loops start at the first corner an edge was found from, and where two
// edges leave a corner the later found is taken first. False when a loop
// has more corners than a path holds.
static bool hvifContours(const uint8_t *pixels, uint32_t size, uint8_t colour, uint32_t units, ToolBufT *paths, uint32_t *pathCount) {
static const int32_t dx[HVIF_SIDES_PER_PIXEL][2] = { { 0, 1 }, { 1, 1 }, { 1, 0 }, { 0, 0 } };
static const int32_t dy[HVIF_SIDES_PER_PIXEL][2] = { { 0, 0 }, { 0, 1 }, { 1, 1 }, { 1, 0 } };
static const int32_t nx[HVIF_SIDES_PER_PIXEL] = { 0, 1, 0, -1 };
static const int32_t ny[HVIF_SIDES_PER_PIXEL] = { -1, 0, 1, 0 };
uint32_t side;
uint32_t points;
uint8_t *outCount;
uint32_t *outTo;
uint32_t *order;
uint32_t *loop;
uint32_t orderCount;
uint32_t cursor;
uint32_t n;
uint32_t i;
uint32_t corners;
uint32_t from;
uint32_t to;
uint32_t start;
uint32_t point;
uint32_t prev;
uint32_t next;
int32_t x;
int32_t y;
int32_t ax;
int32_t ay;
uint32_t k;
bool ok;
side = size + 1u;
points = side * side;
outCount = (uint8_t *)calloc(points, sizeof(uint8_t));
outTo = (uint32_t *)malloc((size_t)points * HVIF_EDGES_PER_POINT * sizeof(uint32_t));
order = (uint32_t *)malloc((size_t)points * sizeof(uint32_t));
loop = (uint32_t *)malloc(((size_t)size * size * HVIF_SIDES_PER_PIXEL + 1u) * sizeof(uint32_t));
if (outCount == NULL || outTo == NULL || order == NULL || loop == NULL) {
toolFail("out of memory");
}
orderCount = 0u;
for (y = 0; y < (int32_t)size; y++) {
for (x = 0; x < (int32_t)size; x++) {
if (pixels[(uint32_t)y * size + (uint32_t)x] != colour) {
continue;
}
for (k = 0; k < HVIF_SIDES_PER_PIXEL; k++) {
ax = x + nx[k];
ay = y + ny[k];
if (ax >= 0 && ay >= 0 && ax < (int32_t)size && ay < (int32_t)size && pixels[(uint32_t)ay * size + (uint32_t)ax] == colour) {
continue;
}
from = (uint32_t)(y + dy[k][0]) * side + (uint32_t)(x + dx[k][0]);
to = (uint32_t)(y + dy[k][1]) * side + (uint32_t)(x + dx[k][1]);
if (outCount[from] == 0u) {
order[orderCount++] = from;
}
outTo[from * HVIF_EDGES_PER_POINT + outCount[from]] = to;
outCount[from]++;
}
}
}
ok = true;
cursor = 0u;
for (;;) {
while (cursor < orderCount && outCount[order[cursor]] == 0u) {
cursor++;
}
if (cursor == orderCount) {
break;
}
start = order[cursor];
n = 0u;
loop[n++] = start;
point = start;
for (;;) {
outCount[point]--;
next = outTo[point * HVIF_EDGES_PER_POINT + outCount[point]];
if (next == start) {
break;
}
loop[n++] = next;
point = next;
}
// The corners alone: drop each point on a straight run (kept in
// place, never ahead of the point read).
corners = 0u;
prev = loop[n - 1u];
for (i = 0; i < n; i++) {
point = loop[i];
next = (i + 1u < n) ? loop[i + 1u] : start;
if ((prev % side == point % side) != (point % side == next % side)) {
loop[corners++] = point;
}
prev = point;
}
if (corners > HVIF_MAX_ITEMS) {
ok = false;
break;
}
toolBufByte(paths, HVIF_PATH_CLOSED | HVIF_PATH_NO_CURVES);
toolBufByte(paths, corners);
for (i = 0; i < corners; i++) {
toolBufByte(paths, HVIF_COORD_BIAS + loop[i] % side * units);
toolBufByte(paths, HVIF_COORD_BIAS + loop[i] / side * units);
}
(*pathCount)++;
}
free(loop);
free(order);
free(outTo);
free(outCount);
return ok;
}
// images (side, colour count, bit count, data) into a .ico.
static void icoFile(const uint32_t *sides, const uint32_t *colourCounts, const uint32_t *bitCounts, const ToolBufT *images, uint32_t count, const char *path) {
ToolBufT out;
uint32_t offset;
uint32_t i;
memset(&out, 0, sizeof(out));
toolBufLe16(&out, 0u);
toolBufLe16(&out, ICO_TYPE);
toolBufLe16(&out, count);
offset = ICONDIR_BYTES + ICONDIRENTRY_BYTES * count;
for (i = 0; i < count; i++) {
toolBufByte(&out, sides[i] % ENTRY_SIZE_WRAP);
toolBufByte(&out, sides[i] % ENTRY_SIZE_WRAP);
toolBufByte(&out, colourCounts[i] % ENTRY_SIZE_WRAP);
toolBufByte(&out, 0u);
toolBufLe16(&out, 1u);
toolBufLe16(&out, bitCounts[i]);
toolBufLe32(&out, (uint32_t)images[i].len);
toolBufLe32(&out, offset);
offset += (uint32_t)images[i].len;
}
for (i = 0; i < count; i++) {
toolBufAdd(&out, images[i].data, images[i].len);
}
writeOut(path, out.data, out.len);
toolBufFree(&out);
}
// One icon image: its BITMAPINFOHEADER, colour table, XOR bits and AND
// mask. bitCount 32: BGRA, black where wholly transparent (a system that
// draws through the AND mask XORs this in); 4 or 8: indices into colours,
// index 0 (black) where transparent.
static void icoImage(const ToolImageT *image, uint32_t bitCount, const uint8_t (*colours)[RGB_BYTES], uint32_t colourCount, ToolBufT *out) {
ToolBufT xorBits;
ToolBufT andBits;
uint8_t *indices;
uint32_t x;
uint32_t y;
uint32_t c;
size_t at;
size_t rowStart;
const uint8_t *p;
memset(&xorBits, 0, sizeof(xorBits));
memset(&andBits, 0, sizeof(andBits));
if (bitCount == ICO_TRUE_COLOUR_BITS) {
for (y = image->height; y-- > 0u;) {
for (x = 0; x < image->width; x++) {
p = image->pixels + ((size_t)y * image->width + x) * RGBA_BYTES;
if (p[ALPHA] == 0u) {
toolBufZeros(&xorBits, RGBA_BYTES);
} else {
toolBufByte(&xorBits, p[2]);
toolBufByte(&xorBits, p[1]);
toolBufByte(&xorBits, p[0]);
toolBufByte(&xorBits, p[ALPHA]);
}
}
}
} else {
indices = (uint8_t *)malloc((size_t)image->width * image->height);
if (indices == NULL) {
toolFail("out of memory");
}
toColours(image, colours, colourCount, bitCount == ICO_256_BITS, kBlack, indices);
for (y = image->height; y-- > 0u;) {
rowStart = xorBits.len;
for (x = 0; x < image->width; x += (bitCount == ICO_16_BITS) ? 2u : 1u) {
at = (size_t)y * image->width + x;
c = opaque(image, at) ? indices[at] : 0u;
if (bitCount == ICO_16_BITS) {
c <<= NIBBLE_BITS;
if (x + 1u < image->width && opaque(image, at + 1u)) {
c |= indices[at + 1u];
}
}
toolBufByte(&xorBits, c);
}
toolBufZeros(&xorBits, (DIB_ROW_ALIGN - (xorBits.len - rowStart) % DIB_ROW_ALIGN) % DIB_ROW_ALIGN);
}
free(indices);
}
andMask(image, &andBits);
toolBufLe32(out, DIB_HEADER_BYTES);
toolBufLe32(out, image->width);
toolBufLe32(out, image->height * 2u);
toolBufLe16(out, 1u);
toolBufLe16(out, bitCount);
toolBufLe32(out, 0u);
toolBufLe32(out, (uint32_t)(xorBits.len + andBits.len));
toolBufLe32(out, 0u);
toolBufLe32(out, 0u);
toolBufLe32(out, bitCount <= ICO_256_BITS ? colourCount : 0u);
toolBufLe32(out, 0u);
for (c = 0; c < colourCount && bitCount <= ICO_256_BITS; c++) {
toolBufByte(out, colours[c][2]);
toolBufByte(out, colours[c][1]);
toolBufByte(out, colours[c][0]);
toolBufByte(out, 0u);
}
toolBufAdd(out, xorBits.data, xorBits.len);
toolBufAdd(out, andBits.data, andBits.len);
toolBufFree(&xorBits);
toolBufFree(&andBits);
}
// The 16 colours of a Finder icon pixel, each the mean of its pair, in
// CIELAB; and which of them a colourful pixel may take.
static void iigsColours(LabT colours[IIGS_VALUES], uint32_t keeps[IIGS_VALUES], uint32_t *keepCount) {
double mean[RGB_BYTES];
uint32_t value;
uint32_t c;
*keepCount = 0u;
for (value = 0; value < IIGS_VALUES; value++) {
for (c = 0; c < RGB_BYTES; c++) {
mean[c] = ((double)kIigsFirst[value >> IIGS_HALF_BITS][c] + (double)kIigsSecond[value & IIGS_HALF_MASK][c]) / 2.0;
}
colours[value] = lab(mean);
if (value == IIGS_BLACK || value == IIGS_WHITE || chroma(colours[value]) >= IIGS_GREY_CHROMA) {
keeps[(*keepCount)++] = value;
}
}
}
// A QuickDraw II colour icon of the picture at w x h icon pixels: black on
// its edge, within it the nearest of the Finder's 16 colours.
static void iigsIcon(const ToolImageT *picture, uint32_t w, uint32_t h, ToolBufT *out) {
static const int32_t nx[] = { 1, -1, 0, 0 };
static const int32_t ny[] = { 0, 0, 1, -1 };
LabT colours[IIGS_VALUES];
LabT colour;
uint32_t keeps[IIGS_VALUES];
uint32_t all[IIGS_VALUES];
uint32_t keepCount;
uint32_t poolCount;
const uint32_t *pool;
uint32_t rowBytes;
uint32_t value;
uint32_t shift;
uint32_t v;
size_t at;
int32_t x;
int32_t y;
int32_t k;
int32_t ax;
int32_t ay;
double rgb[RGB_BYTES];
double best;
double d;
bool edge;
uint8_t *pixels;
uint8_t *shape;
const uint8_t *p;
ToolImageT tall;
ToolImageT image;
fit(picture, w, (uint32_t)nearbyint((double)h * IIGS_PIXEL_ASPECT), &tall);
resizeLanczos(&tall, w, h, &image);
iigsColours(colours, keeps, &keepCount);
for (v = 0; v < IIGS_VALUES; v++) {
all[v] = v;
}
rowBytes = (w + 1u) / 2u;
pixels = (uint8_t *)calloc((size_t)rowBytes * h, 1u);
shape = (uint8_t *)calloc((size_t)rowBytes * h, 1u);
if (pixels == NULL || shape == NULL) {
toolFail("out of memory");
}
for (y = 0; y < (int32_t)h; y++) {
for (x = 0; x < (int32_t)w; x++) {
if (!opaque(&image, (size_t)y * w + (size_t)x)) {
continue;
}
edge = false;
for (k = 0; k < (int32_t)COUNT_OF(nx); k++) {
ax = x + nx[k];
ay = y + ny[k];
if (ax < 0 || ay < 0 || ax >= (int32_t)w || ay >= (int32_t)h || !opaque(&image, (size_t)ay * w + (size_t)ax)) {
edge = true;
}
}
if (edge) {
value = IIGS_BLACK;
} else {
p = image.pixels + ((size_t)y * w + (size_t)x) * RGBA_BYTES;
for (v = 0; v < RGB_BYTES; v++) {
rgb[v] = p[v];
}
colour = lab(rgb);
pool = chroma(colour) < IIGS_GREY_CHROMA ? all : keeps;
poolCount = chroma(colour) < IIGS_GREY_CHROMA ? IIGS_VALUES : keepCount;
value = pool[0];
best = cie94(colour, colours[pool[0]]);
for (v = 1; v < poolCount; v++) {
d = cie94(colour, colours[pool[v]]);
if (d < best) {
best = d;
value = pool[v];
}
}
}
shift = (x % 2 == 0) ? NIBBLE_BITS : 0u;
at = (size_t)y * rowBytes + (size_t)x / 2u;
pixels[at] |= (uint8_t)(value << shift);
shape[at] |= (uint8_t)(IIGS_OPAQUE << shift);
}
}
toolBufLe16(out, IIGS_ICON_TYPE_COLOUR);
toolBufLe16(out, rowBytes * h);
toolBufLe16(out, h);
toolBufLe16(out, w);
toolBufAdd(out, pixels, (size_t)rowBytes * h);
toolBufAdd(out, shape, (size_t)rowBytes * h);
free(shape);
free(pixels);
toolImageFree(&image);
toolImageFree(&tall);
}
static void imageCopy(const ToolImageT *src, ToolImageT *dst) {
imageNew(dst, src->width, src->height);
memcpy(dst->pixels, src->pixels, (size_t)src->width * src->height * RGBA_BYTES);
}
// A transparent RGBA image, or a fatal error.
static void imageNew(ToolImageT *img, uint32_t w, uint32_t h) {
if (!toolImageCreate(img, w, h, false)) {
toolFail("out of memory");
}
}
// An sRGB colour (channels 0-255) in CIELAB.
static LabT lab(const double rgb[RGB_BYTES]) {
double channels[RGB_BYTES];
double f[RGB_BYTES];
double t;
uint32_t row;
uint32_t c;
LabT out;
for (c = 0; c < RGB_BYTES; c++) {
channels[c] = linear(rgb[c]);
}
for (row = 0; row < RGB_BYTES; row++) {
t = 0.0;
for (c = 0; c < RGB_BYTES; c++) {
t += kXyzFromRgb[row][c] * channels[c];
}
t /= kLabWhite[row];
f[row] = t > LAB_EPSILON ? pow(t, 1.0 / 3.0) : LAB_KAPPA * t + LAB_L_OFFSET / LAB_L_SCALE;
}
out.l = LAB_L_SCALE * f[1] - LAB_L_OFFSET;
out.a = LAB_A_SCALE * (f[0] - f[1]);
out.b = LAB_B_SCALE * (f[1] - f[2]);
return out;
}
// Lanczos' window: a sinc truncated to LANCZOS_LOBES lobes, weighted by a
// sinc that many times wider.
static double lanczos(double x) {
if (-LANCZOS_LOBES <= x && x < LANCZOS_LOBES) {
return sinc(x) * sinc(x / LANCZOS_LOBES);
}
return 0.0;
}
// An sRGB channel (0-255) as linear light, 0-1.
static double linear(double value) {
value /= (double)SAMPLE_MAX;
return value <= SRGB_LINEAR_MAX ? value / SRGB_LINEAR_SLOPE : pow((value + SRGB_OFFSET) / (1.0 + SRGB_OFFSET), SRGB_GAMMA);
}
// 256 colours for an 8-bit icon of image: black (where transparent: the
// AND mask's partner), then up to 255 of its own (flattened on black), cut
// down by median cut: the box with the widest channel (the most pixels of
// equals) is split at its pixels' median along it, until there are enough
// boxes or each holds one colour; a box's colour is its pixels' mean.
static void medianCut(const ToolImageT *image, uint8_t palette[PALETTE_SIZE][RGB_BYTES]) {
ToolImageT flat;
uint32_t *packed;
ShadeT *shades;
BoxT boxes[MEDIAN_COLOURS];
uint32_t boxCount;
uint32_t shadeCount;
uint32_t pixels;
uint32_t best;
uint32_t b;
uint32_t i;
uint32_t c;
uint32_t half;
uint32_t seen;
uint32_t cut;
uint64_t sum[RGB_BYTES];
const uint8_t *p;
flatten(image, kBlack, &flat);
pixels = flat.width * flat.height;
packed = (uint32_t *)malloc((size_t)pixels * sizeof(uint32_t));
shades = (ShadeT *)malloc((size_t)pixels * sizeof(ShadeT));
if (packed == NULL || shades == NULL) {
toolFail("out of memory");
}
for (i = 0; i < pixels; i++) {
p = flat.pixels + (size_t)i * RGBA_BYTES;
packed[i] = ((uint32_t)p[0] << RGB_RED_SHIFT) | ((uint32_t)p[1] << RGB_GREEN_SHIFT) | p[2];
}
qsort(packed, pixels, sizeof(uint32_t), packedCompare);
shadeCount = 0u;
for (i = 0; i < pixels; i++) {
if (shadeCount > 0u && shades[shadeCount - 1u].rgb == packed[i]) {
shades[shadeCount - 1u].pixels++;
} else {
shades[shadeCount].rgb = packed[i];
shades[shadeCount].pixels = 1u;
shadeCount++;
}
}
boxes[0].first = 0u;
boxes[0].count = shadeCount;
boxMeasure(&boxes[0], shades);
boxCount = 1u;
while (boxCount < MEDIAN_COLOURS) {
best = boxCount;
for (b = 0; b < boxCount; b++) {
if (boxes[b].range > 0u && (best == boxCount || boxes[b].range > boxes[best].range || (boxes[b].range == boxes[best].range && boxes[b].pixels > boxes[best].pixels))) {
best = b;
}
}
if (best == boxCount) {
break;
}
for (i = boxes[best].first; i < boxes[best].first + boxes[best].count; i++) {
shades[i].key = (((shades[i].rgb >> (RGB_RED_SHIFT - boxes[best].channel * BYTE_BITS)) & SAMPLE_MAX) << KEY_SHIFT) | shades[i].rgb;
}
qsort(shades + boxes[best].first, boxes[best].count, sizeof(ShadeT), shadeCompare);
half = (boxes[best].pixels + 1u) / 2u;
seen = 0u;
for (cut = 1u; cut < boxes[best].count - 1u; cut++) {
seen += shades[boxes[best].first + cut - 1u].pixels;
if (seen >= half) {
break;
}
}
boxes[boxCount].first = boxes[best].first + cut;
boxes[boxCount].count = boxes[best].count - cut;
boxes[best].count = cut;
boxMeasure(&boxes[best], shades);
boxMeasure(&boxes[boxCount], shades);
boxCount++;
}
memset(palette, 0, PALETTE_SIZE * RGB_BYTES);
for (b = 0; b < boxCount; b++) {
for (c = 0; c < RGB_BYTES; c++) {
sum[c] = 0u;
}
for (i = boxes[b].first; i < boxes[b].first + boxes[b].count; i++) {
for (c = 0; c < RGB_BYTES; c++) {
sum[c] += (uint64_t)((shades[i].rgb >> (RGB_RED_SHIFT - c * BYTE_BITS)) & SAMPLE_MAX) * shades[i].pixels;
}
}
for (c = 0; c < RGB_BYTES; c++) {
palette[b + 1u][c] = (uint8_t)((sum[c] + boxes[b].pixels / 2u) / boxes[b].pixels);
}
}
free(shades);
free(packed);
toolImageFree(&flat);
}
// The palette entry nearest (r, g, b) by squared distance, the first of
// equals, measured from the low corner of the colour's cell, as Pillow
// measures it.
static uint8_t nearestColour(PaletteT *palette, uint32_t r, uint32_t g, uint32_t b) {
uint32_t cell;
uint32_t i;
int32_t dr;
int32_t dg;
int32_t db;
uint32_t d;
uint32_t best;
cell = (r >> CELL_SHIFT) + (g >> CELL_SHIFT) * CELL_SIDE + (b >> CELL_SHIFT) * CELL_SIDE * CELL_SIDE;
if (palette->cache[cell] == CELL_EMPTY) {
best = UINT32_MAX;
for (i = 0; i < PALETTE_SIZE; i++) {
dr = (int32_t)(r & CELL_LOW_MASK) - palette->rgb[i][0];
dg = (int32_t)(g & CELL_LOW_MASK) - palette->rgb[i][1];
db = (int32_t)(b & CELL_LOW_MASK) - palette->rgb[i][2];
d = (uint32_t)(dr * dr + dg * dg + db * db);
if (d < best) {
best = d;
palette->cache[cell] = (int16_t)i;
}
}
}
return (uint8_t)palette->cache[cell];
}
// A pixel at least half opaque.
static bool opaque(const ToolImageT *image, size_t pixel) {
return image->pixels[pixel * RGBA_BYTES + ALPHA] >= ALPHA_OPAQUE_MIN;
}
static int packedCompare(const void *a, const void *b) {
uint32_t x;
uint32_t y;
x = *(const uint32_t *)a;
y = *(const uint32_t *)b;
return (x > y) - (x < y);
}
static void paletteFree(PaletteT *palette) {
free(palette->cache);
palette->cache = NULL;
}
// count colours, then the first again to 256, and an empty cache.
static void paletteInit(PaletteT *palette, const uint8_t (*colours)[RGB_BYTES], uint32_t count) {
uint32_t i;
for (i = 0; i < PALETTE_SIZE; i++) {
memcpy(palette->rgb[i], colours[i < count ? i : 0u], RGB_BYTES);
}
palette->cache = (int16_t *)malloc(CELL_COUNT * sizeof(int16_t));
if (palette->cache == NULL) {
toolFail("out of memory");
}
for (i = 0; i < CELL_COUNT; i++) {
palette->cache[i] = CELL_EMPTY;
}
}
// An icon side from the command line: 1 to ICON_SIDE_MAX.
static uint32_t parseSide(const char *text, size_t len, const char *what) {
long value;
if (!toolParseInt(text, len, &value) || value < 1 || value > (long)ICON_SIDE_MAX) {
toolFail("bad %s '%.*s', need 1 to %u", what, (int)len, text, ICON_SIDE_MAX);
}
return (uint32_t)value;
}
// text as a Pascal string (a length byte first) in size bytes.
static void pascal(ToolBufT *out, const char *text, uint32_t size) {
size_t len;
len = strlen(text);
if (len > size - 1u) {
len = size - 1u;
}
toolBufByte(out, (uint32_t)len);
toolBufAdd(out, text, len);
toolBufZeros(out, size - 1u - len);
}
// src with its colour weighted by its opacity (Pillow's RGBa).
static void premultiply(const ToolImageT *src, ToolImageT *dst) {
size_t i;
size_t pixels;
uint32_t c;
uint32_t mix;
const uint8_t *s;
uint8_t *d;
imageNew(dst, src->width, src->height);
pixels = (size_t)src->width * src->height;
for (i = 0; i < pixels; i++) {
s = src->pixels + i * RGBA_BYTES;
d = dst->pixels + i * RGBA_BYTES;
for (c = 0; c < RGB_BYTES; c++) {
mix = (uint32_t)s[c] * s[ALPHA] + BLEND_HALF;
d[c] = (uint8_t)(((mix >> DIV255_SHIFT) + mix) >> DIV255_SHIFT);
}
d[ALPHA] = s[ALPHA];
}
}
// One resampling pass over lines lines of RGBA samples: each line's
// outCount outputs from its inputs, inStep bytes apart (outStep in the
// output), lines inLineStep (outLineStep) apart. The horizontal pass steps
// a pixel along a row, the vertical a row down a column.
static void resamplePass(const uint8_t *in, uint8_t *out, const TapsT *taps, uint32_t outCount, uint32_t lines, size_t inStep, size_t inLineStep, size_t outStep, size_t outLineStep) {
const uint8_t *src;
const int32_t *w;
uint8_t *dst;
int32_t sum[RGBA_BYTES];
uint32_t line;
uint32_t o;
uint32_t t;
uint32_t c;
for (line = 0; line < lines; line++) {
for (o = 0; o < outCount; o++) {
src = in + line * inLineStep + taps->first[o] * inStep;
w = taps->weights + (size_t)o * taps->size;
for (c = 0; c < RGBA_BYTES; c++) {
sum[c] = TAP_HALF;
}
for (t = 0; t < taps->count[o]; t++) {
for (c = 0; c < RGBA_BYTES; c++) {
sum[c] += (int32_t)src[t * inStep + c] * w[t];
}
}
dst = out + line * outLineStep + o * outStep;
for (c = 0; c < RGBA_BYTES; c++) {
dst[c] = clampSample(sum[c]);
}
}
}
}
// src resized to w x h by Lanczos, as Pillow's resize does it: unchanged at
// its own size; otherwise premultiplied, a horizontal pass if the width
// changes then a vertical one if the height does, and divided back.
static void resizeLanczos(const ToolImageT *src, uint32_t w, uint32_t h, ToolImageT *dst) {
ToolImageT work;
ToolImageT next;
TapsT taps;
if (w == src->width && h == src->height) {
imageCopy(src, dst);
return;
}
premultiply(src, &work);
if (w != src->width) {
tapsBuild(&taps, src->width, w);
imageNew(&next, w, src->height);
resamplePass(work.pixels, next.pixels, &taps, w, src->height, RGBA_BYTES, (size_t)src->width * RGBA_BYTES, RGBA_BYTES, (size_t)w * RGBA_BYTES);
tapsFree(&taps);
toolImageFree(&work);
work = next;
}
if (h != src->height) {
tapsBuild(&taps, src->height, h);
imageNew(&next, w, h);
resamplePass(work.pixels, next.pixels, &taps, h, w, (size_t)w * RGBA_BYTES, RGBA_BYTES, (size_t)w * RGBA_BYTES, RGBA_BYTES);
tapsFree(&taps);
toolImageFree(&work);
work = next;
}
unpremultiply(&work, dst);
toolImageFree(&work);
}
static int shadeCompare(const void *a, const void *b) {
uint32_t x;
uint32_t y;
x = ((const ShadeT *)a)->key;
y = ((const ShadeT *)b)->key;
return (x > y) - (x < y);
}
static double sinc(double x) {
if (x == 0.0) {
return 1.0;
}
x = x * PI;
return sin(x) / x;
}
// The taps resampling inSize samples to outSize takes, as Pillow computes
// them: the window widened by the scale when shrinking, each output's
// weights normalised to sum to one, then rounded to fixed point.
static void tapsBuild(TapsT *taps, uint32_t inSize, uint32_t outSize) {
double scale;
double filterScale;
double reciprocal;
double support;
double center;
double total;
double *weights;
double v;
int32_t lo;
int32_t hi;
uint32_t o;
uint32_t t;
scale = (double)inSize / (double)outSize;
filterScale = scale < 1.0 ? 1.0 : scale;
support = LANCZOS_LOBES * filterScale;
reciprocal = 1.0 / filterScale;
taps->size = (uint32_t)ceil(support) * 2u + 1u;
taps->first = (uint32_t *)malloc((size_t)outSize * sizeof(uint32_t));
taps->count = (uint32_t *)malloc((size_t)outSize * sizeof(uint32_t));
taps->weights = (int32_t *)malloc((size_t)outSize * taps->size * sizeof(int32_t));
weights = (double *)malloc((size_t)taps->size * sizeof(double));
if (taps->first == NULL || taps->count == NULL || taps->weights == NULL || weights == NULL) {
toolFail("out of memory");
}
for (o = 0; o < outSize; o++) {
center = ((double)o + 0.5) * scale;
lo = (int32_t)(center - support + 0.5);
if (lo < 0) {
lo = 0;
}
hi = (int32_t)(center + support + 0.5);
if (hi > (int32_t)inSize) {
hi = (int32_t)inSize;
}
taps->first[o] = (uint32_t)lo;
taps->count[o] = (uint32_t)(hi - lo);
total = 0.0;
for (t = 0; t < taps->count[o]; t++) {
weights[t] = lanczos(((double)((int32_t)t + lo) - center + 0.5) * reciprocal);
total += weights[t];
}
for (t = 0; t < taps->count[o]; t++) {
v = total != 0.0 ? weights[t] / total : weights[t];
taps->weights[(size_t)o * taps->size + t] = (int32_t)(v < 0.0 ? -0.5 + v * TAP_ONE : 0.5 + v * TAP_ONE);
}
}
free(weights);
}
static void tapsFree(TapsT *taps) {
free(taps->first);
free(taps->count);
free(taps->weights);
}
// The image's pixels as indices into colours (count of them, padded to 256
// with the first): over background where transparent, dithered
// (Floyd-Steinberg) or the nearest.
static void toColours(const ToolImageT *image, const uint8_t (*colours)[RGB_BYTES], uint32_t count, bool dither, const uint8_t background[RGB_BYTES], uint8_t *indices) {
ToolImageT flat;
PaletteT palette;
size_t i;
size_t pixels;
const uint8_t *p;
flatten(image, background, &flat);
paletteInit(&palette, colours, count);
if (dither) {
ditherFloydSteinberg(&flat, &palette, indices);
} else {
pixels = (size_t)flat.width * flat.height;
for (i = 0; i < pixels; i++) {
p = flat.pixels + i * RGBA_BYTES;
indices[i] = nearestColour(&palette, p[0], p[1], p[2]);
}
}
paletteFree(&palette);
toolImageFree(&flat);
}
// Premultiplied colour divided back by its opacity (Pillow's RGBa to RGBA);
// clear and opaque pixels are left as they are.
static void unpremultiply(const ToolImageT *src, ToolImageT *dst) {
size_t i;
size_t pixels;
uint32_t alpha;
uint32_t c;
uint32_t v;
imageNew(dst, src->width, src->height);
pixels = (size_t)src->width * src->height;
for (i = 0; i < pixels; i++) {
alpha = src->pixels[i * RGBA_BYTES + ALPHA];
for (c = 0; c < RGB_BYTES; c++) {
v = src->pixels[i * RGBA_BYTES + c];
if (alpha != 0u && alpha != SAMPLE_MAX) {
v = SAMPLE_MAX * v / alpha;
if (v > SAMPLE_MAX) {
v = SAMPLE_MAX;
}
}
dst->pixels[i * RGBA_BYTES + c] = (uint8_t)v;
}
dst->pixels[i * RGBA_BYTES + ALPHA] = (uint8_t)alpha;
}
}
static void usage(void) {
size_t i;
for (i = 0; i < COUNT_OF(kUsage); i++) {
fprintf(stderr, "%s\n", kUsage[i]);
}
exit(USAGE_EXIT);
}
static void writeOut(const char *path, const uint8_t *data, size_t len) {
if (!toolWriteFile(path, data, len)) {
toolFail("cannot write %s", path);
}
}
int main(int argc, char **argv) {
const BakeT *bake;
ToolImageT loaded;
ToolImageT picture;
char err[TOOL_PNG_ERROR_BYTES];
size_t b;
toolFileName(TOOL_NAME);
bake = NULL;
for (b = 0; argc >= 2 && b < COUNT_OF(kBakes); b++) {
if (strcmp(argv[1], kBakes[b].name) == 0) {
bake = &kBakes[b];
}
}
if (bake == NULL || argc != (int)(4u + bake->args)) {
usage();
}
if (!toolPngLoad(argv[2], &loaded, err)) {
toolFail("%s: %s", argv[2], err);
}
if (loaded.indexed) {
if (!toolImageToRgba(&loaded, &picture)) {
toolFail("out of memory");
}
toolImageFree(&loaded);
} else {
picture = loaded;
}
bake->bake(&picture, argv + 3);
toolImageFree(&picture);
return 0;
}