#include <SDL3/SDL.h>
#include <math.h>
#include <stdbool.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "candidates.h"
#include "colour.h"
#include "image.h"
static uint32_t failures = 0;
#define CHECK(expression) do { \
if (!(expression)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expression); \
++failures; \
} \
} while (0)
static RGB rgb(uint8_t red, uint8_t green, uint8_t blue) {
RGB colour = {red, green, blue};
return colour;
}
static SDL_Surface *new_surface(int width, int height) {
SDL_Surface *surface = SDL_CreateSurface(width, height, SDL_PIXELFORMAT_RGBA32);
CHECK(surface != NULL);
return surface;
}
static void set_pixel(SDL_Surface *surface, int x, int y, RGB colour,
uint8_t alpha) {
uint8_t *pixel = (uint8_t *)surface->pixels +
(size_t)y * (size_t)surface->pitch + (size_t)x * 4U;
pixel[0] = colour.r;
pixel[1] = colour.g;
pixel[2] = colour.b;
pixel[3] = alpha;
}
static void test_hex_parsing(void) {
RGB colour = {0, 0, 0};
CHECK(colour_parse_hex("#1eA2fF", &colour));
CHECK(colour.r == 0x1e && colour.g == 0xa2 && colour.b == 0xff);
CHECK(!colour_parse_hex("1ea2ff", &colour));
CHECK(!colour_parse_hex("#123", &colour));
CHECK(!colour_parse_hex("#gg0000", &colour));
}
static void test_linear_average(void) {
ColourAccumulator accumulator = {0.0, 0.0, 0.0, 0.0};
RGB average;
colour_accumulator_add_u8(&accumulator, 0, 0, 0, 255);
colour_accumulator_add_u8(&accumulator, 255, 255, 255, 255);
CHECK(colour_accumulator_average(&accumulator, &average));
CHECK(average.r >= 187 && average.r <= 188);
CHECK(average.r == average.g && average.g == average.b);
}
static void test_partial_blocks(void) {
SDL_Surface *surface = new_surface(17, 1);
CandidateSet set;
int x;
candidates_init(&set);
for (x = 0; x < 16; ++x) set_pixel(surface, x, 0, rgb(255, 0, 0), 255);
set_pixel(surface, 16, 0, rgb(0, 0, 255), 255);
CHECK(image_process_surface(surface, 16, 1, &set));
CHECK(set.count == 2);
CHECK(fabs(set.items[0].prevalence - 16.0) < 0.001);
CHECK(fabs(set.items[1].prevalence - 1.0) < 0.001);
candidates_destroy(&set);
SDL_DestroySurface(surface);
}
static void test_transparency(void) {
SDL_Surface *surface = new_surface(2, 1);
CandidateSet set;
candidates_init(&set);
set_pixel(surface, 0, 0, rgb(255, 0, 0), 0);
set_pixel(surface, 1, 0, rgb(0, 0, 255), 255);
CHECK(image_process_surface(surface, 2, 1, &set));
CHECK(set.count == 1);
CHECK(set.items[0].colour.r == 0 && set.items[0].colour.g == 0 &&
set.items[0].colour.b == 255);
CHECK(fabs(set.items[0].prevalence - 1.0) < 0.001);
candidates_destroy(&set);
SDL_DestroySurface(surface);
}
static void test_partial_alpha(void) {
SDL_Surface *surface = new_surface(2, 1);
CandidateSet set;
candidates_init(&set);
set_pixel(surface, 0, 0, rgb(255, 0, 0), 255);
set_pixel(surface, 1, 0, rgb(0, 0, 255), 128);
CHECK(image_process_surface(surface, 2, 1, &set));
CHECK(set.count == 1);
CHECK(set.items[0].colour.r > set.items[0].colour.b);
CHECK(set.items[0].colour.b > 100);
CHECK(fabs(set.items[0].prevalence - (1.0 + 128.0 / 255.0)) < 0.001);
candidates_destroy(&set);
SDL_DestroySurface(surface);
}
static void test_arena(void) {
Arena arena;
uint32_t *first;
max_align_t *aligned;
arena_init(&arena);
first = arena_allocate(&arena, 32, sizeof(*first));
aligned = arena_allocate(&arena, 1, sizeof(*aligned));
CHECK(first != NULL && aligned != NULL);
CHECK((uintptr_t)aligned % _Alignof(max_align_t) == 0);
CHECK(arena_allocate(&arena, SIZE_MAX, 2) == NULL);
arena_destroy(&arena);
}
static void test_deduplication(void) {
CandidateSet set;
candidates_init(&set);
CHECK(candidates_add(&set, rgb(255, 0, 0), 2.0));
CHECK(candidates_add(&set, rgb(250, 5, 5), 3.0));
CHECK(set.count == 1);
CHECK(fabs(set.items[0].prevalence - 5.0) < 0.001);
CHECK(candidates_add(&set, rgb(0, 0, 255), 1.0));
CHECK(set.count == 2);
candidates_destroy(&set);
}
static void test_candidate_ranking(void) {
CandidateSet set;
ScoredCandidate *results = NULL;
size_t count = 0;
size_t index;
candidates_init(&set);
CHECK(candidates_add(&set, rgb(255, 80, 80), 30.0));
CHECK(candidates_add(&set, rgb(80, 180, 255), 20.0));
CHECK(candidates_add(&set, rgb(100, 255, 120), 10.0));
CHECK(candidates_rank(&set, 3, rgb(30, 30, 30), 0.0f, &results, &count));
CHECK(count == 3);
for (index = 1; index < count; ++index) {
CHECK(results[index - 1].score >= results[index].score);
}
results = NULL;
CHECK(candidates_rank(&set, 1, rgb(30, 30, 30), 0.0f, &results, &count));
CHECK(count == 1);
CHECK(results[0].colour.r == 255 && results[0].colour.g == 80);
results = NULL;
CHECK(candidates_rank(&set, 3, rgb(30, 30, 30), 1.01f,
&results, &count));
CHECK(count == 0 && results == NULL);
candidates_destroy(&set);
}
static void test_chromatic_candidate_admission(void) {
CandidateSet set;
ScoredCandidate *results = NULL;
size_t count = 0;
uint32_t value;
candidates_init(&set);
set.merge_distance = 0.001;
for (value = 24; value <= 216; value += 8) {
CHECK(candidates_add(&set, rgb((uint8_t)value, (uint8_t)value,
(uint8_t)value),
1000.0 - (double)value));
}
CHECK(candidates_add(&set, rgb(150, 90, 90), 2.0));
CHECK(candidates_add(&set, rgb(190, 55, 65), 1.0));
CHECK(candidates_rank(&set, 3, rgb(30, 30, 30), 0.0f,
&results, &count));
CHECK(count == 3);
CHECK((results[0].colour.r == 190 && results[0].colour.g == 55) ||
(results[1].colour.r == 190 && results[1].colour.g == 55) ||
(results[2].colour.r == 190 && results[2].colour.g == 55));
candidates_destroy(&set);
}
static void test_final_tweak(void) {
CandidateSet set;
ScoredCandidate *results = NULL;
size_t count = 0;
const RGB background = {0x1e, 0x1e, 0x1e};
RGB original;
float original_score;
candidates_init(&set);
CHECK(candidates_add(&set, rgb(0x7a, 0xa2, 0xf7), 10.0));
CHECK(candidates_rank(&set, 1, background, 0.0f, &results, &count));
CHECK(count == 1);
original = results[0].colour;
original_score = results[0].score;
candidates_tweak(results, count, background);
CHECK(results[0].score >= original_score);
CHECK(fabs((double)results[0].score -
(double)accent_score(results[0].colour, background)) < 0.000001);
CHECK(results[0].colour.r != original.r ||
results[0].colour.g != original.g ||
results[0].colour.b != original.b);
candidates_destroy(&set);
}
static bool write_bytes(const char *path, const uint8_t *bytes,
size_t length) {
FILE *file = fopen(path, "wb");
bool success;
if (file == NULL) return false;
success = fwrite(bytes, 1, length, file) == length;
success = fclose(file) == 0 && success;
return success;
}
static void test_static_input(void) {
static const uint8_t ppm[] = {
'P', '6', '\n', '2', ' ', '1', '\n', '2', '5', '5', '\n',
255, 0, 0, 255, 0, 0
};
const char *path = "/tmp/accent-extract-static-test.data";
CandidateSet set;
CHECK(write_bytes(path, ppm, sizeof(ppm)));
candidates_init(&set);
CHECK(image_process_file(path, 16, &set));
CHECK(set.count == 1);
CHECK(set.items[0].colour.r == 255 && set.items[0].colour.g == 0);
CHECK(fabs(set.items[0].prevalence - 2.0) < 0.001);
candidates_destroy(&set);
CHECK(remove(path) == 0);
}
static void test_duration_weighting(void) {
SDL_Surface *red = new_surface(1, 1);
SDL_Surface *blue = new_surface(1, 1);
CandidateSet set;
candidates_init(&set);
set_pixel(red, 0, 0, rgb(255, 0, 0), 255);
set_pixel(blue, 0, 0, rgb(0, 0, 255), 255);
CHECK(image_process_surface(red, 16, 20, &set));
CHECK(image_process_surface(blue, 16, 200, &set));
CHECK(set.count == 2);
CHECK(fabs(set.items[0].prevalence - 20.0) < 0.001);
CHECK(fabs(set.items[1].prevalence - 200.0) < 0.001);
candidates_destroy(&set);
SDL_DestroySurface(red);
SDL_DestroySurface(blue);
}
static void test_farbfeld(void) {
static const uint8_t farbfeld[] = {
'f','a','r','b','f','e','l','d', 0,0,0,2, 0,0,0,1,
0xff,0xff, 0,0, 0,0, 0xff,0xff,
0,0, 0,0, 0xff,0xff, 0xff,0xff
};
const char *path = "/tmp/accent-extract-farbfeld-test.unknown";
CandidateSet set;
CHECK(write_bytes(path, farbfeld, sizeof(farbfeld)));
candidates_init(&set);
CHECK(image_process_file(path, 1, &set));
CHECK(set.count == 2);
CHECK(set.items[0].colour.r == 255 && set.items[0].colour.b == 0);
CHECK(set.items[1].colour.r == 0 && set.items[1].colour.b == 255);
candidates_destroy(&set);
CHECK(remove(path) == 0);
}
int main(void) {
CHECK(SDL_Init(0));
test_hex_parsing();
test_linear_average();
test_partial_blocks();
test_transparency();
test_partial_alpha();
test_arena();
test_deduplication();
test_candidate_ranking();
test_chromatic_candidate_admission();
test_final_tweak();
test_static_input();
test_duration_weighting();
test_farbfeld();
SDL_Quit();
if (failures != 0) {
fprintf(stderr, "%" PRIu32 " test(s) failed\n", failures);
return EXIT_FAILURE;
}
puts("all tests passed");
return EXIT_SUCCESS;
}