joeylib2/tools/traceLogo/traceLogo.c
2026-10-19 15:46:47 -05:00

1028 lines
33 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// traceLogo.c - trace a flat-color PNG logo into a layered SVG.
//
// The JoeyLib logo ships as a 320x200 PNG, which is fine on screen and poor
// everywhere else: the manual's cover wants it large, and a bitmap that small
// either pixelates or blurs when scaled up. This makes a resolution-free SVG
// of the same artwork, so it can be set at any size.
//
// How it works. The art is flat cartoon color with antialiased edges, so:
//
// 1. k-means the opaque pixels down to --colors clusters, which recovers
// the handful of real colors and throws away the antialiasing gradient
// (in CIELAB, so a gray edge pixel joins the ink or the paper rather
// than whatever color is nearest it in RGB; the centers are seeded
// k-means++ style from a fixed seed, so a run is repeatable),
// 2. make a one-bit mask (a PBM) for each color,
// 3. run each mask through potrace, which fits Bezier curves to the region
// outlines,
// 4. write one SVG <path> for each color, filled with the exact color
// most of its pixels have (the art's own; the mean would be dragged
// toward whatever its antialiased edges blend with), fill-rule evenodd.
//
// Layer order matters: the paths are written largest area first so big
// shapes sit behind small ones, EXCEPT the darkest color, which is written
// last. In cartoon art the darkest color is the ink outline, and it has to
// be on top or the fills eat it.
//
// Transparent pixels are in no mask, so the SVG keeps the PNG's transparent
// background.
//
// Needs the potrace program (Peter Selinger's, GNU GPL v2+: the
// distribution's potrace package), found as $POTRACE or on the PATH. It is
// run as a separate program, a mask at a time, through its SVG backend at a
// point a pixel; its path data, in its own units with y up, is turned back
// into the image's pixels with y down. It is not a build dependency: the
// checked-in assets/joeylibLogo.svg is the deliverable, and this only needs
// running if the art changes:
//
// make -f make/tools.mk traceLogo
// build/tools/traceLogo assets/joeylib_logo_-_320x200.png assets/joeylibLogo.svg --colors 5
#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <errno.h>
#include <float.h>
#include <inttypes.h>
#include <math.h>
#include <spawn.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include "toolBuf.h"
#include "toolFile.h"
#include "toolPng.h"
#define TOOL_NAME "traceLogo"
#define USAGE_EXIT 2
#define CHANNELS 3u
#define RGBA_BYTES 4u
#define ALPHA 3u
#define SAMPLE_MAX 255
// The options' defaults and ranges.
#define ALPHA_DEFAULT 128
#define ALPHA_MIN 0
#define COLORS_DEFAULT 4
#define COLORS_MIN 1
#define COLORS_MAX 256
#define TURDSIZE_DEFAULT 2
#define TURDSIZE_MIN 0
#define TURDSIZE_MAX INT32_MAX
#define OPTION_PREFIX "--"
#define OPTION_VALUE '='
// CIELAB from sRGB (D65): the sRGB transfer curve, the linear RGB to XYZ
// matrix (kRgbToXyz), the white point, and Lab's cube-root curve with its
// linear foot.
#define SRGB_KNEE 0.04045
#define SRGB_SLOPE 12.92
#define SRGB_OFFSET 0.055
#define SRGB_GAMMA 2.4
#define WHITE_X 0.95047
#define WHITE_Y 1.0
#define WHITE_Z 1.08883
#define LAB_EPSILON (216.0 / 24389.0)
#define LAB_FOOT_SLOPE (841.0 / 108.0)
#define LAB_FOOT_OFFSET (4.0 / 29.0)
#define LAB_L_SCALE 116.0
#define LAB_L_OFFSET 16.0
#define LAB_A_SCALE 500.0
#define LAB_B_SCALE 200.0
// k-means: at most this many rounds of assigning and re-centering, from
// centers seeded by a splitmix64 stream that starts here.
#define KMEANS_ITERATIONS 24u
#define KMEANS_SEED 1u
#define SPLITMIX_GAMMA UINT64_C(0x9E3779B97F4A7C15)
#define SPLITMIX_MUL_A UINT64_C(0xBF58476D1CE4E5B9)
#define SPLITMIX_MUL_B UINT64_C(0x94D049BB133111EB)
#define SPLITMIX_SHIFT_A 30u
#define SPLITMIX_SHIFT_B 27u
#define SPLITMIX_SHIFT_C 31u
// A double in [0, 1) from the top 53 bits of a 64-bit draw.
#define UNIT_SHIFT 11u
#define UNIT_SCALE (1.0 / 9007199254740992.0)
// Rec. 601 luma, which picks the darkest color (the ink).
#define LUMA_R 0.299
#define LUMA_G 0.587
#define LUMA_B 0.114
// A pixel in no color's mask.
#define LABEL_CLEAR (-1)
// A raw PBM: one bit a pixel, set where traced, rows of whole bytes.
#define BYTE_BITS 8u
#define PBM_FIRST_BIT 0x80u
// potrace: the program, and its SVG backend at a point a pixel (72 dpi),
// coordinates to a hundredth of a pixel (as they are written), the whole
// trace as one path.
#define POTRACE_ENV "POTRACE"
#define POTRACE_PROGRAM "potrace"
#define POTRACE_DPI "72"
#define POTRACE_UNIT "100"
#define SVG_TRANSFORM "transform=\"translate("
#define SVG_TRANSFORM_REST "%lf,%lf) scale(%lf,%lf)"
#define TRANSFORM_VALUES 4
#define SVG_PATH "<path"
#define SVG_PATH_DATA "d=\""
#define SVG_QUOTE '"'
#define CUBIC_POINTS 3u
#define POINT_VALUES 2u
// The temporary mask and trace.
#define TEMP_DIR_ENV "TMPDIR"
#define TEMP_DIR_DEFAULT "/tmp"
#define TEMP_NAME "traceLogoXXXXXX"
#define TEMP_PATH_BYTES 4096u
#define COLOR_TEXT_BYTES 8u
#define NUMBER_TEXT_BYTES 32u
#define PRINTF_BYTES 128u
#define COUNT_OF(a) (sizeof(a) / sizeof((a)[0]))
#define TEXT_OF(x) #x
#define VALUE_OF(x) TEXT_OF(x)
typedef enum OptionE {
OPTION_ALPHA,
OPTION_COLORS,
OPTION_TURDSIZE,
OPTION_COUNT
} OptionE;
typedef enum PathArgE {
PATH_INPUT,
PATH_OUTPUT,
PATH_COUNT
} PathArgE;
typedef enum TempE {
TEMP_PBM,
TEMP_SVG,
TEMP_COUNT
} TempE;
// A --name N option: its default and the values it takes.
typedef struct OptionT {
const char *name;
long initial;
long min;
long max;
} OptionT;
typedef struct ArgsT {
const char *paths[PATH_COUNT];
long values[OPTION_COUNT];
} ArgsT;
// One color's layer: its k-means cluster, its pixels, its color, and its
// outline as SVG path data.
typedef struct LayerT {
uint32_t cluster;
uint32_t area;
char color[COLOR_TEXT_BYTES];
double luma;
ToolBufT path;
} LayerT;
typedef struct PointT {
double x;
double y;
} PointT;
// potrace's units to the image's pixels: x' = dx + sx x, y' = dy + sy y.
typedef struct TransformT {
double dx;
double dy;
double sx;
double sy;
} TransformT;
static void *allocate(size_t count, size_t size);
static void bufEscaped(ToolBufT *buf, const char *text);
static void bufPrintf(ToolBufT *buf, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static double distance2(const double *a, const double *b);
static void kmeans(const double *samples, size_t count, uint32_t k, double *centers, uint32_t *labels);
static void kmeansSeed(const double *samples, size_t count, uint32_t k, uint64_t *random, double *centers);
static double labCurve(double t);
static void labOf(const double *rgb, double *lab);
static int layerCompare(const void *a, const void *b);
static void modeColor(const double *samples, const uint32_t *labels, size_t count, uint32_t cluster, uint32_t *rgb);
static uint32_t nearestCenter(const double *sample, const double *centers, uint32_t k);
static int packedCompare(const void *a, const void *b);
static void parseArgs(int argc, char **argv, ArgsT *args);
static const char *pathConvert(const char *d, const TransformT *t, ToolBufT *out);
static void pathPoint(ToolBufT *out, char lead, const TransformT *t, PointT point);
static void pbmWrite(const int32_t *labelImage, uint32_t width, uint32_t height, int32_t cluster);
static char *potraceProgram(void);
static void potraceRun(char *turdsize);
static uint64_t randomBelow(uint64_t *state, uint64_t bound);
static uint64_t randomNext(uint64_t *state);
static double randomUnit(uint64_t *state);
static PointT readPoint(const char **d, PointT origin);
static double srgbLinear(double value);
static void svgPaths(const char *svg, ToolBufT *out);
static void tempCreate(void);
static void tempRemove(void);
static void trace(const int32_t *labelImage, uint32_t width, uint32_t height, int32_t cluster, char *turdsize, ToolBufT *path);
static void usage(void);
static void writeSvg(const char *path, const char *title, uint32_t width, uint32_t height, const LayerT *layers, uint32_t layerCount, uint32_t ink);
extern char **environ;
static const OptionT kOptions[OPTION_COUNT] = {
{ "alpha", ALPHA_DEFAULT, ALPHA_MIN, SAMPLE_MAX },
{ "colors", COLORS_DEFAULT, COLORS_MIN, COLORS_MAX },
{ "turdsize", TURDSIZE_DEFAULT, TURDSIZE_MIN, TURDSIZE_MAX }
};
static const PointT kOrigin = { 0.0, 0.0 };
// sRGB's linear light to CIE XYZ, and the D65 white XYZ is scaled by.
static const double kRgbToXyz[CHANNELS][CHANNELS] = {
{ 0.4124, 0.3576, 0.1805 },
{ 0.2126, 0.7152, 0.0722 },
{ 0.0193, 0.1192, 0.9505 }
};
static const double kWhite[CHANNELS] = { WHITE_X, WHITE_Y, WHITE_Z };
static const char *const kUsage[] = {
"usage: traceLogo INPUT.png OUTPUT.svg [--colors N] [--alpha N] [--turdsize N]",
"",
"Trace a flat-color PNG into a layered SVG, one path a color.",
"",
" --colors N number of flat colors to recover (default " VALUE_OF(COLORS_DEFAULT) ")",
" --alpha N alpha above which a pixel counts as opaque (default " VALUE_OF(ALPHA_DEFAULT) ")",
" --turdsize N drop traced specks of up to this many pixels (default " VALUE_OF(TURDSIZE_DEFAULT) ")",
"",
"Needs the potrace program (the distribution's potrace package), found as",
"$POTRACE or on the PATH."
};
// The temporary mask and trace (a path is empty until its file is made, and
// removing an empty path does nothing).
static char gTempPaths[TEMP_COUNT][TEMP_PATH_BYTES];
// Zeroed memory for count items of size bytes; running out is fatal.
static void *allocate(size_t count, size_t size) {
void *p;
p = calloc(count, size);
if (p == NULL) {
toolFail("out of memory");
}
return p;
}
// text as XML character data.
static void bufEscaped(ToolBufT *buf, const char *text) {
for (; *text != '\0'; text++) {
switch (*text) {
case '&':
bufPrintf(buf, "&amp;");
break;
case '<':
bufPrintf(buf, "&lt;");
break;
case '>':
bufPrintf(buf, "&gt;");
break;
default:
toolBufByte(buf, (uint8_t)*text);
break;
}
}
}
// printf onto the end of buf (a short item: a number, a point, a tag).
static void bufPrintf(ToolBufT *buf, const char *fmt, ...) {
char text[PRINTF_BYTES];
va_list args;
int len;
va_start(args, fmt);
len = vsnprintf(text, sizeof(text), fmt, args);
va_end(args);
if (len < 0 || (size_t)len >= sizeof(text)) {
toolFail("text too long: %s", fmt);
}
toolBufAdd(buf, text, (size_t)len);
}
// The squared distance between two colors.
static double distance2(const double *a, const double *b) {
double sum;
double d;
uint32_t c;
sum = 0.0;
for (c = 0; c < CHANNELS; c++) {
d = a[c] - b[c];
sum += d * d;
}
return sum;
}
// k-means over count samples (CHANNELS doubles each): k centers, and each
// sample's nearest. Rounds of assigning every sample its nearest center and
// moving each center to the mean of its samples (a center with none stays
// put) run until no sample changes center, at most KMEANS_ITERATIONS times.
static void kmeans(const double *samples, size_t count, uint32_t k, double *centers, uint32_t *labels) {
double *sums;
size_t *members;
size_t i;
uint64_t random;
uint32_t iteration;
uint32_t label;
uint32_t j;
uint32_t c;
bool changed;
random = KMEANS_SEED;
kmeansSeed(samples, count, k, &random, centers);
sums = allocate((size_t)k * CHANNELS, sizeof(*sums));
members = allocate(k, sizeof(*members));
for (iteration = 0; iteration < KMEANS_ITERATIONS; iteration++) {
changed = false;
for (i = 0; i < count; i++) {
label = nearestCenter(samples + i * CHANNELS, centers, k);
changed = changed || label != labels[i];
labels[i] = label;
}
if (!changed) {
break;
}
memset(sums, 0, (size_t)k * CHANNELS * sizeof(*sums));
memset(members, 0, k * sizeof(*members));
for (i = 0; i < count; i++) {
members[labels[i]]++;
for (c = 0; c < CHANNELS; c++) {
sums[labels[i] * CHANNELS + c] += samples[i * CHANNELS + c];
}
}
for (j = 0; j < k; j++) {
if (members[j] == 0u) {
continue;
}
for (c = 0; c < CHANNELS; c++) {
centers[j * CHANNELS + c] = sums[j * CHANNELS + c] / (double)members[j];
}
}
}
free(sums);
free(members);
}
// k-means++ seeding, simplified: the first center is a sample at random;
// each next one a sample drawn with odds by its squared distance to the
// nearest center so far (so far-apart colors are favored), or at random
// when every sample already sits on a center.
static void kmeansSeed(const double *samples, size_t count, uint32_t k, uint64_t *random, double *centers) {
double *nearest;
double total;
double target;
double cumulative;
double d;
size_t i;
size_t pick;
uint32_t c;
nearest = allocate(count, sizeof(*nearest));
for (i = 0; i < count; i++) {
nearest[i] = DBL_MAX;
}
pick = (size_t)randomBelow(random, count);
memcpy(centers, samples + pick * CHANNELS, CHANNELS * sizeof(*centers));
for (c = 1; c < k; c++) {
total = 0.0;
for (i = 0; i < count; i++) {
d = distance2(samples + i * CHANNELS, centers + (c - 1u) * CHANNELS);
if (d < nearest[i]) {
nearest[i] = d;
}
total += nearest[i];
}
if (total <= 0.0) {
pick = (size_t)randomBelow(random, count);
} else {
// The first sample whose running share of the total reaches
// the draw.
target = randomUnit(random);
cumulative = 0.0;
pick = count - 1u;
for (i = 0; i < count; i++) {
cumulative += nearest[i] / total;
if (cumulative >= target) {
pick = i;
break;
}
}
}
memcpy(centers + c * CHANNELS, samples + pick * CHANNELS, CHANNELS * sizeof(*centers));
}
free(nearest);
}
// Lab's curve: the cube root, with a straight foot near black.
static double labCurve(double t) {
return t > LAB_EPSILON ? cbrt(t) : t * LAB_FOOT_SLOPE + LAB_FOOT_OFFSET;
}
// An sRGB color (0-255 a channel) as CIELAB, where distance follows what the
// eye sees: a mid-gray antialiasing pixel is nearer the ink or the paper
// than an orange it happens to sit close to in RGB.
static void labOf(const double *rgb, double *lab) {
double linear[CHANNELS];
double curved[CHANNELS];
uint32_t row;
uint32_t c;
for (c = 0; c < CHANNELS; c++) {
linear[c] = srgbLinear(rgb[c]);
}
for (row = 0; row < CHANNELS; row++) {
curved[row] = 0.0;
for (c = 0; c < CHANNELS; c++) {
curved[row] += kRgbToXyz[row][c] * linear[c];
}
curved[row] = labCurve(curved[row] / kWhite[row]);
}
lab[0] = LAB_L_SCALE * curved[1] - LAB_L_OFFSET;
lab[1] = LAB_A_SCALE * (curved[0] - curved[1]);
lab[2] = LAB_B_SCALE * (curved[1] - curved[2]);
}
// Largest area first; equal areas in cluster order.
static int layerCompare(const void *a, const void *b) {
const LayerT *la;
const LayerT *lb;
la = (const LayerT *)a;
lb = (const LayerT *)b;
if (la->area != lb->area) {
return la->area > lb->area ? -1 : 1;
}
return la->cluster < lb->cluster ? -1 : 1;
}
// The most common exact color among a cluster's samples: the art's own flat
// color, which the cluster's mean misses (its antialiased edge pixels drag
// the mean toward the colors they blend with). Of equal counts, the
// lowest packed value.
static void modeColor(const double *samples, const uint32_t *labels, size_t count, uint32_t cluster, uint32_t *rgb) {
uint32_t *packed;
uint32_t best;
uint32_t c;
size_t n;
size_t i;
size_t run;
size_t bestRun;
packed = allocate(count, sizeof(*packed));
n = 0;
for (i = 0; i < count; i++) {
if (labels[i] != cluster) {
continue;
}
packed[n] = 0;
for (c = 0; c < CHANNELS; c++) {
packed[n] = (packed[n] << BYTE_BITS) | (uint32_t)samples[i * CHANNELS + c];
}
n++;
}
qsort(packed, n, sizeof(*packed), packedCompare);
best = packed[0];
bestRun = 0;
for (i = 0; i < n; i += run) {
for (run = 1; i + run < n && packed[i + run] == packed[i]; run++) {
}
if (run > bestRun) {
best = packed[i];
bestRun = run;
}
}
for (c = 0; c < CHANNELS; c++) {
rgb[c] = (best >> (BYTE_BITS * (CHANNELS - 1u - c))) & SAMPLE_MAX;
}
free(packed);
}
// The center nearest a sample (the first, on a tie).
static uint32_t nearestCenter(const double *sample, const double *centers, uint32_t k) {
double best;
double d;
uint32_t bestCenter;
uint32_t j;
best = distance2(sample, centers);
bestCenter = 0;
for (j = 1; j < k; j++) {
d = distance2(sample, centers + j * CHANNELS);
if (d < best) {
best = d;
bestCenter = j;
}
}
return bestCenter;
}
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 ? -1 : (x > y ? 1 : 0);
}
// INPUT OUTPUT and the options (--name N or --name=N), in any order.
static void parseArgs(int argc, char **argv, ArgsT *args) {
const char *name;
const char *value;
size_t nameLen;
uint32_t pathCount;
uint32_t o;
int a;
long number;
for (o = 0; o < OPTION_COUNT; o++) {
args->values[o] = kOptions[o].initial;
}
pathCount = 0;
for (a = 1; a < argc; a++) {
if (argv[a][0] != OPTION_PREFIX[0]) {
if (pathCount == PATH_COUNT) {
usage();
}
args->paths[pathCount] = argv[a];
pathCount++;
continue;
}
if (strncmp(argv[a], OPTION_PREFIX, strlen(OPTION_PREFIX)) != 0) {
usage();
}
name = argv[a] + strlen(OPTION_PREFIX);
value = strchr(name, OPTION_VALUE);
if (value != NULL) {
nameLen = (size_t)(value - name);
value++;
} else {
nameLen = strlen(name);
a++;
if (a == argc) {
usage();
}
value = argv[a];
}
for (o = 0; o < OPTION_COUNT; o++) {
if (strlen(kOptions[o].name) == nameLen && strncmp(kOptions[o].name, name, nameLen) == 0) {
break;
}
}
if (o == OPTION_COUNT) {
usage();
}
if (!toolParseInt(value, strlen(value), &number) || number < kOptions[o].min || number > kOptions[o].max) {
toolFail("--%s takes a whole number from %ld to %ld, not \"%s\"", kOptions[o].name, kOptions[o].min, kOptions[o].max, value);
}
args->values[o] = number;
}
if (pathCount != PATH_COUNT) {
usage();
}
}
// potrace's path data (its SVG backend's: moves, lines, and cubics, absolute
// or relative, in its units) appended to out as absolute image-space path
// data (M, L, C, and Z, to a hundredth of a pixel). Returns where the data
// ends (its closing quote).
static const char *pathConvert(const char *d, const TransformT *t, ToolBufT *out) {
PointT current;
PointT start;
PointT origin;
uint32_t i;
char command;
command = '\0';
current = kOrigin;
start = kOrigin;
for (;;) {
while (isspace((unsigned char)*d) || *d == ',') {
d++;
}
if (*d == SVG_QUOTE || *d == '\0') {
return d;
}
if (isalpha((unsigned char)*d)) {
command = *d;
d++;
if (toupper((unsigned char)command) == 'Z') {
toolBufByte(out, 'Z');
current = start;
}
continue;
}
origin = islower((unsigned char)command) ? current : kOrigin;
switch (toupper((unsigned char)command)) {
case 'M':
current = readPoint(&d, origin);
start = current;
pathPoint(out, 'M', t, current);
// Pairs after a move's first are lines.
command = islower((unsigned char)command) ? 'l' : 'L';
break;
case 'L':
current = readPoint(&d, origin);
pathPoint(out, 'L', t, current);
break;
case 'C':
// A cubic's three points are all from where it starts.
for (i = 0; i < CUBIC_POINTS; i++) {
current = readPoint(&d, origin);
pathPoint(out, i == 0u ? 'C' : ' ', t, current);
}
break;
default:
toolFail("potrace wrote path data this does not read, at \"%.16s\"", d);
}
}
}
// One point of image-space path data, after lead (a command or a space).
static void pathPoint(ToolBufT *out, char lead, const TransformT *t, PointT point) {
bufPrintf(out, "%c%.2f,%.2f", lead, t->dx + t->sx * point.x, t->dy + t->sy * point.y);
}
// The mask of one cluster's pixels, as a raw PBM in the temporary mask file.
static void pbmWrite(const int32_t *labelImage, uint32_t width, uint32_t height, int32_t cluster) {
ToolBufT buf;
uint32_t x;
uint32_t y;
uint32_t bits;
memset(&buf, 0, sizeof(buf));
bufPrintf(&buf, "P4\n%" PRIu32 " %" PRIu32 "\n", width, height);
for (y = 0; y < height; y++) {
bits = 0;
for (x = 0; x < width; x++) {
if (labelImage[(size_t)y * width + x] == cluster) {
bits |= PBM_FIRST_BIT >> (x % BYTE_BITS);
}
if (x % BYTE_BITS == BYTE_BITS - 1u || x == width - 1u) {
toolBufByte(&buf, bits);
bits = 0;
}
}
}
if (!toolWriteFile(gTempPaths[TEMP_PBM], buf.data, buf.len)) {
toolFail("cannot write %s", gTempPaths[TEMP_PBM]);
}
toolBufFree(&buf);
}
// The potrace to run: $POTRACE, or potrace on the PATH.
static char *potraceProgram(void) {
char *program;
program = getenv(POTRACE_ENV);
if (program == NULL || program[0] == '\0') {
program = POTRACE_PROGRAM;
}
return program;
}
// potrace over the temporary mask, its SVG into the temporary trace.
static void potraceRun(char *turdsize) {
char *args[] = { potraceProgram(), "--backend", "svg", "--flat", "--resolution", POTRACE_DPI, "--unit", POTRACE_UNIT, "--turdsize", turdsize, "--output", gTempPaths[TEMP_SVG], "--", gTempPaths[TEMP_PBM], NULL };
pid_t pid;
int status;
int err;
err = posix_spawnp(&pid, args[0], NULL, NULL, args, environ);
if (err != 0) {
toolFail("cannot run %s (%s): it needs the potrace program, from the distribution's potrace package, on the PATH or named by $%s", args[0], strerror(err), POTRACE_ENV);
}
if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status) || WEXITSTATUS(status) != 0) {
toolFail("%s failed on %s", args[0], gTempPaths[TEMP_PBM]);
}
}
// A whole number below bound (bound is far below 2^64, so the bias of the
// remainder is nothing).
static uint64_t randomBelow(uint64_t *state, uint64_t bound) {
return randomNext(state) % bound;
}
// splitmix64: the next 64 bits of the stream.
static uint64_t randomNext(uint64_t *state) {
uint64_t z;
*state += SPLITMIX_GAMMA;
z = *state;
z = (z ^ (z >> SPLITMIX_SHIFT_A)) * SPLITMIX_MUL_A;
z = (z ^ (z >> SPLITMIX_SHIFT_B)) * SPLITMIX_MUL_B;
return z ^ (z >> SPLITMIX_SHIFT_C);
}
// A double in [0, 1).
static double randomUnit(uint64_t *state) {
return (double)(randomNext(state) >> UNIT_SHIFT) * UNIT_SCALE;
}
// An x,y pair of potrace's path data, from origin.
static PointT readPoint(const char **d, PointT origin) {
PointT point;
double value[POINT_VALUES];
char *end;
uint32_t v;
for (v = 0; v < POINT_VALUES; v++) {
while (isspace((unsigned char)**d) || **d == ',') {
(*d)++;
}
value[v] = strtod(*d, &end);
if (end == *d) {
toolFail("potrace wrote path data this does not read, at \"%.16s\"", *d);
}
*d = end;
}
point.x = origin.x + value[0];
point.y = origin.y + value[1];
return point;
}
// An sRGB channel (0-255) as linear light.
static double srgbLinear(double value) {
value /= SAMPLE_MAX;
return value <= SRGB_KNEE ? value / SRGB_SLOPE : pow((value + SRGB_OFFSET) / (1.0 + SRGB_OFFSET), SRGB_GAMMA);
}
// potrace's SVG: the path data of its paths, through its group's transform
// (potrace's units, y up, to the image's pixels, y down), appended to out.
static void svgPaths(const char *svg, ToolBufT *out) {
TransformT t;
const char *p;
p = strstr(svg, SVG_TRANSFORM);
if (p == NULL || sscanf(p + strlen(SVG_TRANSFORM), SVG_TRANSFORM_REST, &t.dx, &t.dy, &t.sx, &t.sy) != TRANSFORM_VALUES) {
toolFail("potrace's SVG has no transform this reads");
}
for (p = strstr(p, SVG_PATH); p != NULL; p = strstr(p, SVG_PATH)) {
p = strstr(p, SVG_PATH_DATA);
if (p == NULL) {
toolFail("potrace's SVG has a path with no data");
}
p = pathConvert(p + strlen(SVG_PATH_DATA), &t, out);
}
}
// Make the temporary mask and trace files (in $TMPDIR, or /tmp), removed at
// exit, however it comes.
static void tempCreate(void) {
const char *dir;
char path[TEMP_PATH_BYTES];
uint32_t t;
int fd;
dir = getenv(TEMP_DIR_ENV);
if (dir == NULL || dir[0] == '\0') {
dir = TEMP_DIR_DEFAULT;
}
if (atexit(tempRemove) != 0) {
toolFail("cannot arrange to remove temporary files");
}
for (t = 0; t < TEMP_COUNT; t++) {
if ((size_t)snprintf(path, sizeof(path), "%s/%s", dir, TEMP_NAME) >= sizeof(path)) {
toolFail("$%s is too long", TEMP_DIR_ENV);
}
fd = mkstemp(path);
if (fd < 0) {
toolFail("cannot make a temporary file in %s: %s", dir, strerror(errno));
}
close(fd);
memcpy(gTempPaths[t], path, sizeof(path));
}
}
static void tempRemove(void) {
uint32_t t;
for (t = 0; t < TEMP_COUNT; t++) {
unlink(gTempPaths[t]);
}
}
// One cluster's outline, as image-space path data in path: its mask through
// potrace.
static void trace(const int32_t *labelImage, uint32_t width, uint32_t height, int32_t cluster, char *turdsize, ToolBufT *path) {
uint8_t *svg;
size_t svgLen;
pbmWrite(labelImage, width, height, cluster);
potraceRun(turdsize);
if (!toolReadFile(gTempPaths[TEMP_SVG], &svg, &svgLen)) {
toolFail("cannot read potrace's SVG %s", gTempPaths[TEMP_SVG]);
}
svgPaths((const char *)svg, path);
free(svg);
}
static void usage(void) {
size_t i;
for (i = 0; i < COUNT_OF(kUsage); i++) {
fprintf(stderr, "%s\n", kUsage[i]);
}
exit(USAGE_EXIT);
}
// The SVG: a path a layer, in layers' order (largest first) but the ink
// last, on top.
static void writeSvg(const char *path, const char *title, uint32_t width, uint32_t height, const LayerT *layers, uint32_t layerCount, uint32_t ink) {
ToolBufT svg;
uint32_t i;
uint32_t layer;
memset(&svg, 0, sizeof(svg));
bufPrintf(&svg, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
bufPrintf(&svg, "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 %" PRIu32 " %" PRIu32 "\" width=\"%" PRIu32 "\" height=\"%" PRIu32 "\">\n", width, height, width, height);
bufPrintf(&svg, " <title>");
bufEscaped(&svg, title);
bufPrintf(&svg, "</title>\n");
// Every layer but the ink, in order, then the ink.
for (i = 0; i <= layerCount; i++) {
if (i == ink) {
continue;
}
layer = i < layerCount ? i : ink;
bufPrintf(&svg, " <path fill=\"%s\" fill-rule=\"evenodd\" d=\"", layers[layer].color);
toolBufAdd(&svg, layers[layer].path.data, layers[layer].path.len);
bufPrintf(&svg, "\"/>\n");
}
bufPrintf(&svg, "</svg>\n");
if (!toolWriteFile(path, svg.data, svg.len)) {
toolFail("cannot write %s", path);
}
toolBufFree(&svg);
}
int main(int argc, char **argv) {
ArgsT args;
ToolImageT loaded;
ToolImageT picture;
LayerT *layers;
double *samples;
double *lab;
double *centers;
int32_t *labelImage;
uint32_t *labels;
uint32_t *areas;
const char *input;
size_t pixels;
size_t count;
size_t p;
uint32_t colors;
uint32_t layerCount;
uint32_t ink;
uint32_t j;
uint32_t c;
uint32_t rgb[CHANNELS];
char err[TOOL_PNG_ERROR_BYTES];
char turdsize[NUMBER_TEXT_BYTES];
toolFileName(TOOL_NAME);
parseArgs(argc, argv, &args);
input = args.paths[PATH_INPUT];
colors = (uint32_t)args.values[OPTION_COLORS];
if (!toolPngLoad(input, &loaded, err)) {
toolFail("%s: %s", input, err);
}
if (loaded.indexed) {
if (!toolImageToRgba(&loaded, &picture)) {
toolFail("out of memory");
}
toolImageFree(&loaded);
} else {
picture = loaded;
}
// The opaque pixels' colors, in order, and where each pixel's goes.
pixels = (size_t)picture.width * picture.height;
samples = allocate(pixels, CHANNELS * sizeof(*samples));
labelImage = allocate(pixels, sizeof(*labelImage));
count = 0;
for (p = 0; p < pixels; p++) {
labelImage[p] = LABEL_CLEAR;
if (picture.pixels[p * RGBA_BYTES + ALPHA] > args.values[OPTION_ALPHA]) {
for (c = 0; c < CHANNELS; c++) {
samples[count * CHANNELS + c] = picture.pixels[p * RGBA_BYTES + c];
}
labelImage[p] = (int32_t)count;
count++;
}
}
if (count == 0u) {
toolFail("%s is fully transparent", input);
}
centers = allocate(colors, CHANNELS * sizeof(*centers));
labels = allocate(count, sizeof(*labels));
areas = allocate(colors, sizeof(*areas));
// Clustered in CIELAB; each layer still takes its color from the RGB.
lab = allocate(count, CHANNELS * sizeof(*lab));
for (p = 0; p < count; p++) {
labOf(samples + p * CHANNELS, lab + p * CHANNELS);
}
kmeans(lab, count, colors, centers, labels);
free(lab);
for (p = 0; p < pixels; p++) {
if (labelImage[p] != LABEL_CLEAR) {
labelImage[p] = (int32_t)labels[labelImage[p]];
areas[labelImage[p]]++;
}
}
// A layer for each color some pixel has, traced.
tempCreate();
snprintf(turdsize, sizeof(turdsize), "%ld", args.values[OPTION_TURDSIZE]);
layers = allocate(colors, sizeof(*layers));
layerCount = 0;
for (j = 0; j < colors; j++) {
if (areas[j] == 0u) {
continue;
}
modeColor(samples, labels, count, j, rgb);
layers[layerCount].cluster = j;
layers[layerCount].area = areas[j];
layers[layerCount].luma = LUMA_R * rgb[0] + LUMA_G * rgb[1] + LUMA_B * rgb[2];
snprintf(layers[layerCount].color, COLOR_TEXT_BYTES, "#%02" PRIX32 "%02" PRIX32 "%02" PRIX32, rgb[0], rgb[1], rgb[2]);
trace(labelImage, picture.width, picture.height, (int32_t)j, turdsize, &layers[layerCount].path);
layerCount++;
}
// Largest first, so big shapes sit behind; the ink (the darkest) on top.
qsort(layers, layerCount, sizeof(*layers), layerCompare);
ink = 0;
for (j = 1; j < layerCount; j++) {
if (layers[j].luma < layers[ink].luma) {
ink = j;
}
}
writeSvg(args.paths[PATH_OUTPUT], input, picture.width, picture.height, layers, layerCount, ink);
printf("%s: %s -- %" PRIu32 " color layers, %" PRIu32 "x%" PRIu32 " viewBox\n", TOOL_NAME, args.paths[PATH_OUTPUT], layerCount, picture.width, picture.height);
for (j = 0; j < layerCount; j++) {
printf(" %s %6" PRIu32 " px\n", layers[j].color, layers[j].area);
toolBufFree(&layers[j].path);
}
free(layers);
free(areas);
free(labels);
free(centers);
free(labelImage);
free(samples);
toolImageFree(&picture);
return 0;
}