// 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 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 #include #include #include #include #include #include #include #include #include #include #include #include #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 "': 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, "\n"); bufPrintf(&svg, "\n", width, height, width, height); bufPrintf(&svg, " "); bufEscaped(&svg, title); bufPrintf(&svg, "\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, " \n"); } bufPrintf(&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; }