From 3c62d3d54244731b641952bd7e93edbab16d5d79 Mon Sep 17 00:00:00 2001 From: Mitja Felicijan Date: Thu, 22 Jan 2026 23:53:24 +0100 Subject: Add PPM simple image implementation --- examples/.gitignore | 4 + examples/Makefile | 7 +- examples/logging.c | 32 ++++---- examples/ppm.c | 29 ++++++++ nonstd.h | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++- tests.c | 97 +++++++++++++++++++++++- 6 files changed, 355 insertions(+), 23 deletions(-) create mode 100644 examples/ppm.c diff --git a/examples/.gitignore b/examples/.gitignore index dcb0ae9..b4d84dd 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -1,3 +1,4 @@ +# Build artifacts stringv stringb foreach @@ -6,4 +7,7 @@ slice arena files logging +ppm +# Generated artifacts +*.ppm \ No newline at end of file diff --git a/examples/Makefile b/examples/Makefile index cd83e92..50d531b 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -5,7 +5,7 @@ CFLAGS = -Wall -Wextra -std=c99 -fsanitize=address -g -O0 LDFLAGS = # Example targets -EXAMPLES = foreach stringv stringb array slice arena files logging +EXAMPLES = foreach stringv stringb array slice arena files logging ppm # Default target all: $(EXAMPLES) @@ -35,6 +35,9 @@ files: files.c logging: logging.c $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) +ppm: ppm.c + $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) + # Run all examples run: all @echo "\n=== Running stringv ===\n" @@ -53,6 +56,8 @@ run: all @./files @echo "\n=== Running logging ===\n" @./logging + @echo "\n=== Running ppm ===\n" + @./ppm # Clean build artifacts clean: diff --git a/examples/logging.c b/examples/logging.c index 1d2744c..8e87d59 100644 --- a/examples/logging.c +++ b/examples/logging.c @@ -2,24 +2,24 @@ #include "../nonstd.h" int main(void) { - // Default level is LOG_INFO - LOG_INFO_MSG("This is an info message: %d", 42); - LOG_DEBUG_MSG("This debug message will NOT be shown by default"); + // Default level is LOG_INFO + LOG_INFO_MSG("This is an info message: %d", 42); + LOG_DEBUG_MSG("This debug message will NOT be shown by default"); - // Change level to LOG_DEBUG - set_log_level(LOG_DEBUG); - LOG_DEBUG_MSG("Now debug messages are shown: %s", "hello"); + // Change level to LOG_DEBUG + set_log_level(LOG_DEBUG); + LOG_DEBUG_MSG("Now debug messages are shown: %s", "hello"); - // Warnings and Errors - LOG_WARN_MSG("This is a warning!"); - LOG_ERROR_MSG("This is an error!"); + // Warnings and Errors + LOG_WARN_MSG("This is a warning!"); + LOG_ERROR_MSG("This is an error!"); - // Environment variable override test - // You can set LOG_LEVEL=1 (WARN) etc. - LogLevel env_level = get_log_level_from_env(); - if (env_level != LOG_DEBUG) { - printf("Environment overrides level to: %d\n", env_level); - } + // Environment variable override test + // You can set LOG_LEVEL=1 (WARN) etc. + LogLevel env_level = get_log_level_from_env(); + if (env_level != LOG_DEBUG) { + printf("Environment overrides level to: %d\n", env_level); + } - return 0; + return 0; } diff --git a/examples/ppm.c b/examples/ppm.c new file mode 100644 index 0000000..95fafcf --- /dev/null +++ b/examples/ppm.c @@ -0,0 +1,29 @@ +#define NONSTD_IMPLEMENTATION +#include "../nonstd.h" + +int main() { + u32 width = 400; + u32 height = 400; + Canvas img = ppm_init(width, height); + + // Background + ppm_fill(&img, COLOR_HEX(0x1a1a1a)); + + // Draw some shapes + ppm_draw_rect(&img, 50, 50, 100, 100, CLR_RED); + ppm_draw_circle(&img, 250, 100, 40, CLR_BLUE); + ppm_draw_triangle(&img, 50, 350, 150, 350, 100, 250, CLR_YELLOW); + ppm_draw_line(&img, 200, 200, 350, 350, CLR_GREEN); + + // Random colors and macros + ppm_draw_rect(&img, 200, 250, 50, 80, COLOR_RGB(255, 165, 0)); + + if (ppm_save(&img, "example.ppm")) { + printf("Image saved to example.ppm\n"); + } else { + printf("Failed to save image\n"); + } + + ppm_free(&img); + return 0; +} diff --git a/nonstd.h b/nonstd.h index b826dfb..efe10af 100644 --- a/nonstd.h +++ b/nonstd.h @@ -7,14 +7,14 @@ #ifndef NONSTD_H #define NONSTD_H +#include #include #include #include #include #include -#include -#include #include +#include #include #ifndef NONSTD_DEF @@ -249,6 +249,41 @@ NONSTD_DEF void arena_grow(Arena *a, size_t min_size); NONSTD_DEF void *arena_alloc(Arena *a, size_t size); NONSTD_DEF void arena_free(Arena *a); +// Image - simple RGB image structure +typedef struct { + u8 r, g, b; +} Color; + +typedef struct { + u32 width; + u32 height; + Color *pixels; +} Canvas; + +#define COLOR_RGB(r, g, b) ((Color){(u8)(r), (u8)(g), (u8)(b)}) +#define COLOR_HEX(hex) ((Color){(u8)(((hex) >> 16) & 0xFF), (u8)(((hex) >> 8) & 0xFF), (u8)((hex) & 0xFF)}) + +#define CLR_BLACK COLOR_RGB(0, 0, 0) +#define CLR_WHITE COLOR_RGB(255, 255, 255) +#define CLR_RED COLOR_RGB(255, 0, 0) +#define CLR_GREEN COLOR_RGB(0, 255, 0) +#define CLR_BLUE COLOR_RGB(0, 0, 255) +#define CLR_YELLOW COLOR_RGB(255, 255, 0) +#define CLR_MAGENTA COLOR_RGB(255, 0, 255) +#define CLR_CYAN COLOR_RGB(0, 255, 255) + +NONSTD_DEF Canvas ppm_init(u32 width, u32 height); +NONSTD_DEF void ppm_free(Canvas *img); +NONSTD_DEF void ppm_set_pixel(Canvas *img, u32 x, u32 y, Color color); +NONSTD_DEF Color ppm_get_pixel(const Canvas *img, u32 x, u32 y); +NONSTD_DEF int ppm_save(const Canvas *img, const char *filename); +NONSTD_DEF Canvas ppm_read(const char *filename); +NONSTD_DEF void ppm_fill(Canvas *canvas, Color color); +NONSTD_DEF void ppm_draw_rect(Canvas *canvas, u32 x, u32 y, u32 w, u32 h, Color color); +NONSTD_DEF void ppm_draw_line(Canvas *canvas, i32 x0, i32 y0, i32 x1, i32 y1, Color color); +NONSTD_DEF void ppm_draw_circle(Canvas *canvas, i32 x, i32 y, i32 r, Color color); +NONSTD_DEF void ppm_draw_triangle(Canvas *canvas, i32 x0, i32 y0, i32 x1, i32 y1, i32 x2, i32 y2, Color color); + // File I/O helpers NONSTD_DEF char *read_entire_file(const char *filepath, size_t *out_size); NONSTD_DEF int write_entire_file(const char *filepath, const void *data, size_t size); @@ -297,6 +332,8 @@ NONSTD_DEF void *safe_realloc(void *ptr, size_t item_size, size_t count) { return realloc(ptr, item_size * count); } +// String View Implementation + NONSTD_DEF stringv sv_from_cstr(const char *s) { return (stringv){.data = s, .length = s ? strlen(s) : 0}; } @@ -330,6 +367,8 @@ NONSTD_DEF int sv_ends_with(stringv sv, stringv suffix) { return sv.length >= suffix.length && memcmp(sv.data + sv.length - suffix.length, suffix.data, suffix.length) == 0; } +// String Builder Implementation + NONSTD_DEF void sb_init(stringb *sb, size_t initial_cap) { sb->capacity = initial_cap ? initial_cap : 16; sb->data = ALLOC(char, sb->capacity); @@ -411,6 +450,8 @@ NONSTD_DEF Arena arena_make(void) { return a; } +// Arena Implementation + NONSTD_DEF void arena_grow(Arena *a, size_t min_size) { size_t size = MAX(ARENA_DEFAULT_BLOCK_SIZE, min_size); char *block = ALLOC(char, size); @@ -452,6 +493,8 @@ NONSTD_DEF void arena_free(Arena *a) { a->end = NULL; } +// File I/O Implementation + NONSTD_DEF char *read_entire_file(const char *filepath, size_t *out_size) { FILE *f = fopen(filepath, "rb"); if (!f) { @@ -571,8 +614,7 @@ NONSTD_DEF void log_message(FILE *stream, LogLevel level, const char *format, .. const char *reset = isatty(fileno(stream)) ? COLOR_RESET : ""; const char *log_format = "%s[%s.%03d] [%-5s] "; - fprintf(stream, log_format, color, time_str, (int)(tv.tv_usec / 1000), - level_strings[level]); + fprintf(stream, log_format, color, time_str, (int)(tv.tv_usec / 1000), level_strings[level]); va_list args; va_start(args, format); @@ -583,4 +625,163 @@ NONSTD_DEF void log_message(FILE *stream, LogLevel level, const char *format, .. fflush(stream); } +// PPM Image Implementation + +NONSTD_DEF Canvas ppm_init(u32 width, u32 height) { + Canvas img = {0}; + img.width = width; + img.height = height; + img.pixels = ALLOC(Color, width * height); + if (img.pixels) { + memset(img.pixels, 0, sizeof(Color) * width * height); + } + return img; +} + +NONSTD_DEF void ppm_free(Canvas *img) { + if (img->pixels) { + FREE(img->pixels); + } + img->width = 0; + img->height = 0; +} + +NONSTD_DEF void ppm_set_pixel(Canvas *img, u32 x, u32 y, Color color) { + if (x < img->width && y < img->height) { + img->pixels[y * img->width + x] = color; + } +} + +NONSTD_DEF Color ppm_get_pixel(const Canvas *img, u32 x, u32 y) { + if (x < img->width && y < img->height) { + return img->pixels[y * img->width + x]; + } + return (Color){0, 0, 0}; +} + +NONSTD_DEF int ppm_save(const Canvas *img, const char *filename) { + FILE *f = fopen(filename, "w"); + if (!f) { + return 0; + } + + fprintf(f, "P3\n%u %u\n255\n", img->width, img->height); + for (u32 y = 0; y < img->height; ++y) { + for (u32 x = 0; x < img->width; ++x) { + Color c = ppm_get_pixel(img, x, y); + fprintf(f, "%d %d %d ", c.r, c.g, c.b); + } + fprintf(f, "\n"); + } + + fclose(f); + return 1; +} + +NONSTD_DEF Canvas ppm_read(const char *filename) { + Canvas img = {0}; + FILE *f = fopen(filename, "r"); + if (!f) { + return img; + } + + char magic[3]; + if (fscanf(f, "%2s", magic) != 1 || strcmp(magic, "P3") != 0) { + fclose(f); + return img; + } + + u32 w, h, max_val; + if (fscanf(f, "%u %u %u", &w, &h, &max_val) != 3) { + fclose(f); + return img; + } + + img = ppm_init(w, h); + if (!img.pixels) { + fclose(f); + return img; + } + + for (u32 i = 0; i < w * h; ++i) { + int r, g, b; + if (fscanf(f, "%d %d %d", &r, &g, &b) != 3) { + ppm_free(&img); + fclose(f); + return (Canvas){0}; + } + img.pixels[i] = (Color){(u8)r, (u8)g, (u8)b}; + } + + fclose(f); + return img; +} + +NONSTD_DEF void ppm_fill(Canvas *canvas, Color color) { + for (u32 i = 0; i < canvas->width * canvas->height; ++i) { + canvas->pixels[i] = color; + } +} + +NONSTD_DEF void ppm_draw_rect(Canvas *canvas, u32 x, u32 y, u32 w, u32 h, Color color) { + if (w == 0 || h == 0) + return; + for (u32 i = x; i < x + w; ++i) { + ppm_set_pixel(canvas, i, y, color); + ppm_set_pixel(canvas, i, y + h - 1, color); + } + for (u32 j = y; j < y + h; ++j) { + ppm_set_pixel(canvas, x, j, color); + ppm_set_pixel(canvas, x + w - 1, j, color); + } +} + +NONSTD_DEF void ppm_draw_line(Canvas *canvas, i32 x0, i32 y0, i32 x1, i32 y1, Color color) { + i32 dx = abs(x1 - x0); + i32 dy = -abs(y1 - y0); + i32 sx = x0 < x1 ? 1 : -1; + i32 sy = y0 < y1 ? 1 : -1; + i32 err = dx + dy; + + while (1) { + ppm_set_pixel(canvas, (u32)x0, (u32)y0, color); + if (x0 == x1 && y0 == y1) { + break; + } + + i32 e2 = 2 * err; + if (e2 >= dy) { + err += dy; + x0 += sx; + } + if (e2 <= dx) { + err += dx; + y0 += sy; + } + } +} + +NONSTD_DEF void ppm_draw_circle(Canvas *canvas, i32 xm, i32 ym, i32 r, Color color) { + i32 x = -r, y = 0, err = 2 - 2 * r; + do { + ppm_set_pixel(canvas, (u32)(xm - x), (u32)(ym + y), color); + ppm_set_pixel(canvas, (u32)(xm - y), (u32)(ym - x), color); + ppm_set_pixel(canvas, (u32)(xm + x), (u32)(ym - y), color); + ppm_set_pixel(canvas, (u32)(xm + y), (u32)(ym + x), color); + r = err; + if (r <= y) { + err += ++y * 2 + 1; + } + if (r > x || err > y) { + err += ++x * 2 + 1; + } + } while (x < 0); +} + +NONSTD_DEF void ppm_draw_triangle(Canvas *canvas, i32 x0, i32 y0, i32 x1, i32 y1, i32 x2, i32 y2, Color color) { + ppm_draw_line(canvas, x0, y0, x1, y1, color); + ppm_draw_line(canvas, x1, y1, x2, y2, color); + ppm_draw_line(canvas, x2, y2, x0, y0, color); +} + #endif // NONSTD_IMPLEMENTATION \ No newline at end of file diff --git a/tests.c b/tests.c index 9eb7469..bca3a89 100644 --- a/tests.c +++ b/tests.c @@ -864,7 +864,7 @@ MU_TEST(test_logging_level_filtering) { set_log_level(LOG_WARN); - log_message(tmp, LOG_INFO, "Info message"); // Should not be logged + log_message(tmp, LOG_INFO, "Info message"); // Should not be logged log_message(tmp, LOG_ERROR, "Error message"); // Should be logged rewind(tmp); @@ -910,11 +910,95 @@ MU_TEST(test_logging_format) { mu_check(strstr(buffer, "Test 123 format") != NULL); mu_check(strstr(buffer, "[INFO ]") != NULL); // Check timestamp format roughly (YYYY-MM-DD) - mu_check(strstr(buffer, "20") != NULL); + mu_check(strstr(buffer, "20") != NULL); fclose(tmp); } +// Image tests +MU_TEST(test_ppm_init_free) { + Canvas img = ppm_init(100, 100); + mu_assert_int_eq(100, (int)img.width); + mu_assert_int_eq(100, (int)img.height); + mu_check(img.pixels != NULL); + ppm_free(&img); + mu_assert_int_eq(0, (int)img.width); + mu_assert_int_eq(0, (int)img.height); + mu_check(img.pixels == NULL); +} + +MU_TEST(test_ppm_set_get_pixel) { + Canvas img = ppm_init(10, 10); + Color c = {255, 128, 64}; + ppm_set_pixel(&img, 5, 5, c); + Color got = ppm_get_pixel(&img, 5, 5); + mu_assert_int_eq(255, got.r); + mu_assert_int_eq(128, got.g); + mu_assert_int_eq(64, got.b); + + // Test out of bounds (should return black) + got = ppm_get_pixel(&img, 100, 100); + mu_assert_int_eq(0, got.r); + + ppm_free(&img); +} + +MU_TEST(test_ppm_save_read) { + Canvas img = ppm_init(10, 10); + for (u32 y = 0; y < 10; ++y) { + for (u32 x = 0; x < 10; ++x) { + ppm_set_pixel(&img, x, y, (Color){(u8)(x * 20), (u8)(y * 20), 100}); + } + } + + const char *tmp_ppm = "test_image.ppm"; + mu_check(ppm_save(&img, tmp_ppm)); + + Canvas read = ppm_read(tmp_ppm); + mu_assert_int_eq((int)img.width, (int)read.width); + mu_assert_int_eq((int)img.height, (int)read.height); + mu_check(read.pixels != NULL); + + for (u32 i = 0; i < 100; ++i) { + mu_assert_int_eq(img.pixels[i].r, read.pixels[i].r); + mu_assert_int_eq(img.pixels[i].g, read.pixels[i].g); + mu_assert_int_eq(img.pixels[i].b, read.pixels[i].b); + } + + ppm_free(&img); + ppm_free(&read); + remove(tmp_ppm); +} + +MU_TEST(test_ppm_draw_helpers) { + Canvas img = ppm_init(100, 100); + + // Test fill + ppm_fill(&img, CLR_RED); + Color c1 = ppm_get_pixel(&img, 0, 0); + mu_assert_int_eq(255, c1.r); + mu_assert_int_eq(0, c1.g); + + // Test rect + ppm_fill(&img, CLR_BLACK); + ppm_draw_rect(&img, 10, 10, 20, 20, CLR_WHITE); + mu_assert_int_eq(255, ppm_get_pixel(&img, 10, 10).r); + mu_assert_int_eq(0, ppm_get_pixel(&img, 15, 15).r); // Inside should be black + + // Test line + ppm_fill(&img, CLR_BLACK); + ppm_draw_line(&img, 0, 0, 10, 10, CLR_GREEN); + mu_assert_int_eq(255, ppm_get_pixel(&img, 5, 5).g); + + // Test hexagon/color macros + Color hex = COLOR_HEX(0x112233); + mu_assert_int_eq(0x11, hex.r); + mu_assert_int_eq(0x22, hex.g); + mu_assert_int_eq(0x33, hex.b); + + ppm_free(&img); +} + // Test suites MU_TEST_SUITE(test_suite_stringv) { printf("\n[String View Tests]\n"); @@ -1025,6 +1109,14 @@ MU_TEST_SUITE(test_suite_logging) { RUN_TEST_WITH_NAME(test_logging_format); } +MU_TEST_SUITE(test_suite_image) { + printf("\n[Image Tests]\n"); + RUN_TEST_WITH_NAME(test_ppm_init_free); + RUN_TEST_WITH_NAME(test_ppm_set_get_pixel); + RUN_TEST_WITH_NAME(test_ppm_save_read); + RUN_TEST_WITH_NAME(test_ppm_draw_helpers); +} + int main(int argc, char *argv[]) { (void)argc; (void)argv; @@ -1038,6 +1130,7 @@ int main(int argc, char *argv[]) { MU_RUN_SUITE(test_suite_arena); MU_RUN_SUITE(test_suite_files); MU_RUN_SUITE(test_suite_logging); + MU_RUN_SUITE(test_suite_image); MU_REPORT(); -- cgit v1.2.3