photomosaics.c (19649B)
1 #include <assert.h> 2 #include <errno.h> 3 #include <getopt.h> 4 #include <limits.h> 5 #include <locale.h> 6 #include <math.h> 7 #include <stdbool.h> 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <string.h> 11 #include <sys/stat.h> 12 #include <time.h> 13 #include <unistd.h> 14 #include <MagickCore/MagickCore.h> 15 16 #define DIE(fmt, ...) { fprintf(stderr, "FATAL: "fmt"\n", __VA_ARGS__); exit(EXIT_FAILURE); } 17 #define WARN(fmt, ...) { fprintf(stderr, "WARN: " fmt"\n", __VA_ARGS__); } 18 #define assert_error(expression, s) if(!expression) { perror(s); abort(); } 19 #define assert_call_succeeds(exit_code, s) if(exit_code != EXIT_SUCCESS) { perror(s); abort(); } 20 21 typedef struct { 22 unsigned int r, g, b; 23 } Pixel; 24 typedef enum { L, UL, XU, XUL, F } NUM_TYPES; 25 26 #define MAX_FN_LEN 150 27 #define IMG_LIST_MAX_SIZE 5091 28 #define DELETABLES_INITIAL_SIZE 50 29 30 static const char *cache_filename = "/home/wilson/.cache/photomosaics/avgs"; 31 static char *cache_buf = NULL; 32 static size_t cache_max_size = 0; 33 static ssize_t initial_cache_size = 1; 34 static ssize_t cache_size = 0; 35 static time_t cache_mtime; 36 static long *deletables; 37 static size_t deletables_ind = 0; 38 static char temp_dirname[] = "/tmp/photomosaics-XXXXXX"; 39 static char **inner_cache_tmp_files; 40 static char **files_inner_cached = NULL; 41 static size_t files_inner_cached_ind = 0; 42 43 static size_t slen(const char *s, size_t maxlen) { 44 char *pos = memchr(s, '\0', maxlen); 45 return pos ? (size_t)(pos - s) : maxlen; 46 } 47 static size_t indof(const char *s, char ch, size_t maxlen) { 48 char *pos = memchr(s, ch, maxlen); 49 return pos ? (size_t)(pos - s) : maxlen; 50 } 51 52 static bool parse_num(const char *str, NUM_TYPES type, void *out) { 53 char *endptr; 54 const char *old_locale = setlocale(LC_ALL, NULL); 55 setlocale(LC_ALL|~LC_NUMERIC, ""); 56 int my_errno = 0; 57 errno = 0; 58 59 switch(type) { 60 case L: 61 *((long *)out) = strtol(str, &endptr, 10); 62 break; 63 case UL: 64 *((unsigned long *)out) = strtoul(str, &endptr, 10); 65 break; 66 case XU: { 67 unsigned long tmp = strtoul(str, &endptr, 16); 68 if(tmp > UINT_MAX) my_errno = ERANGE; 69 else *((unsigned int *)out) = tmp; 70 } 71 break; 72 case XUL: 73 *((unsigned long *)out) = strtoul(str, &endptr, 16); 74 break; 75 case F: 76 *((float *)out) = strtof(str, &endptr); 77 break; 78 } 79 80 int their_errno = errno; 81 setlocale(LC_ALL, old_locale); 82 if((errno=their_errno)) return false; 83 if(my_errno) { 84 errno = my_errno; 85 return false; 86 } 87 /*N.B. fails on "partial" conversions or if str is empty*/ 88 return *str != '\0' && *endptr == '\0'; 89 } 90 static bool parse_hex_tou(char *str, unsigned int *out) { 91 return parse_num(str, XU, out); 92 } 93 static bool parse_ulong(char *str, unsigned long *out) { 94 return parse_num(str, UL, out); 95 } 96 97 static Pixel hexstr_top(const char *hs) { 98 char rstr[3] = {hs[0], hs[1],}; 99 char gstr[3] = {hs[2], hs[3],}; 100 char bstr[3] = {hs[4], hs[5],}; 101 Pixel p; 102 parse_hex_tou(rstr, &p.r); 103 parse_hex_tou(gstr, &p.g); 104 parse_hex_tou(bstr, &p.b); 105 return p; 106 } 107 108 static bool cache_init() { 109 errno = 0; 110 FILE *cache_file = fopen(cache_filename, "r"); 111 struct stat cache_st; 112 if(!cache_file) { 113 WARN("Couldn't open cache file '%s'. " 114 "Please ensure the directory exists.", cache_filename); 115 perror("fopen"); 116 } 117 else { 118 if(stat(cache_filename, &cache_st) == 0) 119 errno = 0; 120 else if(errno == ENOENT) { 121 /* create the file and try again just in case that's the only error */ 122 errno = 0; 123 FILE *tmp_cache_file = fopen(cache_filename, "a"); 124 if(!tmp_cache_file) { 125 WARN("Couldn't open cache file '%s'. " 126 "Please ensure the directory exists.", cache_filename); 127 perror("fopen"); 128 } 129 else { 130 assert_call_succeeds(fclose(tmp_cache_file), "fclose"); 131 if(stat(cache_filename, &cache_st) != 0) { 132 WARN("Could not stat cache file '%s'", cache_filename); 133 perror("stat"); 134 } 135 } 136 } 137 } 138 139 if(errno) { 140 WARN("Will stop attempting to cache to '%s' for the remainder of execution.", cache_filename); 141 cache_size = -1; 142 return false; 143 } 144 145 /* No errors, proceed to populate cache_buf */ 146 147 cache_mtime = cache_st.st_mtime; 148 long cache_file_size = cache_st.st_size; 149 /* The following 2 mallocs are guesses; will realloc later if needed */ 150 cache_max_size = (cache_file_size < 5822 ? 5822 : cache_file_size) + 5 * MAX_FN_LEN; 151 cache_buf = malloc(cache_max_size); 152 deletables = malloc(DELETABLES_INITIAL_SIZE * sizeof(long)); 153 initial_cache_size = cache_size = fread(cache_buf, 1, cache_file_size, cache_file); 154 cache_buf[cache_size] = 0; /* For the initial strncat later */ 155 156 assert(cache_size == cache_file_size); 157 assert_call_succeeds(fclose(cache_file), "fclose"); 158 return true; 159 } 160 static ssize_t cache_grep(char *key) { 161 if(cache_size == -1) 162 return -1; 163 if(!cache_buf) { 164 if(!cache_init()) return -1; 165 } 166 if(cache_size == 0) return -1; 167 168 char filename[MAX_FN_LEN]; 169 struct stat file_st; 170 171 for(ssize_t i=0; i < cache_size; i += indof(cache_buf + i, '\n', cache_size - i) + 1) { 172 /* If we already marked it for deletion, we want the image's cache entry which 173 we put at the bottom of the buffer, in case the avg color has changed. */ 174 bool skip = false; 175 for(size_t j=0; j < deletables_ind; j++) { 176 if(deletables[j] == i) { 177 skip = true; 178 break; 179 } 180 } 181 if(skip) continue; 182 size_t fn_len = 0; 183 size_t fn_begin = i; 184 for(; i < cache_size; i++) { 185 if((filename[i-fn_begin]=cache_buf[i]) == '\t') { 186 fn_len = i - fn_begin; 187 filename[fn_len] = '\0'; 188 i++; 189 break; 190 } 191 } 192 assert(fn_len); 193 if(!strncmp(filename, key, fn_len)) { 194 //Already exists in cache 195 assert_call_succeeds(stat(filename, &file_st), "stat"); 196 //The sole use of `initial_...`. Prevents the caller from re- and recaching newly-added files 197 if(i > initial_cache_size - 1 || file_st.st_mtime < cache_mtime) { 198 /* Cache entry is up to date */ 199 return i; 200 } 201 /* Not up to date. Caller will create a new cache entry, 202 then we will delete this line at the end of the program */ 203 if(deletables_ind >= DELETABLES_INITIAL_SIZE) { 204 deletables = realloc(deletables, (deletables_ind + 1) * sizeof(deletables[0])); 205 assert_error(deletables, "realloc"); 206 } 207 deletables[deletables_ind++] = fn_begin; 208 return -1; 209 } 210 } 211 return -1; 212 } 213 static bool cache_fetch(char *key, Pixel *value) { 214 ssize_t i = cache_grep(key); 215 if(i == -1) return false; 216 char hexstr[7]; 217 assert(indof(cache_buf + i, '\n', cache_size - i) == 6); 218 hexstr[0] = 0; 219 strncat(hexstr, cache_buf + i, 6); 220 *value = hexstr_top(hexstr); 221 return true; 222 } 223 static bool cache_put(char *key, Pixel value) { 224 if(!cache_buf) return false; 225 char entry[MAX_FN_LEN + 9]; 226 int entry_length = sprintf(entry, "%s\t%02x%02x%02x\n", key, value.r, value.g, value.b); 227 size_t new_size_of_cache = cache_size + entry_length + 1; 228 if(new_size_of_cache > cache_max_size) { 229 cache_buf = realloc(cache_buf, new_size_of_cache); 230 assert_error(cache_buf, "realloc"); 231 cache_max_size = new_size_of_cache; 232 } 233 strncat(cache_buf, entry, entry_length); 234 cache_size = new_size_of_cache - 1; 235 return true; 236 } 237 238 239 static Pixel get_avg_color(unsigned char *pixels, const size_t pixels_column_cnt, const ssize_t x, const ssize_t y, const size_t width, const size_t height) { 240 Pixel p = {0}; 241 int i = y * pixels_column_cnt + x * 3; 242 for(unsigned long c=0; c < width*height;) { 243 p.r += pixels[i++]; 244 p.g += pixels[i++]; 245 p.b += pixels[i++]; 246 if(++c % width == 0) 247 i += (pixels_column_cnt - width) * 3; //next row ... 248 } 249 250 p.r /= width*height; 251 p.g /= width*height; 252 p.b /= width*height; 253 return p; 254 } 255 256 static bool get_resized_pixel_info(const char *filename, const size_t width, const size_t height, unsigned char *pixels_out, ExceptionInfo *exception) { 257 if(!files_inner_cached) { 258 inner_cache_tmp_files = malloc(IMG_LIST_MAX_SIZE * sizeof(char*)); 259 files_inner_cached = malloc(IMG_LIST_MAX_SIZE * sizeof(char*)); 260 } 261 const size_t pixels_arr_size = width * height * 3; 262 bool file_is_cached = false; 263 size_t i; 264 for(i=0; i < files_inner_cached_ind; i++) { 265 if(!strcmp(files_inner_cached[i], filename)) { 266 file_is_cached = true; 267 break; 268 } 269 } 270 271 if(file_is_cached) { 272 FILE *inner_cache = fopen(inner_cache_tmp_files[i], "rb"); 273 assert_error(inner_cache, "fopen"); 274 size_t z = fread(pixels_out, 1, pixels_arr_size, inner_cache); 275 fclose(inner_cache); 276 return z == pixels_arr_size; 277 } 278 else { 279 if(files_inner_cached_ind == 0) 280 mkdtemp(temp_dirname); 281 const size_t filename_len = strlen(filename); 282 const size_t dirname_len = strlen(temp_dirname); 283 char *temp_name = malloc(filename_len); 284 char *temp_path = malloc(filename_len + dirname_len + 2); 285 temp_name[0] = 0; 286 strncat(temp_name, filename, filename_len); 287 /* TODO gracefully handle slashes (?) and percents in the filenames themselves */ 288 for(size_t c=0; c < filename_len; ++c) if(temp_name[c] == '/') temp_name[c] = '%'; 289 temp_path[0] = 0; 290 strncat(temp_path, temp_dirname, dirname_len); 291 temp_path[dirname_len] = '/'; 292 temp_path[dirname_len+1] = 0; 293 strncat(temp_path, temp_name, filename_len); 294 free(temp_name); 295 296 FILE *inner_cache = fopen(temp_path, "wb"); 297 assert_error(inner_cache, "fopen"); 298 ImageInfo *image_info = CloneImageInfo((ImageInfo *)NULL); 299 image_info->filename[0] = 0; 300 strncat(image_info->filename, filename, filename_len); 301 Image *src_img = ReadImage(image_info, exception); 302 Image *src_img_r = ResizeImage(src_img, width, height, LanczosFilter, exception); 303 304 if(!src_img_r) MagickError(exception->severity, exception->reason, exception->description); 305 ExportImagePixels(src_img_r, 0, 0, width, height, "RGB", CharPixel, pixels_out, exception); 306 if(exception->severity != UndefinedException) CatchException(exception); 307 DestroyImage(src_img); 308 DestroyImage(src_img_r); 309 DestroyImageInfo(image_info); 310 assert(fwrite(pixels_out, 3, pixels_arr_size / 3, inner_cache) == pixels_arr_size / 3); 311 fclose(inner_cache); 312 inner_cache_tmp_files[files_inner_cached_ind] = malloc(strlen(temp_path) + 1); 313 strcpy(inner_cache_tmp_files[files_inner_cached_ind], temp_path); 314 files_inner_cached[files_inner_cached_ind] = malloc(strlen(filename) + 1); 315 files_inner_cached[files_inner_cached_ind][0] = 0; 316 strncat(files_inner_cached[files_inner_cached_ind++], filename, filename_len); 317 free(temp_path); 318 return true; 319 } 320 } 321 322 static unsigned char *get_img_with_closest_avg(char *img_list, size_t img_list_size, Pixel p, const size_t width, const size_t height, ExceptionInfo *exception) { 323 const size_t pixels_arr_size = width * height * 3; 324 unsigned char *pixels_of_closest = malloc(pixels_arr_size); 325 unsigned char *pixels = malloc(pixels_arr_size); 326 float distance_of_closest = sqrtf(powf(0xff, 2) * 3); //max diff value 327 bool test_pxofcls_populated = false; 328 329 for(size_t c=0; c < img_list_size;) { 330 Pixel avg; 331 bool fetched_avg_from_cache = cache_fetch(&img_list[c], &avg); 332 if(!fetched_avg_from_cache) { 333 assert(get_resized_pixel_info(&img_list[c], width, height, pixels, exception)); 334 avg = get_avg_color(pixels, width, 0, 0, width, height); 335 assert(cache_put(&img_list[c], avg)); 336 } 337 long rdiff = (long)avg.r - p.r; 338 long gdiff = (long)avg.g - p.g; 339 long bdiff = (long)avg.b - p.b; 340 float new_distance = sqrtf(powf(rdiff, 2) + powf(gdiff, 2) + powf(bdiff, 2)); 341 if(new_distance < distance_of_closest) { 342 distance_of_closest = new_distance; 343 if(fetched_avg_from_cache) 344 assert(get_resized_pixel_info(&img_list[c], width, height, pixels, exception)); 345 // For now, return any perfect match 346 if(new_distance < FLT_EPSILON) { 347 free(pixels_of_closest); 348 return pixels; 349 } 350 memcpy(pixels_of_closest, pixels, pixels_arr_size); 351 test_pxofcls_populated = true; 352 } 353 c += slen(&img_list[c], img_list_size - c) + 1; 354 } 355 assert(test_pxofcls_populated); 356 free(pixels); 357 return pixels_of_closest; 358 } 359 360 static Image *photomosaic(Image *image, const size_t each_width, const size_t each_height, ExceptionInfo *exception) { 361 const size_t pixel_cnt = image->columns * image->rows; 362 unsigned char *pixels = malloc(pixel_cnt * 3); 363 FILE *f = popen("find $(find ~/pics -type d | grep -vE 'redacted|not_real') -maxdepth 1 -type f -print0", "r"); 364 char buf[IMG_LIST_MAX_SIZE]; 365 size_t bytes_read = fread(buf, 1, IMG_LIST_MAX_SIZE, f); 366 assert_call_succeeds(pclose(f), "pclose"); 367 368 assert(ExportImagePixels(image, 0, 0, image->columns, image->rows, "RGB", CharPixel, pixels, exception)); 369 370 for(size_t i=0, j=0; i < pixel_cnt;) { 371 /*Specifying 0 for y allows us to automatically use i to "roll over" into next row*/ 372 Pixel p = get_avg_color(pixels, image->columns, i, 0, each_width, each_height); 373 unsigned char *new_pixels = get_img_with_closest_avg(buf, bytes_read, p, each_width, each_height, exception); 374 for(size_t c=0; c < each_width*each_height;) { 375 pixels[j] = new_pixels[c*3]; 376 pixels[j+1] = new_pixels[c*3+1]; 377 pixels[j+2] = new_pixels[c*3+2]; 378 j += 3; 379 if(++c % each_width == 0) 380 j += (image->columns - each_width) * 3; //next row ... 381 } 382 i += each_width; //next splotch 383 /*If this row is done, skip over all the rows we just splotched*/ 384 if(i % image->columns == 0) 385 i += image->columns * (each_height - 1); 386 j = i * 3; 387 free(new_pixels); 388 } 389 Image *new_image = ConstituteImage(image->columns, image->rows, "RGB", CharPixel, pixels, exception); 390 free(pixels); 391 if(!new_image) 392 MagickError(exception->severity, exception->reason, exception->description); 393 return new_image; 394 } 395 396 void usage(char *progname) { 397 fprintf(stderr, 398 "Usage: %s (-h | (-i <input_file> -o <output_file> -w <width> -l <length>))\n" 399 "\t-h\tPrint this help message and exit.\n" 400 "\tThis program creates a photomosaic by replacing each block\n\t" 401 "of 'input_file' of size 'width' x 'length' by the resized\n\t" 402 "version of some image with a similar average color.\n\t" 403 "Writes the new image to the filename specified by 'output_file'.\n" 404 "\nExit status:\n" 405 "\t0\tSpecified operation succeeded\n" 406 "\t1\tError reading or performing some operation on an image\n" 407 "\t2\tError parsing command line arguments\n" 408 , progname); 409 } 410 411 int main(int argc, char **argv) { 412 ExceptionInfo *exception; 413 Image *input_img, *output_img = NULL; 414 const size_t max_fn_len = 400; 415 char input_img_filename[max_fn_len]; 416 input_img_filename[0] = 0; 417 char output_img_filename[max_fn_len]; 418 output_img_filename[0] = 0; 419 ImageInfo *image_info, *new_image_info = NULL; 420 size_t length = 1, width = 1; 421 422 int opt; 423 while((opt=getopt(argc, argv, "hi:o:l:w:")) > -1) { 424 switch(opt) { 425 case 'h': 426 usage(argv[0]); 427 return 0; 428 case 'i': 429 if(slen(optarg, max_fn_len) == max_fn_len) DIE("Argument \"%s\" to option -i should be less than %zu characters.", optarg, max_fn_len) 430 strncat(input_img_filename, optarg, max_fn_len - 1); 431 break; 432 case 'l': 433 if(!parse_ulong(optarg, &length)) 434 DIE("Argument \"%s\" to option -l could not be parsed to an unsigned long int.", optarg); 435 break; 436 case 'o': 437 if(slen(optarg, max_fn_len) == max_fn_len) DIE("Argument \"%s\" to option -o should be less than %zu characters.", optarg, max_fn_len) 438 strncat(output_img_filename, optarg, max_fn_len - 1); 439 break; 440 case 'w': 441 if(!parse_ulong(optarg, &width)) 442 DIE("Argument \"%s\" to option -w could not be parsed to an unsigned long int.", optarg); 443 break; 444 } 445 } 446 447 if(slen(input_img_filename, max_fn_len) < 1) DIE("No input image specified.%s", ""); 448 if(slen(output_img_filename, max_fn_len) < 1) DIE("No output image specified.%s", ""); 449 450 MagickCoreGenesis(*argv, MagickTrue); 451 exception = AcquireExceptionInfo(); 452 453 image_info = CloneImageInfo((ImageInfo *)NULL); 454 strcpy(image_info->filename, input_img_filename); 455 input_img = ReadImage(image_info, exception); 456 if(exception->severity != UndefinedException) 457 CatchException(exception); 458 if(!input_img) 459 DIE("Input image %s could not be read.", input_img_filename); 460 461 output_img = photomosaic(input_img, width, length, exception); 462 463 if(exception->severity != UndefinedException) 464 CatchException(exception); 465 466 467 /* Teardown */ 468 if(files_inner_cached) { 469 for(size_t i=0; i < files_inner_cached_ind; i++) { 470 assert_call_succeeds(remove(inner_cache_tmp_files[i]), "remove"); 471 free(inner_cache_tmp_files[i]); 472 free(files_inner_cached[i]); 473 } 474 assert_call_succeeds(remove(temp_dirname), "remove"); 475 } 476 if(cache_buf) { 477 FILE *cache = fopen(cache_filename, "w"); 478 if(!cache) { 479 WARN("Failed to reopen the cache file '%s' for writing " 480 "in order to update the cache properly:", cache_filename); 481 perror("fopen"); 482 WARN("The cache at '%s' may now contain duplicate entries.", cache_filename); 483 } 484 else for(ssize_t i=0; i < cache_size;) { 485 bool keep = true; 486 for(size_t j=0; j < deletables_ind; j++) { 487 if(deletables[j] == i) { 488 keep = false; 489 break; 490 } 491 } 492 size_t line_len = indof(cache_buf + i, '\n', cache_size - i); 493 if(keep) for(size_t j=0; j <= line_len; j++) assert(fputc(cache_buf[i+j], cache) != EOF); 494 i += line_len + 1; 495 } 496 if(cache) fclose(cache); 497 free(deletables); 498 free(cache_buf); 499 } 500 501 if(output_img) { 502 new_image_info = CloneImageInfo((ImageInfo *)NULL); 503 strcpy(output_img->filename, output_img_filename); 504 WriteImage(new_image_info, output_img, exception); 505 DestroyImage(output_img); 506 DestroyImageInfo(new_image_info); 507 } 508 DestroyImage(input_img); 509 DestroyImageInfo(image_info); 510 DestroyExceptionInfo(exception); 511 MagickCoreTerminus(); 512 return EXIT_SUCCESS; 513 }