#include <SDL3/SDL.h>
#include <errno.h>
#include <inttypes.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "candidates.h"
#include "colour.h"
#include "image.h"
typedef struct {
const char *input;
RGB background;
size_t limit;
float minimum_score;
uint32_t block_size;
bool have_background;
bool tweak;
} Options;
static void print_usage(FILE *stream) {
fprintf(stream,
"Usage: accent-extract IMAGE --background '#RRGGBB' [OPTIONS]\n"
"\n"
"Options:\n"
" --background COLOR Required background colour (#RRGGBB).\n"
" --limit N Pre-score candidate limit (default: 20).\n"
" --min-score FLOAT Minimum score to print (default: 0.60).\n"
" --block-size N Square averaging block size (default: 16).\n"
" --tweak Refine output lightness and saturation.\n"
" --help Show this help.\n");
}
static bool parse_positive_size(const char *text, size_t *out) {
char *end;
uintmax_t value;
errno = 0;
value = strtoumax(text, &end, 10);
if (errno != 0 || end == text || *end != '\0' || value == 0 ||
value > SIZE_MAX) {
return false;
}
*out = (size_t)value;
return true;
}
static bool parse_block_size(const char *text, uint32_t *out) {
size_t value;
if (!parse_positive_size(text, &value) || value > UINT32_MAX) {
return false;
}
*out = (uint32_t)value;
return true;
}
static bool parse_score(const char *text, float *out) {
char *end;
float value;
errno = 0;
value = strtof(text, &end);
if (errno != 0 || end == text || *end != '\0' || !isfinite(value) ||
value < 0.0f || value > 1.0f) {
return false;
}
*out = value;
return true;
}
static int parse_options(int argc, char **argv, Options *options) {
int index;
options->input = NULL;
options->limit = 20;
options->minimum_score = 0.60f;
options->block_size = 16;
options->have_background = false;
options->tweak = false;
for (index = 1; index < argc; ++index) {
const char *argument = argv[index];
if (strcmp(argument, "--help") == 0) {
print_usage(stdout);
return 1;
}
if (strcmp(argument, "--tweak") == 0) {
options->tweak = true;
continue;
}
if (strcmp(argument, "--background") == 0 ||
strcmp(argument, "--limit") == 0 ||
strcmp(argument, "--min-score") == 0 ||
strcmp(argument, "--block-size") == 0) {
const char *value;
if (++index >= argc) {
fprintf(stderr, "accent-extract: %s requires a value\n", argument);
return -1;
}
value = argv[index];
if (strcmp(argument, "--background") == 0) {
if (!colour_parse_hex(value, &options->background)) {
fprintf(stderr, "accent-extract: invalid background colour: %s\n",
value);
return -1;
}
options->have_background = true;
} else if (strcmp(argument, "--limit") == 0) {
if (!parse_positive_size(value, &options->limit)) {
fprintf(stderr, "accent-extract: invalid limit: %s\n", value);
return -1;
}
} else if (strcmp(argument, "--min-score") == 0) {
if (!parse_score(value, &options->minimum_score)) {
fprintf(stderr, "accent-extract: invalid minimum score: %s\n",
value);
return -1;
}
} else if (!parse_block_size(value, &options->block_size)) {
fprintf(stderr, "accent-extract: invalid block size: %s\n", value);
return -1;
}
} else if (argument[0] == '-') {
fprintf(stderr, "accent-extract: unknown option: %s\n", argument);
return -1;
} else if (options->input != NULL) {
fprintf(stderr, "accent-extract: only one input image is accepted\n");
return -1;
} else {
options->input = argument;
}
}
if (options->input == NULL) {
fprintf(stderr, "accent-extract: no input image supplied\n");
return -1;
}
if (!options->have_background) {
fprintf(stderr, "accent-extract: --background is required\n");
return -1;
}
return 0;
}
int main(int argc, char **argv) {
Options options;
CandidateSet candidates;
ScoredCandidate *results = NULL;
size_t result_count = 0;
size_t index;
int parsed = parse_options(argc, argv, &options);
if (parsed != 0) {
if (parsed < 0) {
print_usage(stderr);
}
return parsed < 0 ? EXIT_FAILURE : EXIT_SUCCESS;
}
if (!SDL_Init(0)) {
fprintf(stderr, "accent-extract: SDL initialisation failed: %s\n",
SDL_GetError());
return EXIT_FAILURE;
}
candidates_init(&candidates);
if (!image_process_file(options.input, options.block_size, &candidates)) {
fprintf(stderr, "accent-extract: could not process '%s': %s\n",
options.input, SDL_GetError());
candidates_destroy(&candidates);
SDL_Quit();
return EXIT_FAILURE;
}
if (!candidates_rank(&candidates, options.limit, options.background,
options.minimum_score, &results, &result_count)) {
fprintf(stderr, "accent-extract: out of memory while ranking colours\n");
candidates_destroy(&candidates);
SDL_Quit();
return EXIT_FAILURE;
}
if (options.tweak) {
candidates_tweak(results, result_count, options.background);
}
for (index = 0; index < result_count; ++index) {
printf("#%02x%02x%02x %.3f\n", results[index].colour.r,
results[index].colour.g, results[index].colour.b,
(double)results[index].score);
}
candidates_destroy(&candidates);
SDL_Quit();
return EXIT_SUCCESS;
}