diff --git a/Makefile b/Makefile index 4679d8b..060818f 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CLIBS := -lm INCLUDES := -Isrc OBJECTS := obj/stb_image.o obj/stb_image_resize.o \ obj/colors.o obj/args.o obj/image.o obj/commons.o \ - obj/mod_blocks.o obj/mod_braille.o + obj/mod_blocks.o obj/mod_braille.o obj/mod_charmap.o VERSION := "v-git-$(shell git rev-parse --short HEAD)" all: lib yaitaa diff --git a/src/args.c b/src/args.c index e2785e9..9e7a88f 100644 --- a/src/args.c +++ b/src/args.c @@ -9,6 +9,7 @@ #include "commons.h" #include "mod_blocks.h" #include "mod_braille.h" +#include "mod_charmap.h" typedef struct { int value; @@ -32,7 +33,7 @@ const asc_handler_t asc_handlers[ASC_MOD_ENDL + 1] = { { ASC_MOD_GRADIENT, { "g", "grd", "gradient", NULL }, "Gradient of characters. No matching at all", - NULL, NULL }, + mod_cmap_prepare, mod_cmap_main }, { ASC_MOD_BRUTEFORCE, { "f", "guess", "bruteforce", NULL }, "Looking for best possible character", @@ -100,6 +101,7 @@ void usage(int argc, char **argv) fprintf(stderr, "-O FILE\t\tOutput file. Default: - (stdout)\n"); fprintf(stderr, "-W WIDTH\tOutput width (in characters)\n"); fprintf(stderr, "-H HEIGHT\tOutput height (in characters)\n"); + fprintf(stderr, "-C CHARSET\tList of characters. Used in some modes\n"); fprintf(stderr, "\n"); fprintf(stderr, "-M MODE\t\tOutput mode\n"); fprintf(stderr, "-S STYLE\tStyle (palette)\n"); @@ -147,9 +149,9 @@ int parse_args(int argc, char **argv, asc_args_t *args) args->charset = " .'-*+$@"; int c; #ifdef DISABLE_LOGGING - while ((c = getopt(argc, argv, "hdVW:H:M:S:F:P:O:")) != -1) + while ((c = getopt(argc, argv, "hdVC:W:H:M:S:F:P:O:")) != -1) #else - while ((c = getopt(argc, argv, "vhdVW:H:M:S:F:P:O:")) != -1) + while ((c = getopt(argc, argv, "vhdVC:W:H:M:S:F:P:O:")) != -1) #endif { switch (c) @@ -168,6 +170,9 @@ int parse_args(int argc, char **argv, asc_args_t *args) case 'd': args->dither = true; break; + case 'C': + args->charset = optarg; + break; case 'W': if ((args->width = atoi(optarg)) < 0) { @@ -282,6 +287,10 @@ int prepare_state(int argc, char **argv, asc_args_t args, asc_state_t *state) LOG("Image size: %dx%d", state->source_image->width, state->source_image->height); + state->image = state->source_image; + + state->userdata = NULL; + // Palette configuration switch (args.out_style) { diff --git a/src/args.h b/src/args.h index b21f939..9d3a98d 100644 --- a/src/args.h +++ b/src/args.h @@ -68,11 +68,12 @@ typedef struct { image_t *source_image; image_t *image; palette_t *palette; + void *userdata; FILE *out_file; } asc_state_t; -typedef void (*asc_module_initializer_fn)(asc_state_t *state); -typedef void (*asc_module_handler_fn)(asc_state_t state); +typedef bool (*asc_module_initializer_fn)(asc_state_t *state); +typedef bool (*asc_module_handler_fn)(asc_state_t state); typedef struct { int id; diff --git a/src/colors.c b/src/colors.c index 48a5877..dfc1b99 100644 --- a/src/colors.c +++ b/src/colors.c @@ -141,7 +141,7 @@ void make_pal256(palette_t *dst, palette_t ansi) float calc_brightness(rgba8 c) { - return 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b; + return (0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b) / 255.0; } bool load_palette_gpl(palette_t *pal, FILE *fp) diff --git a/src/commons.c b/src/commons.c index 3de7ef6..0d3c11f 100644 --- a/src/commons.c +++ b/src/commons.c @@ -44,9 +44,14 @@ void _log(const char *fmt, ...) } #endif -void c_fatal(int code, const char *reason) +void c_fatal(int code, const char *reason, ...) { - fprintf(stderr, "Error: %s\n", reason); + fprintf(stderr, "Error: "); + va_list args; + va_start(args, reason); + vfprintf(stderr, reason, args); + va_end(args); + fprintf(stderr, "\n"); exit(code); } @@ -64,3 +69,129 @@ palette_t *get_palette_by_id(asc_style_t stl) } return pal; } + +// From: https://github.com/raysan5/raylib/blob/e5ee69a/src/rtext.c#L1729 +__attribute__((annotate("oclint:suppress[bitwise operator in conditional]"))) +__attribute__((annotate("oclint:suppress[high cyclomatic complexity]"))) +__attribute__((annotate("oclint:suppress[high npath complexity]"))) +__attribute__((annotate("oclint:suppress[high ncss method]"))) +__attribute__((annotate("oclint:suppress[long method]"))) +int get_codepoint(char *text, int *processed) +{ + int code = 0; + int octet = (unsigned char)(text[0]); + *processed = 1; + + if (octet <= 0x7f) + { + code = text[0]; + } + else if ((octet & 0xe0) == 0xc0) + { + unsigned char octet1 = text[1]; + if ((octet1 == '\0') || ((octet1 >> 6) != 2)) + { *processed = 2; return 0; } + if ((octet >= 0xc2) && (octet <= 0xdf)) + { + code = ((octet & 0x1f) << 6) | (octet1 & 0x3f); + *processed = 2; + } + } + else if ((octet & 0xf0) == 0xe0) + { + unsigned char octet1 = text[1]; + unsigned char octet2 = '\0'; + if ((octet1 == '\0') || ((octet1 >> 6) != 2)) + { *processed = 2; return 0; } + octet2 = text[2]; + if ((octet2 == '\0') || ((octet2 >> 6) != 2)) + { *processed = 3; return 0; } + if (((octet == 0xe0) && !((octet1 >= 0xa0) && (octet1 < 0xbf))) || + ((octet == 0xed) && !((octet1 >= 0x80) && (octet1 <= 0x9f)))) + { *processed = 2; return 0; } + if ((octet >= 0xe0) && (octet <= 0xef)) + { code = ((octet & 0xf) << 12) | ((octet1 & 0x3f) << 6) | (octet2 & 0x3f); + *processed = 3; } + } + else if ((octet & 0xf8) == 0xf0) + { + if (octet > 0xf4) return 0; + unsigned char octet1 = text[1]; + unsigned char octet2 = '\0'; + unsigned char octet3 = '\0'; + if ((octet1 == '\0') || ((octet1 >> 6) != 2)) + { *processed = 2; return 0; } + octet2 = text[2]; + if ((octet2 == '\0') || ((octet2 >> 6) != 2)) + { *processed = 3; return 0; } + if ((octet3 == '\0') || ((octet3 >> 6) != 2)) + { *processed = 4; return 0; } + if (((octet == 0xf0) && !((octet1 >= 0x90) && (octet1 <= 0xbf))) || + ((octet == 0xf4) && !((octet1 >= 0x80) && (octet1 <= 0x8f)))) + { *processed = 2; return 0; } + if (octet >= 0xf0) + { + code = (((octet & 0x7) << 18) | + ((octet1 & 0x3f) << 12) | + ((octet2 & 0x3f) << 6) | + (octet3 & 0x3f)); + *processed = 4; + } + } + return code; +} + +// From: https://gist.github.com/MightyPork/52eda3e5677b4b03524e40c9f0ab1da5 +__attribute__((annotate("oclint:suppress[high ncss method]"))) +int set_codepoint(char *buf, int codepoint) +{ + if (codepoint <= 0x7F) + { + *buf++ = (char)codepoint; + *buf++ = '\0'; + return 1; + } + else if (codepoint <= 0x07FF) + { + *buf++ = (char)(((codepoint >> 6) & 0x1F) | 0xC0); + *buf++ = (char)(((codepoint >> 0) & 0x3F) | 0x80); + *buf++ = '\0'; + return 2; + } + else if (codepoint <= 0xFFFF) + { + *buf++ = (char)(((codepoint >> 12) & 0x0F) | 0xE0); + *buf++ = (char)(((codepoint >> 6) & 0x3F) | 0x80); + *buf++ = (char)(((codepoint >> 0) & 0x3F) | 0x80); + *buf++ = '\0'; + return 3; + } + else if (codepoint <= 0x10FFFF) + { + *buf++ = (char)(((codepoint >> 18) & 0x07) | 0xF0); + *buf++ = (char)(((codepoint >> 12) & 0x3F) | 0x80); + *buf++ = (char)(((codepoint >> 6) & 0x3F) | 0x80); + *buf++ = (char)(((codepoint >> 0) & 0x3F) | 0x80); + *buf++ = '\0'; + return 4; + } + *buf++ = '\xEF'; + *buf++ = '\xBF'; + *buf++ = '\xBD'; + *buf++ = '\0'; + return 0; +} + +int n_codepoints(char *text) +{ + int proc = 0, length = 0; + while (*text != '\0') + { + if (get_codepoint(text, &proc) == 0) + break; + length++; + text += proc; + } + return length; +} + diff --git a/src/commons.h b/src/commons.h index 3e85ccc..8148fb3 100644 --- a/src/commons.h +++ b/src/commons.h @@ -32,8 +32,12 @@ extern bool b_logging; void _log(const char *fmt, ...); #endif -void c_fatal(int code, const char *reason); +void c_fatal(int code, const char *reason, ...); void m_prepare_dither(asc_state_t *state); palette_t *get_palette_by_id(asc_style_t stl); +int get_codepoint(char *text, int *processed); +int set_codepoint(char *buf, int codepoint); +int n_codepoints(char *text); + #endif diff --git a/src/fmt_strings.h b/src/fmt_strings.h index 4719ecb..4a66ea9 100644 --- a/src/fmt_strings.h +++ b/src/fmt_strings.h @@ -22,6 +22,7 @@ #define S_JSON_LSTA " [\n" #define S_JSON_PBBW " { \"char\": \"%s\", \"fg\": 16777215, \"bg\": 0 }" #define S_JSON_PRGB " { \"char\": \"\\u%04x\", \"fg\": %d, \"bg\": %d }" +#define S_JSON_PUCC " { \"char\": \"\\u%04x\", \"fg\": %d, \"bg\": 0 }" #define S_JSON_PBLK " { \"char\": \"%s\", \"fg\": %d, \"bg\": %d }" #define S_JSON_LEND " ],\n" #define S_JSON_LEND_FINAL " ]\n" @@ -33,13 +34,18 @@ "background: rgb(%d, %d, %d);\">&#%d;" #define S_HTML_PBLK "%s" +#define S_HTML_PUCC "&#%d;" +#define S_HTML_PUBW "&#%d;" #define S_HTML_PBBW "%s" #define S_HTML_LEND "\n" #define S_HTML_TAIL "" -#define S_ANSI "\033[%d;%dm" -#define S_ANSI_RGB "\033[38;2;%d;%d;%d;48;2;%d;%d;%dm" -#define S_ANSI_256 "\033[38;5;%d;48;5;%dm" -#define S_ANSI_RST "\033[0m" +#define S_ANSI "\033[%d;%dm" +#define S_ANSI_FG "\033[%dm" +#define S_ANSI_RGB "\033[38;2;%d;%d;%d;48;2;%d;%d;%dm" +#define S_ANSI_256 "\033[38;5;%d;48;5;%dm" +#define S_ANSI_FG_256 "\033[38;5;%dm" +#define S_ANSI_FG_RGB "\033[38;2;%d;%d;%dm" +#define S_ANSI_RST "\033[0m" #endif diff --git a/src/main.c b/src/main.c index d4e27a6..7d7e437 100644 --- a/src/main.c +++ b/src/main.c @@ -20,10 +20,9 @@ #include #include #include "args.h" -#include "mod_blocks.h" -#include "mod_braille.h" #include "commons.h" +__attribute__((annotate("oclint:suppress[high npath complexity]"))) int main(int argc, char **argv) { asc_args_t args; @@ -40,10 +39,14 @@ int main(int argc, char **argv) if (handler.prepare == NULL) c_fatal(12, "this mode is not implemented yet"); - handler.prepare(&state); - handler.main(state); + if (!handler.prepare(&state)) + c_fatal(15, "handler.prepare() said a big no-no"); + + if (!handler.main(state)) + c_fatal(16, "handler.main() said a big no-no"); - image_unload(state.image); + if (state.image != state.source_image) + image_unload(state.image); image_unload(state.source_image); if (args.out_style == ASC_STL_PALETTE) diff --git a/src/mod_blocks.c b/src/mod_blocks.c index 2c7e3ce..5404d5b 100644 --- a/src/mod_blocks.c +++ b/src/mod_blocks.c @@ -19,7 +19,7 @@ void __blk_end_line(asc_state_t s, bool final); void __blk_end_output(asc_state_t s); -void mod_blocks_prepare(asc_state_t *state) +bool mod_blocks_prepare(asc_state_t *state) { int width, height; get_size_keep_aspect( @@ -33,9 +33,10 @@ void mod_blocks_prepare(asc_state_t *state) state->image = image_resize(state->source_image, width, height); if (state->args.dither) m_prepare_dither(state); + return true; } -void mod_blocks_main(asc_state_t state) +bool mod_blocks_main(asc_state_t state) { image_t *img = state.image; __blk_start_output(state); @@ -52,6 +53,7 @@ void mod_blocks_main(asc_state_t state) __blk_end_line(state, final); } __blk_end_output(state); + return true; } void __blk_start_output(asc_state_t state) diff --git a/src/mod_blocks.h b/src/mod_blocks.h index 0e16dea..797afea 100644 --- a/src/mod_blocks.h +++ b/src/mod_blocks.h @@ -21,7 +21,7 @@ #include "colors.h" #include "args.h" -void mod_blocks_prepare(asc_state_t *state); -void mod_blocks_main(asc_state_t state); +bool mod_blocks_prepare(asc_state_t *state); +bool mod_blocks_main(asc_state_t state); #endif diff --git a/src/mod_braille.c b/src/mod_braille.c index 183e34f..f5dc600 100644 --- a/src/mod_braille.c +++ b/src/mod_braille.c @@ -30,7 +30,7 @@ void __bra_update2x4(image_t *img, rgba8 block[8], int x, int y) } -void mod_braille_prepare(asc_state_t *state) +bool mod_braille_prepare(asc_state_t *state) { int width, height; get_size_keep_aspect( @@ -44,9 +44,10 @@ void mod_braille_prepare(asc_state_t *state) state->image = image_resize(state->source_image, width, height); if (state->args.dither) m_prepare_dither(state); + return true; } -void mod_braille_main(asc_state_t state) +bool mod_braille_main(asc_state_t state) { image_t *img = state.image; @@ -92,6 +93,7 @@ void mod_braille_main(asc_state_t state) __bra_end_line(state, final); } __bra_end_output(state); + return true; } int __bra_best_match_i(rgba8 a, rgba8 b, rgba8 t) diff --git a/src/mod_braille.h b/src/mod_braille.h index 7f9b0ad..ac6b398 100644 --- a/src/mod_braille.h +++ b/src/mod_braille.h @@ -21,7 +21,7 @@ #include "colors.h" #include "args.h" -void mod_braille_prepare(asc_state_t *state); -void mod_braille_main(asc_state_t state); +bool mod_braille_prepare(asc_state_t *state); +bool mod_braille_main(asc_state_t state); #endif diff --git a/src/mod_charmap.c b/src/mod_charmap.c new file mode 100644 index 0000000..923288d --- /dev/null +++ b/src/mod_charmap.c @@ -0,0 +1,209 @@ +#include +#include +#include "mod_charmap.h" +#include "image.h" +#include "colors.h" +#include "commons.h" +#include "fmt_strings.h" + +void __chr_start_output(asc_state_t s); +void __chr_start_line(asc_state_t s, bool final); +void __chr_put_pixel(asc_state_t s, rgba8 fg, bool final); +void __chr_putc_bw(asc_state_t s, int cc, bool final); +void __chr_putc_ansi(asc_state_t s, int fg, int cc, bool final); +void __chr_putc_256(asc_state_t s, int fg, int cc, bool final); +void __chr_putc_true(asc_state_t s, rgba8 fg, int cc, bool final); +void __chr_end_line(asc_state_t s, bool final); +void __chr_end_output(asc_state_t s); + +typedef struct { + int n_codepoints; + int *codepoints; +} __m_cmap_userdata_t; + +bool mod_cmap_prepare(asc_state_t *state) +{ + static __m_cmap_userdata_t userdata; + static int codepoints[M_CMAP_MAX_CODEPOINTS]; + userdata.codepoints = codepoints; + int n_cp = 0, cp, proc; + char *charset = state->args.charset; + while ((cp = get_codepoint(charset, &proc)) != 0) + { codepoints[n_cp++] = cp; charset += proc; } + if (n_cp > M_CMAP_MAX_CODEPOINTS) + { + fprintf(stderr, "Error: maximum of %d codepoints is allowed, got %d\n", + M_CMAP_MAX_CODEPOINTS, n_cp); + return false; + } + userdata.n_codepoints = n_cp; + state->userdata = (void *)&userdata; + + int width, height; + get_size_keep_aspect( + state->source_image->width * 2, state->source_image->height, + state->args.width, state->args.height, &width, &height); + LOG("Source size: %dx%d", state->source_image->width, + state->source_image->height); + LOG("Requested size: %dx%d", state->args.width, state->args.height); + LOG("Resizing image to %dx%d", width, height); + state->image = image_resize(state->source_image, width, height); + if (state->args.dither) + m_prepare_dither(state); + return true; +} + +bool mod_cmap_main(asc_state_t state) +{ + image_t *img = state.image; + __chr_start_output(state); + for (int y = 0; y < img->height; y++) + { + bool final = y >= img->height; + __chr_start_line(state, final); + for (int x = 0; x < img->width; x++) + { + __chr_put_pixel(state, img->pixels[x + y * img->width], x >= img->width); + } + __chr_end_line(state, final); + } + __chr_end_output(state); + return true; +} + +void __chr_start_output(asc_state_t state) +{ + if (state.args.out_format == ASC_FMT_JSON) + fprintf(state.out_file, S_JSON_HEAD, + state.image->width, state.image->height / 2); + else if (state.args.out_format == ASC_FMT_HTML) + fprintf(state.out_file, S_HTML_HEAD); +} + +void __chr_start_line(asc_state_t state, bool final) +{ + (void)final; + if (state.args.out_format == ASC_FMT_JSON) + fprintf(state.out_file, S_JSON_LSTA); + else if (state.args.out_format == ASC_FMT_HTML) + fprintf(state.out_file, S_HTML_LSTA); +} + +void __chr_put_pixel(asc_state_t s, rgba8 fg, bool final) +{ + float brightness = calc_brightness(fg); + __m_cmap_userdata_t data = *(__m_cmap_userdata_t *)s.userdata; + int ndx = floorf(brightness * data.n_codepoints); + if (ndx >= data.n_codepoints) ndx = data.n_codepoints - 1; + int codepoint = data.codepoints[ndx]; + switch (s.args.out_style) + { + case ASC_STL_BLACKWHITE: + __chr_putc_bw(s, codepoint, final); + break; + case ASC_STL_ANSI_VGA: + case ASC_STL_ANSI_XTERM: + case ASC_STL_ANSI_DISCORD: + __chr_putc_ansi(s, closest_color(*s.palette, fg), codepoint, final); + break; + case ASC_STL_256COLOR: + __chr_putc_256(s, closest_color(c_palette_256, fg), codepoint, final); + break; + case ASC_STL_PALETTE: + __chr_putc_true(s, clamp_to_pal(*s.palette, fg), codepoint, final); + break; + case ASC_STL_TRUECOLOR: + __chr_putc_true(s, fg, codepoint, final); + break; + case ASC_STL_ENDL: + break; + } +} + +void __chr_putc_bw(asc_state_t s, int cc, bool final) +{ + static char buf[8]; + set_codepoint(buf, cc); + if (s.args.out_format == ASC_FMT_JSON) + { + fprintf(s.out_file, S_JSON_PUCC, cc, 0xFFFFFF); + if (!final) fprintf(s.out_file, ","); + fprintf(s.out_file, "\n"); + } + else if (s.args.out_format == ASC_FMT_HTML) + fprintf(s.out_file, S_HTML_PUBW, cc); + else + fprintf(s.out_file, "%s", buf); +} + +void __chr_putc_ansi(asc_state_t s, int fg, int cc, bool final) +{ + static char buf[8]; + rgba8 fgc = s.palette->palette[fg]; + set_codepoint(buf, cc); + if (s.args.out_format == ASC_FMT_JSON) + { + fprintf(s.out_file, S_JSON_PUCC, cc, RGBN(fgc)); + if (!final) fprintf(s.out_file, ","); + fprintf(s.out_file, "\n"); + } + else if (s.args.out_format == ASC_FMT_HTML) + fprintf(s.out_file, S_HTML_PUCC, fgc.r, fgc.g, fgc.b, cc); + else + fprintf(s.out_file, S_ANSI_FG"%s", fg + (fg >= 8 ? 82 : 30), buf); +} + +void __chr_putc_256(asc_state_t s, int fg, int cc, bool final) +{ + static char buf[8]; + rgba8 fgc = pal256_to_rgb(*s.palette, fg); + set_codepoint(buf, cc); + if (s.args.out_format == ASC_FMT_JSON) + { + fprintf(s.out_file, S_JSON_PUCC, cc, RGBN(fgc)); + if (!final) fprintf(s.out_file, ","); + fprintf(s.out_file, "\n"); + } + else if (s.args.out_format == ASC_FMT_HTML) + fprintf(s.out_file, S_HTML_PUCC, fgc.r, fgc.g, fgc.b, cc); + else + fprintf(s.out_file, S_ANSI_FG_256"%s", fg, buf); +} + +void __chr_putc_true(asc_state_t s, rgba8 fg, int cc, bool final) +{ + static char buf[8]; + set_codepoint(buf, cc); + if (s.args.out_format == ASC_FMT_JSON) + { + fprintf(s.out_file, S_JSON_PUCC, cc, RGBN(fg)); + if (!final) fprintf(s.out_file, ","); + fprintf(s.out_file, "\n"); + } + else if (s.args.out_format == ASC_FMT_HTML) + fprintf(s.out_file, S_HTML_PUCC, fg.r, fg.g, fg.b, cc); + else + fprintf(s.out_file, S_ANSI_FG_RGB"%s", fg.r, fg.g, fg.b, buf); +} + +void __chr_end_line(asc_state_t state, bool final) +{ + if (state.args.out_format == ASC_FMT_JSON) + fprintf(state.out_file, final ? S_JSON_LEND_FINAL : S_JSON_LEND); + else if (state.args.out_format == ASC_FMT_HTML) + fprintf(state.out_file, S_HTML_LEND); + else + { + if (state.args.out_style != ASC_STL_BLACKWHITE) + fprintf(state.out_file, S_ANSI_RST); + fprintf(state.out_file, "\n"); + } +} + +void __chr_end_output(asc_state_t state) +{ + if (state.args.out_format == ASC_FMT_JSON) + fprintf(state.out_file, S_JSON_TAIL); + else if (state.args.out_format == ASC_FMT_HTML) + fprintf(state.out_file, S_HTML_TAIL); +} diff --git a/src/mod_charmap.h b/src/mod_charmap.h new file mode 100644 index 0000000..787a170 --- /dev/null +++ b/src/mod_charmap.h @@ -0,0 +1,29 @@ +/* + * yaitaa - yet another image to ascii art converter + * Copyright (C) 2022 hatkidchan + * + * 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 3 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, see . + */ +#ifndef _MOD_CHARMAP_ +#define _MOD_CHARMAP_ +#include +#include "colors.h" +#include "args.h" + +#define M_CMAP_MAX_CODEPOINTS 256 + +bool mod_cmap_prepare(asc_state_t *state); +bool mod_cmap_main(asc_state_t state); + +#endif