1028 lines
33 KiB
C
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-colour 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 colour with antialiased edges, so:
|
|
//
|
|
// 1. k-means the opaque pixels down to --colors clusters, which recovers
|
|
// the handful of real colours and throws away the antialiasing gradient
|
|
// (in CIELAB, so a grey edge pixel joins the ink or the paper rather
|
|
// than whatever colour is nearest it in RGB; the centres are seeded
|
|
// k-means++ style from a fixed seed, so a run is repeatable),
|
|
// 2. make a one-bit mask (a PBM) for each colour,
|
|
// 3. run each mask through potrace, which fits Bezier curves to the region
|
|
// outlines,
|
|
// 4. write one SVG <path> for each colour, filled with the exact colour
|
|
// 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 colour, which is written
|
|
// last. In cartoon art the darkest colour 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-centring, from
|
|
// centres 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 colour (the ink).
|
|
#define LUMA_R 0.299
|
|
#define LUMA_G 0.587
|
|
#define LUMA_B 0.114
|
|
|
|
// A pixel in no colour'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 colour's layer: its k-means cluster, its pixels, its colour, 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 *centres, uint32_t *labels);
|
|
static void kmeansSeed(const double *samples, size_t count, uint32_t k, uint64_t *random, double *centres);
|
|
static double labCurve(double t);
|
|
static void labOf(const double *rgb, double *lab);
|
|
static int layerCompare(const void *a, const void *b);
|
|
static void modeColour(const double *samples, const uint32_t *labels, size_t count, uint32_t cluster, uint32_t *rgb);
|
|
static uint32_t nearestCentre(const double *sample, const double *centres, 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-colour PNG into a layered SVG, one path a colour.",
|
|
"",
|
|
" --colors N number of flat colours 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, "&");
|
|
break;
|
|
case '<':
|
|
bufPrintf(buf, "<");
|
|
break;
|
|
case '>':
|
|
bufPrintf(buf, ">");
|
|
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 colours.
|
|
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 centres, and each
|
|
// sample's nearest. Rounds of assigning every sample its nearest centre and
|
|
// moving each centre to the mean of its samples (a centre with none stays
|
|
// put) run until no sample changes centre, at most KMEANS_ITERATIONS times.
|
|
static void kmeans(const double *samples, size_t count, uint32_t k, double *centres, 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, centres);
|
|
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 = nearestCentre(samples + i * CHANNELS, centres, 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++) {
|
|
centres[j * CHANNELS + c] = sums[j * CHANNELS + c] / (double)members[j];
|
|
}
|
|
}
|
|
}
|
|
free(sums);
|
|
free(members);
|
|
}
|
|
|
|
|
|
// k-means++ seeding, simplified: the first centre is a sample at random;
|
|
// each next one a sample drawn with odds by its squared distance to the
|
|
// nearest centre so far (so far-apart colours are favoured), or at random
|
|
// when every sample already sits on a centre.
|
|
static void kmeansSeed(const double *samples, size_t count, uint32_t k, uint64_t *random, double *centres) {
|
|
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(centres, samples + pick * CHANNELS, CHANNELS * sizeof(*centres));
|
|
for (c = 1; c < k; c++) {
|
|
total = 0.0;
|
|
for (i = 0; i < count; i++) {
|
|
d = distance2(samples + i * CHANNELS, centres + (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(centres + c * CHANNELS, samples + pick * CHANNELS, CHANNELS * sizeof(*centres));
|
|
}
|
|
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 colour (0-255 a channel) as CIELAB, where distance follows what the
|
|
// eye sees: a mid-grey 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 colour among a cluster's samples: the art's own flat
|
|
// colour, which the cluster's mean misses (its antialiased edge pixels drag
|
|
// the mean toward the colours they blend with). Of equal counts, the
|
|
// lowest packed value.
|
|
static void modeColour(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 centre nearest a sample (the first, on a tie).
|
|
static uint32_t nearestCentre(const double *sample, const double *centres, uint32_t k) {
|
|
double best;
|
|
double d;
|
|
uint32_t bestCentre;
|
|
uint32_t j;
|
|
|
|
best = distance2(sample, centres);
|
|
bestCentre = 0;
|
|
for (j = 1; j < k; j++) {
|
|
d = distance2(sample, centres + j * CHANNELS);
|
|
if (d < best) {
|
|
best = d;
|
|
bestCentre = j;
|
|
}
|
|
}
|
|
return bestCentre;
|
|
}
|
|
|
|
|
|
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 *centres;
|
|
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' colours, 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);
|
|
}
|
|
|
|
centres = allocate(colors, CHANNELS * sizeof(*centres));
|
|
labels = allocate(count, sizeof(*labels));
|
|
areas = allocate(colors, sizeof(*areas));
|
|
// Clustered in CIELAB; each layer still takes its colour 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, centres, 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 colour 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;
|
|
}
|
|
modeColour(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 " colour 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(centres);
|
|
free(labelImage);
|
|
free(samples);
|
|
toolImageFree(&picture);
|
|
return 0;
|
|
}
|