/* ascii.c Converts the image to ASCII art. Tux Paint - A simple drawing program for children. Copyright (c) 2024 by Bill Kendrick This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA (See COPYING.txt) Last updated: September 27, 2024 */ //#define DEBUG #if defined(DEBUG) #define DEBUG_PRINTF(...) printf(__VA_ARGS__) #else #define DEBUG_PRINTF(...) #endif #include #include #include "tp_magic_api.h" #include "SDL_image.h" #include "SDL_mixer.h" enum { TOOL_TYPEWRITER, TOOL_COMPUTER, NUM_TOOLS }; #define TOOL_COMPUTER_COLOR NUM_TOOLS char * ascii_tool_names[NUM_TOOLS + 1] = { gettext_noop("Typewriter"), gettext_noop("Computer"), gettext_noop("Color Computer"), // special version of "computer" }; char * ascii_tool_filenames[NUM_TOOLS + 1] = { "typewriter", "computer", "color_computer", }; static Mix_Chunk *ascii_snd[NUM_TOOLS]; /* For each variation, we'll have a bitmap with an arbitrary number * of potentially-proportionally-spaced characters (which we'll treat * as fixed-width), so we need to keep track of each character's X * poistion, how many characters there are, and the maximum width. */ #define MAX_CHARS 256 SDL_Surface * ascii_bitmap[NUM_TOOLS]; int ascii_char_x[NUM_TOOLS][MAX_CHARS]; int ascii_num_chars[NUM_TOOLS]; int ascii_char_maxwidth[NUM_TOOLS]; int ascii_char_brightness[NUM_TOOLS][MAX_CHARS]; SDL_Surface * ascii_snapshot = NULL; int ascii_size; Uint8 ascii_r, ascii_g, ascii_b; Uint8 ascii_clear_r[NUM_TOOLS], ascii_clear_g[NUM_TOOLS], ascii_clear_b[NUM_TOOLS]; /* Based on CGA color palette */ const Uint8 ascii_computer_colors[16][3] = { { 0x00, 0x00, 0x00 }, // Black { 0x55, 0x55, 0x55 }, // Dark gray { 0xAA, 0xAA, 0xAA }, // Light gray { 0xFF, 0xFF, 0xFF }, // White { 0x00, 0x00, 0xAA }, // Blue { 0x55, 0x55, 0xFF }, // Light blue { 0x00, 0xAA, 0x00 }, // Green { 0x55, 0xFF, 0x55 }, // Light green { 0x00, 0xAA, 0xAA }, // Cyan { 0x55, 0xFF, 0xFF }, // Light cyan { 0xAA, 0x00, 0x00 }, // Red { 0xFF, 0x55, 0x55 }, // Light red { 0xAA, 0x00, 0xAA }, // Magenta { 0xFF, 0x55, 0xFF }, // Light magenta { 0xAA, 0x55, 0x00 }, // Brown { 0xFF, 0xFF, 0x55 }, // Yellow }; Uint32 ascii_api_version(void); int ascii_init(magic_api * api, Uint8 disabled_features, Uint8 complexity_level); int ascii_get_tool_count(magic_api * api); SDL_Surface *ascii_get_icon(magic_api * api, int which); char *ascii_get_name(magic_api * api, int which); int ascii_get_group(magic_api * api, int which); int ascii_get_order(int which); char *ascii_get_description(magic_api * api, int which, int mode); void ascii_drag(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * last, int ox, int oy, int x, int y, SDL_Rect * update_rect); void ascii_click(magic_api * api, int which, int mode, SDL_Surface * canvas, SDL_Surface * last, int x, int y, SDL_Rect * update_rect); void ascii_release(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * last, int x, int y, SDL_Rect * update_rect); void ascii_shutdown(magic_api * api); void ascii_set_color(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * last, Uint8 r, Uint8 g, Uint8 b, SDL_Rect * update_rect); int ascii_requires_colors(magic_api * api, int which); void ascii_switchin(magic_api * api, int which, int mode, SDL_Surface * canvas); void ascii_switchout(magic_api * api, int which, int mode, SDL_Surface * canvas); int ascii_modes(magic_api * api, int which); Uint8 ascii_accepted_sizes(magic_api * api, int which, int mode); Uint8 ascii_default_size(magic_api * api, int which, int mode); void ascii_set_size(magic_api * api, int which, int mode, SDL_Surface * canvas, SDL_Surface * last, Uint8 size, SDL_Rect * update_rect); void do_ascii_effect(void *ptr, int which, SDL_Surface * canvas, SDL_Surface * last, int x, int y); int get_best_char(int which, int brightness); int get_bright(magic_api * api, int r, int g, int b); Uint32 ascii_api_version(void) { return (TP_MAGIC_API_VERSION); } int ascii_init(magic_api * api, Uint8 disabled_features ATTRIBUTE_UNUSED, Uint8 complexity_level ATTRIBUTE_UNUSED) { char fname[1024]; int i, j, x, y, xx, w, num_chars, all_clear, area, bright, clear_brightness; int min_bright, max_bright; Uint32 clear_pixel, pixel; Uint8 r, g, b; Uint8 clear_r, clear_g, clear_b; for (i = 0; i < NUM_TOOLS; i++) { ascii_snd[i] = NULL; ascii_bitmap[i] = NULL; } /* (N.B. Computer & Color Computer share sound & bitmap) */ for (i = 0; i < NUM_TOOLS; i++) { /* Load our sound */ snprintf(fname, sizeof(fname), "%ssounds/magic/ascii-%s.ogg", api->data_directory, ascii_tool_filenames[i]); ascii_snd[i] = Mix_LoadWAV(fname); /* Load and process our bitmap "font" */ snprintf(fname, sizeof(fname), "%simages/magic/ascii-%s.png", api->data_directory, ascii_tool_filenames[i]); ascii_bitmap[i] = IMG_Load(fname); if (ascii_bitmap[i] == NULL) { fprintf(stderr, "Cannot load %s\n", fname); return(0); } clear_pixel = api->getpixel(ascii_bitmap[i], 0, 0); SDL_GetRGB(clear_pixel, ascii_bitmap[i]->format, &clear_r, &clear_g, &clear_b); DEBUG_PRINTF("%s; clear pixel %d (%d,%d,%d)\n", fname, clear_pixel, clear_r, clear_g, clear_b); clear_brightness = (clear_r + clear_g + clear_b) / 3; ascii_clear_r[i] = clear_r; ascii_clear_g[i] = clear_g; ascii_clear_b[i] = clear_b; num_chars = 0; for (x = 0; x < ascii_bitmap[i]->w; x++) { /* Skip whitespace between characters */ do { all_clear = 1; for (y = 0; y < ascii_bitmap[i]->h /* && all_clear */; y++) { pixel = api->getpixel(ascii_bitmap[i], x, y); if (pixel != clear_pixel) all_clear = 0; } if (all_clear) x++; } while (all_clear && x < ascii_bitmap[i]->w); ascii_char_x[i][num_chars] = x; /* Capture the extent of the character */ all_clear = 0; for (xx = x; xx < ascii_bitmap[i]->w && !all_clear; xx++) { all_clear = 1; for (y = 0; y < ascii_bitmap[i]->h /* && all_clear */; y++) { pixel = api->getpixel(ascii_bitmap[i], xx, y); if (pixel != clear_pixel) { all_clear = 0; SDL_GetRGB(pixel, ascii_bitmap[i]->format, &r, &g, &b); if (r == 255 && g == 0 && b == 255) { /* Magenta counts as a connecting pixel, but we * want it to appear as the clear color */ api->putpixel(ascii_bitmap[i], xx, y, clear_pixel); DEBUG_PRINTF("x"); } else { DEBUG_PRINTF("#"); } } else { DEBUG_PRINTF("-"); } } DEBUG_PRINTF("\n"); } x = xx - 1; num_chars++; DEBUG_PRINTF(".......................................\n"); } ascii_num_chars[i] = num_chars; DEBUG_PRINTF("%s has %d characters\n", fname, num_chars); /* Determine the max. width of any character */ ascii_char_x[i][num_chars] = x; ascii_char_maxwidth[i] = 0; for (j = 0; j < num_chars; j++) { w = ascii_char_x[i][j + 1] - ascii_char_x[i][j]; DEBUG_PRINTF("%d->%d = %d\n", j, j + 1, w); if (w > ascii_char_maxwidth[i]) { ascii_char_maxwidth[i] = w; } } /* Calculate the intensity of each character */ area = ascii_char_maxwidth[i] * ascii_bitmap[i]->h; DEBUG_PRINTF("%s max char width is %d -- * %d = area %d\n", fname, ascii_char_maxwidth[i], ascii_bitmap[i]->h, area); for (j = 0; j < num_chars; j++) { bright = 0; for (y = 0; y < ascii_bitmap[i]->h; y++) { for (x = ascii_char_x[i][j]; x < ascii_char_x[i][j + 1]; x++) { pixel = api->getpixel(ascii_bitmap[i], x, y); SDL_GetRGB(pixel, ascii_bitmap[i]->format, &r, &g, &b); DEBUG_PRINTF("%3d (%3d) ", (r + g + b) / 3, get_bright(api, r, g, b)); bright += get_bright(api, r, g, b); } DEBUG_PRINTF("\n"); } DEBUG_PRINTF("char %3d brightness = %3d before padding -- ", j, bright / area); w = ascii_char_maxwidth[i] - (ascii_char_x[i][j + 1] - ascii_char_x[i][j]) - 2; /* don't let padding affect _too_ much */ if (w >= 1) bright += (clear_brightness * ascii_bitmap[i]->h * w); DEBUG_PRINTF("%3d after padding %d width\n", bright / area, w); ascii_char_brightness[i][j] = bright / area; } /* Stretch the brightnesses, so we cover more of 0->255 */ min_bright = 255; max_bright = 0; for (j = 0; j < num_chars; j++) { if (ascii_char_brightness[i][j] > max_bright) max_bright = ascii_char_brightness[i][j]; if (ascii_char_brightness[i][j] < max_bright) min_bright = ascii_char_brightness[i][j]; } DEBUG_PRINTF("brightnesses between %d and %d\n", min_bright, max_bright); /* https://rosettacode.org/wiki/Map_range#C */ #define map_range(a1,a2,b1,b2,s) (b1 + (s-a1)*(b2-b1)/(a2-a1)) for (j = 0; j < num_chars; j++) { DEBUG_PRINTF("mapping %3d -> ", ascii_char_brightness[i][j]); ascii_char_brightness[i][j] = map_range(min_bright, max_bright, 0, 255, ascii_char_brightness[i][j]); DEBUG_PRINTF("%3d\n", ascii_char_brightness[i][j]); } } return (1); } int ascii_get_tool_count(magic_api * api ATTRIBUTE_UNUSED) { return (NUM_TOOLS + 1); } SDL_Surface *ascii_get_icon(magic_api * api, int which) { char fname[1024]; snprintf(fname, sizeof(fname), "%simages/magic/ascii_%s.png", api->data_directory, ascii_tool_filenames[which]); return (IMG_Load(fname)); } char *ascii_get_name(magic_api * api ATTRIBUTE_UNUSED, int which) { char tmp[1024]; snprintf(tmp, sizeof(tmp), gettext("ASCII %s"), gettext(ascii_tool_names[which])); return(strdup(tmp)); } int ascii_get_group(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED) { return MAGIC_TYPE_DISTORTS; } int ascii_get_order(int which) { return 2050 + which; } char *ascii_get_description(magic_api * api ATTRIBUTE_UNUSED, int which, int mode) { char tmp[1024]; if (mode == MODE_PAINT) snprintf(tmp, sizeof(tmp), gettext("Click and drag to transform parts of your drawing to ASCII art (%s style)."), ascii_tool_names[which]); else snprintf(tmp, sizeof(tmp), gettext("Click to transform your entire drawing to ASCII art (%s style)."), ascii_tool_names[which]); return(strdup(tmp)); } void ascii_drag(magic_api * api, int which, SDL_Surface * canvas, SDL_Surface * last, int ox, int oy, int x, int y, SDL_Rect * update_rect) { api->line((void *)api, which, canvas, last, ox, oy, x, y, 1, do_ascii_effect); /* FIXME */ update_rect->x = 0; update_rect->y = 0; update_rect->w = canvas->w; update_rect->h = canvas->h; if (which == TOOL_COMPUTER_COLOR) which = TOOL_COMPUTER; api->playsound(ascii_snd[which], (x * 255) / canvas->w, 255); } void ascii_click(magic_api * api, int which, int mode, SDL_Surface * canvas, SDL_Surface * last, int x, int y, SDL_Rect * update_rect) { if (mode == MODE_PAINT) ascii_drag(api, which, canvas, last, x, y, x, y, update_rect); else { int xx, yy; if (which == TOOL_COMPUTER_COLOR) api->playsound(ascii_snd[TOOL_COMPUTER], (x * 255) / canvas->w, 255); else api->playsound(ascii_snd[which], (x * 255) / canvas->w, 255); for (yy = 0; yy < canvas->h; yy++) { for (xx = 0; xx < canvas->w; xx++) { do_ascii_effect(api, which, canvas, last, xx, yy); } if (yy % 10 == 0) api->update_progress_bar(); } update_rect->x = 0; update_rect->y = 0; update_rect->w = canvas->w; update_rect->h = canvas->h; } } void ascii_release(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, SDL_Surface * canvas ATTRIBUTE_UNUSED, SDL_Surface * last ATTRIBUTE_UNUSED, int x ATTRIBUTE_UNUSED, int y ATTRIBUTE_UNUSED, SDL_Rect * update_rect ATTRIBUTE_UNUSED) { api->stopsound(); } void ascii_shutdown(magic_api * api ATTRIBUTE_UNUSED) { int i; for (i = 0; i < NUM_TOOLS; i++) { if (ascii_snd[i] != NULL) Mix_FreeChunk(ascii_snd[i]); if (ascii_bitmap[i] != NULL) SDL_FreeSurface(ascii_bitmap[i]); } if (ascii_snapshot != NULL) { SDL_FreeSurface(ascii_snapshot); ascii_snapshot = NULL; } } void ascii_set_color(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, SDL_Surface * canvas ATTRIBUTE_UNUSED, SDL_Surface * last ATTRIBUTE_UNUSED, Uint8 r, Uint8 g, Uint8 b, SDL_Rect * update_rect ATTRIBUTE_UNUSED) { /* If the bitmap's "clear" color, choose the opposite! */ if (abs(r - ascii_clear_r[which]) < 8 && abs(g - ascii_clear_g[which]) < 8 && abs(b - ascii_clear_b[which]) < 8) { r = 255 - r; g = 255 - g; b = 255 - b; } ascii_r = r; ascii_g = g; ascii_b = b; } int ascii_requires_colors(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED) { if (which == TOOL_TYPEWRITER || which == TOOL_COMPUTER) return 1; return 0; } void ascii_switchin(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED, SDL_Surface * canvas ATTRIBUTE_UNUSED) { if (ascii_snapshot == NULL) ascii_snapshot = SDL_CreateRGBSurface(SDL_SWSURFACE, canvas->w, canvas->h, canvas->format->BitsPerPixel, canvas->format->Rmask, canvas->format->Gmask, canvas->format->Bmask, canvas->format->Amask); if (ascii_snapshot != NULL) { /* FIXME: When switching from PAINT to FULLSCREEN mode, * we switch out & back in, which means we take a fresh * snapshot even though we didn't leave the overall tool, * which is less than ideal. -bjk 2024.09.27 */ SDL_BlitSurface(canvas, NULL, ascii_snapshot, NULL); } } void ascii_switchout(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED, SDL_Surface * canvas ATTRIBUTE_UNUSED) { } int ascii_modes(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED) { return (MODE_PAINT | MODE_FULLSCREEN); } Uint8 ascii_accepted_sizes(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED) { if (mode == MODE_PAINT) return 6; else return 0; } Uint8 ascii_default_size(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode) { if (mode == MODE_PAINT) return 3; else return 0; } void ascii_set_size(magic_api * api ATTRIBUTE_UNUSED, int which ATTRIBUTE_UNUSED, int mode ATTRIBUTE_UNUSED, SDL_Surface * canvas ATTRIBUTE_UNUSED, SDL_Surface * last ATTRIBUTE_UNUSED, Uint8 size, SDL_Rect * update_rect ATTRIBUTE_UNUSED) { ascii_size = size; } void do_ascii_effect(void *ptr, int which, SDL_Surface * canvas, SDL_Surface * last ATTRIBUTE_UNUSED, int x, int y) { magic_api *api = (magic_api *) ptr; int w, h, n, sx, sy, xx, yy, ww, brightness, rr, gg, bb; Uint8 r, g, b; Uint32 clear_pixel; Uint8 clear_brightness; SDL_Rect src, dest; int computer_color = 0; if (which == TOOL_COMPUTER_COLOR) { which = TOOL_COMPUTER; computer_color = 1; } w = ascii_char_maxwidth[which]; h = ascii_bitmap[which]->h; x = (x / w) * w; y = (y / h) * h; for (sy = y - (h * (ascii_size - 1)); sy <= y + (h * (ascii_size - 1)); sy += h) { for (sx = x - (w * (ascii_size - 1)); sx <= x + (w * (ascii_size - 1)); sx += w) { if (!api->touched(sx, sy)) { clear_pixel = api->getpixel(ascii_bitmap[which], 0, 0); SDL_GetRGB(clear_pixel, ascii_bitmap[which]->format, &r, &g, &b); clear_brightness = ((r + g + b) / 3.0); brightness = 0; rr = 0; gg = 0; bb = 0; for (yy = sy; yy < sy + h; yy++) { for (xx = sx; xx < sx + w; xx++) { SDL_GetRGB(api->getpixel(ascii_snapshot, xx, yy), ascii_snapshot->format, &r, &g, &b); brightness += get_bright(api, r, g, b); if (computer_color) { rr += r; gg += g; bb += b; } } } brightness = brightness / (w * h); /* Background (and also, all we draw, if "Space") */ dest.x = sx; dest.y = sy; dest.w = w; dest.h = h; SDL_FillRect(canvas, &dest, clear_pixel); if (brightness != clear_brightness) { /* A visible character */ if (computer_color) { int i, best; /* Find the best color, based on the avg. of the pixels we're replacing */ rr /= (w * h); gg /= (w * h); bb /= (w * h); DEBUG_PRINTF("avg is %02x%02x%02x; ", rr, gg, bb); /* Map each RGB component to _plausible_ values (0x00, 0x55, 0xAA, 0xFF) */ if (rr < 0x40) rr = 0x00; else if (rr <= 0x80) rr = 0x55; else if (rr <= 0xC0) rr = 0xAA; else rr = 0xFF; if (gg < 0x40) gg = 0x00; else if (gg <= 0x80) gg = 0x55; else if (gg <= 0xC0) gg = 0xAA; else gg = 0xFF; if (bb < 0x40) bb = 0x00; else if (bb <= 0x80) bb = 0x55; else if (bb <= 0xC0) bb = 0xAA; else bb = 0xFF; best = -1; for (i = 0; i < 16; i++) { if (rr == ascii_computer_colors[i][0] && gg == ascii_computer_colors[i][1] && bb == ascii_computer_colors[i][2]) { /* Exact match */ best = i; } } if (best == -1) { for (i = 0; i < 16; i++) { if ((rr == ascii_computer_colors[i][0] && gg == ascii_computer_colors[i][1] && abs(bb - ascii_computer_colors[i][2]) <= 0x55) || (gg == ascii_computer_colors[i][1] && bb == ascii_computer_colors[i][2] && abs(rr - ascii_computer_colors[i][0]) <= 0x55) || (bb == ascii_computer_colors[i][2] && rr == ascii_computer_colors[i][0] && abs(gg - ascii_computer_colors[i][1]) <= 0x55)) { /* Very close match */ best = i; } } } if (best == -1) { for (i = 0; i < 16; i++) { if ((rr == ascii_computer_colors[i][0] && abs(gg - ascii_computer_colors[i][1]) <= 0x55 && abs(bb - ascii_computer_colors[i][2]) <= 0x55) || (gg == ascii_computer_colors[i][1] && abs(bb - ascii_computer_colors[i][2]) <= 0x55 && abs(rr - ascii_computer_colors[i][0]) <= 0x55) || (bb == ascii_computer_colors[i][2] && abs(rr - ascii_computer_colors[i][0]) <= 0x55 && abs(gg - ascii_computer_colors[i][1]) <= 0x55)) { /* Pretty close match */ best = i; } } } DEBUG_PRINTF("best for %02x%02x%02x = %d: ", rr, gg, bb, best); if (best == -1) best = 0; // oops! rr = ascii_computer_colors[best][0]; gg = ascii_computer_colors[best][1]; bb = ascii_computer_colors[best][2]; DEBUG_PRINTF("%02x%02x%02x\n", rr, gg, bb); } else { /* Use the user-chosen color */ rr = ascii_r; gg = ascii_g; bb = ascii_b; } /* Blit the glyph */ n = get_best_char(which, brightness); ww = ascii_char_x[which][n + 1] - ascii_char_x[which][n]; dest.x = sx + (w - ww) / 2; dest.y = sy; dest.w = ww; dest.h = h; SDL_FillRect(canvas, &dest, SDL_MapRGB(canvas->format, rr, gg, bb)); src.x = ascii_char_x[which][n]; src.y = 0; src.w = ww; src.h = h; dest.x = sx + (w - src.w) / 2; dest.y = sy; SDL_BlitSurface(ascii_bitmap[which], &src, canvas, &dest); } } } } } int get_best_char(int which, int brightness) { int i, diff, best_idx, best_diff; best_idx = -1; best_diff = 255; for (i = 0; i < ascii_num_chars[which]; i++) { diff = abs(ascii_char_brightness[which][i] - brightness); if (diff == best_diff) { if (rand() % 10 <= 3) best_idx = 1; } else if (diff < best_diff) { best_diff = diff; best_idx = i; } } if (best_idx == -1) { /* Shouldn't happen, but just in case */ best_idx = rand() % ascii_num_chars[which]; printf("!?\n"); } DEBUG_PRINTF("best for brightness %d is %d (brightness %d)\n", brightness, best_idx, ascii_char_brightness[which][best_idx]); return best_idx; } int get_bright(magic_api * api, int r, int g, int b) { float fr, fg, fb, y; fr = api->sRGB_to_linear(r); fg = api->sRGB_to_linear(g); fb = api->sRGB_to_linear(b); y = (0.2126 * fr) + (0.7152 * fg) + (0.0722 * fb); return (int) (y * 255); }