// # eaae: Escaped Ascii Art Editor // How often do you have to edit ASCII art? Probably not very often. eaae (pronounced // EE-AA-YY-AA-EE) is an ASCII art editor that's not that great. But it's good enough for // all of those times when you want to edit ASCII art. If you're making a lot of colored // ASCII art, use a better program, or find a better hobby. // // eaae is written in ~800 lines of C (located in this file), and only depends on // [termbox2](https://github.com/termbox/termbox2), a terminal GUI library. It takes in // existing ASCII art from stdin, and writes it to stdout on quit. // // ## Features // - Write colors and letters into a grid of characters. // - Easily modify the hue, saturation, or value of any region of the image. // - Eyedropper tool that writes to a 10-color palette. // - Select a region of the image with the same letter, "magic-wand" style. // - Resize the image. // // ## Usage // Because stdin & stdout are the only inputs & outputs for the program, in order to read // & write files, you have to use pipes & redirects, like so: // ```sh // cat working_on_this.txt | ./eaae >> final.txt // ``` // Keep in mind reading and writing to the same file is not possible, because of the way // the shell runs programs. // // You can also redirect output from another program, like jp2a. // ```sh // jp2a --width=80 --colors | ./eaae >> final.txt // ``` // **MAKE SURE WRITE TO A FILE**, otherwise, there is no way to retrieve your hard work. // // ## Build // ``` // # make sure you download termbox2.h // curl https://raw.githubusercontent.com/termbox/termbox2/master/termbox2.h > termbox2.h // clang eaae.c -O3 -o eaae // ``` #include #include #include #include #define TB_IMPL #define TB_OPT_ATTR_W 32 #include "termbox2.h" struct Color { unsigned char r, g, b; }; struct Cell { struct Color color; char ch; int isselected; }; struct Canvas { struct Cell* cells; int width, height; struct Canvas *undo, *redo; }; enum Mode { MODE_MOVE, MODE_SELECT, MODE_RESIZE, MODE_FILTER, }; typedef struct Cell (*Filter) (struct Cell cell); struct Canvas *main_canvas; enum Mode mode; struct Color palette[10]; int most_recently_written_pallete_index; int filter_amt; Filter current_filter; int cursor_x, cursor_y; const int CURSOR_BG_COLOR = 0x606060; const int SELECTION_BG_COLOR = 0x404040; const int STRONG_COL = 0xffffff; const int WEAK_COL = 0xbbbbbb; const int INST_BG_COL = 0x000000; int color_to_hex_int(struct Color color) { return (color.r << 16) + (color.g << 8) + color.b; } int canvas_area(struct Canvas *canvas) { return canvas->width * canvas->height; } int canvas_area_in_bytes(struct Canvas *canvas) { return sizeof(struct Cell) * canvas->width * canvas->height; } struct Canvas* clone_canvas(struct Canvas *c) { struct Canvas *newc = malloc(sizeof(struct Canvas)); memcpy(newc, c, sizeof(struct Canvas)); int sizeb = canvas_area_in_bytes(c); struct Cell *newcells = malloc(sizeb); memcpy(newcells, newc->cells, sizeb); newc->cells = newcells; newc->undo = NULL; newc->redo = NULL; return newc; } void canvas_free(struct Canvas *c) { free(c->cells); if (c->redo) canvas_free(c->redo); free(c); } void init_5x5_canvas(struct Canvas* canvas) { canvas->width = 5; canvas->height = 5; int size = canvas_area_in_bytes(canvas); canvas->cells = malloc(size); // 32 works as 'space' and also dark gray (low values in rgb) memset(canvas->cells, 33, size); } int parse_num_by_reading_to_char(char terminate, char **in_str) { char *c = *in_str, read; c++; int num = 0; for (; (read = *c) != terminate; c++) { if (read < '0' || read > '9') break; num *= 10; num += read - 48; } *in_str = c; return num; } void read_canvas_from_stdin(struct Canvas* canvas) { if (isatty(STDIN_FILENO)) { init_5x5_canvas(canvas); return; } int linescap = 4, lineslen = 0; char **lines = malloc(linescap * sizeof(*lines)); char *line = NULL; size_t linesiz; while (getline(&line, &linesiz, stdin) != -1) { char *linecpy = malloc(linesiz + 1); memcpy(linecpy, line, linesiz); linecpy[linesiz] = 0; lines[lineslen] = linecpy; lineslen++; if (lineslen == linescap) { linescap *= 2; lines = realloc(lines, linescap * sizeof(char*)); } } int highest_width_so_far = 0; int width_of_current_line = 0; int entered_escape_sequence = 0; for (int l = 0; l < lineslen; l++) { for (char *c = lines[l]; *c != 0; c++) { if (entered_escape_sequence) { if (*c == 'm') entered_escape_sequence = 0; } else { switch (*c) { case 0x1b: entered_escape_sequence = 1; break; case '\n': if (highest_width_so_far < width_of_current_line) highest_width_so_far = width_of_current_line; width_of_current_line = 0; break; default: width_of_current_line++; break; } } } } canvas->width = highest_width_so_far; canvas->height = lineslen; int sizeb = canvas_area_in_bytes(canvas); canvas->cells = malloc(sizeb); memset(canvas->cells, 32, sizeb); struct Color color; for (int l = 0; l < lineslen; l++) { int x = 0; for (char *c = lines[l]; *c != 0; c++) { switch (*c) { case 0x1b: if (*++c != '[') break; if (strncmp(c+1, "38;", 3) == 0) { c += 3; if (strncmp(c+1, "2;", 2) == 0) { c += 2; color.r = parse_num_by_reading_to_char(';', &c); color.g = parse_num_by_reading_to_char(';', &c); color.b = parse_num_by_reading_to_char('m', &c); } else if (strncmp(c+1, "5;", 2) == 0) { c += 2; color.r = parse_num_by_reading_to_char('m', &c); } } else if (strncmp(c+1, "0m", 2) == 0) { c += 2; color.r = 0; color.g = 0; color.b = 0; } assert(*c == 'm'); break; default: canvas->cells[l * canvas->width + x++] = (struct Cell){ .color=color, .ch=*c, }; break; } } free(lines[l]); } free(lines); } void register_change() { if (main_canvas->redo) canvas_free(main_canvas->redo); struct Canvas *new_version = clone_canvas(main_canvas); new_version->undo = main_canvas; main_canvas->redo = new_version; main_canvas = new_version; } void bring_cursor_to_bounds() { if (cursor_y >= main_canvas->height) cursor_y = main_canvas->height - 1; if (cursor_y < 0) cursor_y = 0; if (cursor_x >= main_canvas->width) cursor_x = main_canvas->width - 1; if (cursor_x < 0) cursor_x = 0; } void enter_resize_mode() { mode = MODE_RESIZE; } void remove_row_top() { if (main_canvas->height == 1) return; register_change(); main_canvas->height--; memmove(main_canvas->cells, &main_canvas->cells[main_canvas->width], sizeof(struct Cell) * main_canvas->width * (main_canvas->height - 1)); bring_cursor_to_bounds(); } void remove_row_bottom() { if (main_canvas->height == 1) return; register_change(); main_canvas->height--; bring_cursor_to_bounds(); } void remove_col_left() { if (main_canvas->width == 1) return; register_change(); main_canvas->width--; for (int y = 0; y < main_canvas->height; y++) { struct Cell *new_row_idx = &main_canvas->cells[y * main_canvas->width]; struct Cell *old_row_idx = new_row_idx + y + 1; memmove(new_row_idx, old_row_idx, sizeof(struct Cell) * main_canvas->width); } bring_cursor_to_bounds(); } void remove_col_right() { if (main_canvas->width == 1) return; register_change(); main_canvas->width--; for (int y = 0; y < main_canvas->height; y++) { struct Cell *new_row_idx = &main_canvas->cells[y * main_canvas->width]; struct Cell *old_row_idx = new_row_idx + y; memmove(new_row_idx, old_row_idx, sizeof(struct Cell) * main_canvas->width); } bring_cursor_to_bounds(); } struct Canvas * clone_main_canvas_at_size_diff(int width_diff, int height_diff) { struct Canvas *newc = malloc(sizeof(struct Canvas)); main_canvas->redo = newc; newc->undo = main_canvas; newc->redo = NULL; newc->width = main_canvas->width + width_diff; newc->height = main_canvas->height + height_diff; newc->cells = malloc(canvas_area_in_bytes(newc)); return newc; } void add_row_top() { struct Canvas *newc = clone_main_canvas_at_size_diff(0, 1); memcpy(&newc->cells[main_canvas->width], main_canvas->cells, canvas_area_in_bytes(main_canvas)); memset(&newc->cells[0], '.', sizeof(struct Cell) * newc->width); main_canvas = newc; } void add_row_bottom() { struct Canvas *newc = clone_main_canvas_at_size_diff(0, 1); memcpy(newc->cells, main_canvas->cells, canvas_area_in_bytes(main_canvas)); memset(&newc->cells[canvas_area(main_canvas)], '.', sizeof(struct Cell) * newc->width); main_canvas = newc; } void add_col_right() { struct Canvas *newc = clone_main_canvas_at_size_diff(1, 0); for (int y = 0; y < newc->height; y++) { memcpy(&newc->cells[y * newc->width], &main_canvas->cells[y * main_canvas->width], sizeof(struct Cell) * main_canvas->width); memset(&newc->cells[y * newc->width - 1], '.', sizeof(struct Cell)); } main_canvas = newc; } void add_col_left() { struct Canvas *newc = clone_main_canvas_at_size_diff(1, 0); for (int y = 0; y < newc->height; y++) { memcpy(&newc->cells[y * newc->width + 1], &main_canvas->cells[y * main_canvas->width], sizeof(struct Cell) * main_canvas->width); memset(&newc->cells[y * newc->width], '.', sizeof(struct Cell)); } main_canvas = newc; } void enter_select_mode() { for (int i = 0; i < canvas_area(main_canvas); i++) main_canvas->cells[i].isselected = 0; mode = MODE_SELECT; } void enter_move_mode() { mode = MODE_MOVE; } void sel_letter_region_visit(int x, int y, char regionc) { struct Cell* cell = &main_canvas->cells[y * main_canvas->width + x]; if (cell->isselected || cell->ch != regionc) return; cell->isselected = 1; sel_letter_region_visit(x+1, y , regionc); sel_letter_region_visit(x-1, y , regionc); sel_letter_region_visit(x , y+1, regionc); sel_letter_region_visit(x , y-1, regionc); } void sel_letter_region() { char c = main_canvas->cells[cursor_y * main_canvas->width + cursor_x].ch; sel_letter_region_visit(cursor_x+1, cursor_y , c); sel_letter_region_visit(cursor_x-1, cursor_y , c); sel_letter_region_visit(cursor_x , cursor_y+1, c); sel_letter_region_visit(cursor_x , cursor_y-1, c); } void asciibrush() { struct tb_event ev; register_change(); tb_print(0, 0, 0xffffff, TB_HI_BLACK, "PRESS ANY KEY"); tb_present(); do { tb_poll_event(&ev); } while (ev.type != TB_EVENT_KEY && ev.ch != 0); tb_clear(); for (int i = 0; i < canvas_area(main_canvas); i++) { struct Cell *cell = &main_canvas->cells[i]; if (cell->isselected) cell->ch = ev.ch; } enter_move_mode(); } void invert_selection() { for (int i = 0; i < canvas_area(main_canvas); i++) { struct Cell *cell = &main_canvas->cells[i]; cell->isselected = !cell->isselected; } } void select_all() { for (int i = 0; i < canvas_area(main_canvas); i++) main_canvas->cells[i].isselected = 1; } void deselect_all() { for (int i = 0; i < canvas_area(main_canvas); i++) main_canvas->cells[i].isselected = 0; } void enter_filter_mode() { mode = MODE_FILTER; filter_amt = 0; current_filter = NULL; } void apply_filter_and_exit_filter_mode() { register_change(); int area = main_canvas->height * main_canvas->width; for (int i = 0; i < area; i++) { struct Cell *cell = &main_canvas->cells[i]; if (cell->isselected) *cell = current_filter(*cell); } enter_move_mode(); } void rgb2hsv(struct Color color, double *h, double *s, double *v) { double min, max, delta; double r = (double)color.r, g = (double)color.g, b = (double)color.b; if (r == g && g == b) { *h = 0; *s = 0; *v = r; return; } min = r < g ? r : g; min = min < b ? min : b; max = r > g ? r : g; max = max > b ? max : b; *v = max; delta = max - min; *s = (delta / max); if (r >= max) *h = (g - b) / delta; // between yellow & magenta else if (g >= max) *h = 2.0 + (b - r) / delta; // between cyan & yellow else *h = 4.0 + (r - g) / delta; // between magenta & cyan *h *= 60.0; // degrees if (*h < 0.0) *h += 360.0; } void hsv2rgb(double h, double s, double v, struct Color* color) { double hh, p, q, t, ff, r, g, b; long i; if(s <= 0.001) { *color = (struct Color){ .r = r, .g = g, .b = b }; return; } hh = h; if(hh >= 360.0) hh = 0.0; hh /= 60.0; i = (long)hh; ff = hh - i; p = v * (1.0 - s); q = v * (1.0 - (s * ff)); t = v * (1.0 - (s * (1.0 - ff))); switch(i) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; break; default: break; } *color = (struct Color){ .r = r, .g = g, .b = b }; } struct Cell hueshift(struct Cell cell) { double h, s, v; rgb2hsv(cell.color, &h, &s, &v); h += (double)filter_amt; while (h >= 360) h -= 360.0; while (h <= 0) h += 360.0; hsv2rgb(h, s, v, &cell.color); return cell; } struct Cell saturate(struct Cell cell) { double h, s, v; rgb2hsv(cell.color, &h, &s, &v); s += ((double)filter_amt / 20.0); if (s <= 0.0) s = 0.0; if (s >= 1.0) s = 1.0; hsv2rgb(h, s, v, &cell.color); return cell; } struct Cell valuechange(struct Cell cell) { double h, s, v; rgb2hsv(cell.color, &h, &s, &v); v += ((double)filter_amt); if (v <= 0.0) v = 0.0; if (v >= 255.0) v = 255.0; hsv2rgb(h, s, v, &cell.color); return cell; } void eyedrop() { struct Cell *selcell = &main_canvas->cells[cursor_y * main_canvas->width + cursor_x]; palette[most_recently_written_pallete_index] = selcell->color; most_recently_written_pallete_index = (most_recently_written_pallete_index + 1) % 10; } void paintbrush(int index) { int area = main_canvas->width * main_canvas->height; for (int i = 0; i < area; i++) { struct Cell *cell = &main_canvas->cells[i]; if (cell->isselected) cell->color = palette[index]; } } void draw_canvas(struct Canvas* c, int offset_x, int offset_y) { for (int x = 0; x < c->width; x++) { for (int y = 0; y < c->height; y++) { int index = y * c->width + x; struct Cell cell = c->cells[index]; if (mode == MODE_FILTER && current_filter != NULL && cell.isselected) cell = current_filter(cell); int fg_color = (cell.color.r << 16) + (cell.color.g << 8) + cell.color.b; if (fg_color == 0x000000) fg_color = TB_HI_BLACK; int bg_color = 0; switch(mode) { case MODE_SELECT: if (cell.isselected) bg_color = SELECTION_BG_COLOR; /* FALLTHROUGH */ case MODE_MOVE: if (x == cursor_x && y == cursor_y) bg_color = CURSOR_BG_COLOR; break; default: break; } tb_set_cell(offset_x + x, offset_y + y, cell.ch, fg_color, bg_color); } } } void draw_instruction(int x, int y, char *inst, int underl_index, int color) { tb_print(x, y, color, 0, inst); tb_set_cell(x + underl_index, y, inst[underl_index], color | TB_UNDERLINE, 0); } void draw_instructions(struct Canvas* main_canvas) { int x = main_canvas->width + 1; int y = 0; switch(mode) { case MODE_MOVE: tb_print(x, y++, STRONG_COL, 0, "MOVE:"); tb_print(x, y++, WEAK_COL, 0, " k "); tb_print(x, y++, WEAK_COL, 0, " h l "); tb_print(x, y++, WEAK_COL, 0, " j "); tb_print(x, y++, WEAK_COL, 0, " "); draw_instruction(x, y++, "select", 0, WEAK_COL); draw_instruction(x, y++, "resize", 4, WEAK_COL); draw_instruction(x, y++, "eye(i)dropper", 4, WEAK_COL); draw_instruction(x, y++, "exit", 1, WEAK_COL); break; case MODE_SELECT: tb_print(x, y++, STRONG_COL | TB_BOLD, 0, "SELECT:"); tb_print(x, y++, WEAK_COL , 0, " k "); tb_print(x, y++, WEAK_COL , 0, " h l "); tb_print(x, y++, WEAK_COL , 0, " j "); y++; draw_instruction(x, y++, "select here", 9, WEAK_COL); draw_instruction(x, y++, "select all", 7, WEAK_COL); draw_instruction(x, y++, "deselect all", 0, WEAK_COL); draw_instruction(x, y++, "invert selection", 1, WEAK_COL); tb_print (x, y++, WEAK_COL, 0, "paintbrush (num)"); draw_instruction(x, y++, "asciibrush", 4, WEAK_COL); draw_instruction(x, y++, "move", 0, WEAK_COL); draw_instruction(x, y++, "letter region", 1, WEAK_COL); break; case MODE_RESIZE: tb_print(x, y++, STRONG_COL | TB_BOLD, 0, " GROW: "); tb_print(x, y++, WEAK_COL , 0, " k "); tb_print(x, y++, WEAK_COL , 0, " h l "); tb_print(x, y++, WEAK_COL , 0, " j "); y++; tb_print(x, y++, STRONG_COL | TB_BOLD, 0, "SHRINK:"); tb_print(x, y++, WEAK_COL , 0, " K "); tb_print(x, y++, WEAK_COL , 0, " H L "); tb_print(x, y++, WEAK_COL , 0, " J "); break; case MODE_FILTER: if (current_filter == NULL) { tb_print(x, y++, STRONG_COL | TB_BOLD, 0, "FILTERS:"); draw_instruction(x, y++, "hueshift", 0, WEAK_COL); draw_instruction(x, y++, "saturate", 0, WEAK_COL); draw_instruction(x, y++, "valuechange", 0, WEAK_COL); } else { draw_instruction(x, y++, "increase (k)", 10, WEAK_COL); draw_instruction(x, y++, "decrease (j)", 10, WEAK_COL); draw_instruction(x, y++, "apply", 0, WEAK_COL); tb_printf(x, y++, WEAK_COL, 0, "%d", filter_amt); } break; } y = main_canvas->height; if (y < 15) y = 15; x = 0; tb_printf(x, y, WEAK_COL, 0, "%d x %d", main_canvas->width, main_canvas->height); x += 10; draw_instruction(x, y, "undo", 0, WEAK_COL); x += 5; draw_instruction(x, y, "redo", 0, WEAK_COL); x += 5; for (int i = 0; i < 10; i++) { struct Color pc = palette[i]; int fg = (pc.r + pc.g + pc.b < (256*2/3)) ? 0xffffff : TB_HI_BLACK;; int bg = (pc.r << 16) + (pc.g << 8) + pc.b; tb_printf(x++, y, fg, bg, "%d", i); } } void print_main_canvas_to_stdout() { struct Color color; for (int y = 0; y < main_canvas->height; y++) { for (int x = 0; x < main_canvas->width; x++) { struct Cell cell = main_canvas->cells[y * main_canvas->width + x]; if (color.r != cell.color.r || color.g != cell.color.g || color.b != cell.color.b) { color = cell.color; printf("\x1b[38;2;%d;%d;%dm", color.r, color.g, color.b); } printf("%c", cell.ch); } printf("\n"); } } int main(int argc, char **argv) { tb_init(); tb_set_output_mode(TB_OUTPUT_TRUECOLOR); tb_set_input_mode(TB_INPUT_ESC | TB_INPUT_MOUSE); main_canvas = malloc(sizeof(struct Canvas)); read_canvas_from_stdin(main_canvas); struct tb_event ev; int y = 0; while(1) { draw_instructions(main_canvas); draw_canvas(main_canvas, 0, 0); tb_present(); tb_poll_event(&ev); if (ev.ch == 'x') break; tb_clear(); switch (ev.type) { case TB_EVENT_KEY: if (ev.ch == 'u') { if (main_canvas->undo) main_canvas = main_canvas->undo; } else if (ev.ch == 'r') { if (main_canvas->redo) main_canvas = main_canvas->redo; } else if (ev.key == TB_KEY_ESC) { enter_move_mode(); } switch (mode) { case MODE_SELECT: if (ev.ch >= '0' && ev.ch <= '9') { paintbrush((int)(ev.ch - '0')); break; } switch (ev.ch) { case 'i': asciibrush(); break; case 'e': sel_letter_region(); break; case 'a': select_all(); break; case 'd': deselect_all(); break; case 'n': invert_selection(); break; case 'f': enter_filter_mode(); break; case 'r': main_canvas->cells[cursor_y * main_canvas->width + cursor_x].isselected = 1; break; } /* fallthrough */ case MODE_MOVE: if (ev.ch == 'h' || ev.key == TB_KEY_ARROW_LEFT) { if (cursor_x > 0) cursor_x--; } else if (ev.ch == 'k' || ev.key == TB_KEY_ARROW_UP) { if (cursor_y > 0) cursor_y--; } else if (ev.ch == 'l' || ev.key == TB_KEY_ARROW_RIGHT) { if (cursor_x < main_canvas->width - 1) cursor_x++; } else if (ev.ch == 'j' || ev.key == TB_KEY_ARROW_DOWN) { if (cursor_y < main_canvas->height - 1) cursor_y++; } else if (ev.ch == '0') { cursor_x = 0; } else if (ev.ch == '$') { cursor_x = main_canvas->width - 1; } else if (ev.ch == 'i') { eyedrop(); } else if (ev.ch == 's') { enter_select_mode(); } else if (ev.ch == 'z') { enter_resize_mode(); } break; case MODE_RESIZE: switch (ev.ch) { case 'K': remove_row_top(); break; case 'J': remove_row_bottom(); break; case 'H': remove_col_left(); break; case 'L': remove_col_right(); break; case 'k': add_row_top(); break; case 'j': add_row_bottom(); break; case 'l': add_col_right(); break; case 'h': add_col_left(); break; } case MODE_FILTER: if (current_filter == NULL) { switch (ev.ch) { case 'h': current_filter = hueshift; break; case 's': current_filter = saturate; break; case 'v': current_filter = valuechange; break; } } else { switch (ev.ch) { case 'k': filter_amt++; break; case 'j': filter_amt--; break; case 'a': apply_filter_and_exit_filter_mode(); break; } } default: break; } break; case TB_EVENT_MOUSE: cursor_x = ev.x < main_canvas->width ? ev.x : main_canvas->width - 1; cursor_y = ev.y < main_canvas->height ? ev.y : main_canvas->height - 1; break; } } tb_clear(); draw_instruction(0, 0, "Quit Without Printing Result", 0, 0xffffff); draw_instruction(0, 1, "Print & Quit", 0, 0xffffff); tb_present(); do { tb_poll_event(&ev); } while (ev.ch != 'q' && ev.ch != 'p'); tb_shutdown(); switch (ev.ch) { case 'q': break; case 'p': print_main_canvas_to_stdout(); break; default: break; } return 0; }