/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ │vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ ╞══════════════════════════════════════════════════════════════════════════════╡ │ Copyright 2020 Justine Alexandra Roberts Tunney │ │ │ │ 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; version 2 of the License. │ │ │ │ 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., 51 Franklin Street, Fifth Floor, Boston, MA │ │ 02110-1301 USA │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "dsp/tty/itoa8.h" #include "libc/assert.h" #include "libc/calls/calls.h" #include "libc/calls/hefty/spawn.h" #include "libc/calls/ioctl.h" #include "libc/calls/struct/stat.h" #include "libc/calls/termios.h" #include "libc/conv/conv.h" #include "libc/fmt/fmt.h" #include "libc/limits.h" #include "libc/log/check.h" #include "libc/log/log.h" #include "libc/macros.h" #include "libc/math.h" #include "libc/mem/mem.h" #include "libc/nexgen32e/x86feature.h" #include "libc/runtime/gc.h" #include "libc/runtime/runtime.h" #include "libc/stdio/stdio.h" #include "libc/str/str.h" #include "libc/str/tpenc.h" #include "libc/sysv/consts/ex.h" #include "libc/sysv/consts/exit.h" #include "libc/sysv/consts/fileno.h" #include "libc/sysv/consts/madv.h" #include "libc/sysv/consts/map.h" #include "libc/sysv/consts/o.h" #include "libc/sysv/consts/prot.h" #include "libc/x/x.h" #include "third_party/avir/lanczos.h" #include "third_party/getopt/getopt.h" #include "third_party/stb/stb_image.h" #include "third_party/stb/stb_image_resize.h" #define HELPTEXT \ "\n\ NAME\n\ \n\ derasterize - convert pictures to text using unicode ANSI art\n\ \n\ SYNOPSIS\n\ \n\ derasterize [FLAGS] [PNG|JPG|ETC]...\n\ \n\ DESCRIPTION\n\ \n\ This program converts pictures into unicode text and ANSI colors so\n\ that images can be displayed within a terminal. It performs lots of\n\ AVX2 optimized math to deliver the best quality on modern terminals\n\ with 24-bit color support, e.g. Kitty, Gnome Terminal, CMD.EXE, etc\n\ \n\ The default output if fullscreen but can be changed:\n\ -w X\n\ -x X\n\ If X is positive, hardcode the width in tty cells to X\n\ If X is negative, remove as much from the fullscreen width\n\ X may be specified as base 10 decimal, octal, binary, or hex\n\ -h Y\n\ -y Y\n\ If Y is positive, hardcode the height in tty cells to Y\n\ If Y is negative, remove as much from the fullscreen height\n\ May be specified as base 10 decimal, octal, binary, or hex\n\ -m\n\ Use ImageMagick `convert` command to load/scale graphics\n\ -?\n\ -H\n\ Show this help information\n\ \n\ EXAMPLES\n\ \n\ $ ./derasterize.com samples/wave.png > wave.uaart\n\ $ cat wave.uaart\n\ \n\ AUTHORS\n\ \n\ Csdvrx \n\ Justine Tunney \n\ " int m_; /* -m [use imagemagick] */ int x_; /* -x WIDTH [in flexidecimal] */ int y_; /* -y HEIGHT [in flexidecimal] */ #define BEST 0 #define FAST 1 #define FASTER 2 #define MODE BEST #if MODE == BEST #define MC 9u /* log2(#) of color combos to consider */ #define GN 35u /* # of glyphs to consider */ #elif MODE == FAST #define MC 6u #define GN 35u #elif MODE == FASTER #define MC 4u #define GN 25u #endif #define CN 3u /* # channels (rgb) */ #define YS 8u /* row stride -or- block height */ #define XS 4u /* column stride -or- block width */ #define GT 44u /* total glyphs */ #define BN (YS * XS) /* # scalars in block/glyph plane */ #define PHIPRIME 0x9E3779B1u extern const uint32_t kGlyphs[]; extern const char16_t kRunes[]; /*───────────────────────────────────────────────────────────────────────────│─╗ │ derasterize § encoding ─╬─│┼ ╚────────────────────────────────────────────────────────────────────────────│*/ /** * Formats Thompson-Pike variable length integer to array. * * @param p needs at least 8 bytes * @return p + number of bytes written, cf. mempcpy * @note no NUL-terminator is added */ static char *tptoa(char *p, wchar_t x) { unsigned long w; for (w = tpenc(x); w; w >>= 010) *p++ = w & 0xff; return p; } /*───────────────────────────────────────────────────────────────────────────│─╗ │ derasterize § colors ─╬─│┼ ╚────────────────────────────────────────────────────────────────────────────│*/ static float frgb2lin(float x) { float r1, r2; r1 = x / 12.92f; r2 = pow((x + 0.055) / (1 + 0.055), 2.4); return x < 0.04045f ? r1 : r2; } static float frgb2std(float x) { float r1, r2; r1 = x * 12.92f; r2 = 1.055 * pow(x, 1 / 2.4) - 0.055; return x < 0.0031308f ? r1 : r2; } /** * Converts 8-bit RGB samples to floating point. */ static void rgb2float(unsigned n, float *f, const unsigned char *u) { unsigned i; for (i = 0; i < n; ++i) f[i] = u[i]; for (i = 0; i < n; ++i) f[i] /= 255; } /** * Converts floating point RGB samples to 8-bit. */ static void float2rgb(unsigned n, unsigned char *u, float *f) { unsigned i; for (i = 0; i < n; ++i) f[i] *= 256; for (i = 0; i < n; ++i) f[i] = roundf(f[i]); for (i = 0; i < n; ++i) u[i] = MAX(0, MIN(255, f[i])); } /** * Converts standard RGB to linear RGB. * * This makes subtraction look good by flattening out the bias curve * that PC display manufacturers like to use. */ static noinline void rgb2lin(unsigned n, float *f, const unsigned char *u) { unsigned i; rgb2float(n, f, u); for (i = 0; i < n; ++i) f[i] = frgb2lin(f[i]); } /** * Converts linear RGB to standard RGB. */ static noinline void rgb2std(unsigned n, unsigned char *u, float *f) { unsigned i; for (i = 0; i < n; ++i) f[i] = frgb2std(f[i]); float2rgb(n, u, f); } /*───────────────────────────────────────────────────────────────────────────│─╗ │ derasterize § blocks ─╬─│┼ ╚────────────────────────────────────────────────────────────────────────────│*/ struct Cell { char16_t rune; unsigned char bg[CN], fg[CN]; }; /** * Serializes ANSI background, foreground, and UNICODE glyph to wire. */ static char *celltoa(char *p, struct Cell cell) { *p++ = 033; *p++ = '['; *p++ = '4'; *p++ = '8'; *p++ = ';'; *p++ = '2'; *p++ = ';'; p = itoa8(p, cell.bg[0]); *p++ = ';'; p = itoa8(p, cell.bg[1]); *p++ = ';'; p = itoa8(p, cell.bg[2]); *p++ = ';'; *p++ = '3'; *p++ = '8'; *p++ = ';'; *p++ = '2'; *p++ = ';'; p = itoa8(p, cell.fg[0]); *p++ = ';'; p = itoa8(p, cell.fg[1]); *p++ = ';'; p = itoa8(p, cell.fg[2]); *p++ = 'm'; p = tptoa(p, cell.rune); return p; } /** * Picks ≤2**MC unique (bg,fg) pairs from product of lb. */ static unsigned combinecolors(unsigned char bf[1u << MC][2], const unsigned char bl[CN][YS * XS]) { uint64_t hv, ht[(1u << MC) * 2]; unsigned i, j, n, b, f, h, hi, bu, fu; memset(ht, 0, sizeof(ht)); for (n = b = 0; b < BN && n < (1u << MC); ++b) { bu = bl[2][b] << 020 | bl[1][b] << 010 | bl[0][b]; hi = 0; hi = (((bu >> 000) & 0xff) + hi) * PHIPRIME; hi = (((bu >> 010) & 0xff) + hi) * PHIPRIME; hi = (((bu >> 020) & 0xff) + hi) * PHIPRIME; for (f = b + 1; f < BN && n < (1u << MC); ++f) { fu = bl[2][f] << 020 | bl[1][f] << 010 | bl[0][f]; h = hi; h = (((fu >> 000) & 0xff) + h) * PHIPRIME; h = (((fu >> 010) & 0xff) + h) * PHIPRIME; h = (((fu >> 020) & 0xff) + h) * PHIPRIME; h = h & 0xffff; h = MAX(1, h); hv = 0; hv <<= 030; hv |= fu; hv <<= 030; hv |= bu; hv <<= 020; hv |= h; for (i = 0;; ++i) { j = (h + i * (i + 1) / 2) & (ARRAYLEN(ht) - 1); if (!ht[j]) { ht[j] = hv; bf[n][0] = b; bf[n][1] = f; n++; break; } else if (ht[j] == hv) { break; } } } } return n; } /** * Computes distance between synthetic block and actual. */ #define ADJUDICATE(SYMBOL, ARCH) \ ARCH static float SYMBOL(unsigned b, unsigned f, unsigned g, \ const float lb[CN][YS * XS]) { \ unsigned i, k, gu; \ float p[BN], q[BN], fu, bu, r; \ memset(q, 0, sizeof(q)); \ for (k = 0; k < CN; ++k) { \ gu = kGlyphs[g]; \ bu = lb[k][b]; \ fu = lb[k][f]; \ for (i = 0; i < BN; ++i) p[i] = (gu & (1u << i)) ? fu : bu; \ for (i = 0; i < BN; ++i) p[i] -= lb[k][i]; \ for (i = 0; i < BN; ++i) p[i] *= p[i]; \ for (i = 0; i < BN; ++i) q[i] += p[i]; \ } \ r = 0; \ for (i = 0; i < BN; ++i) q[i] = sqrtf(q[i]); \ for (i = 0; i < BN; ++i) r += q[i]; \ return r; \ } ADJUDICATE(adjudicate$avx2, microarchitecture("avx2,fma")) ADJUDICATE(adjudicate$avx, microarchitecture("avx")) ADJUDICATE(adjudicate$default, ) static float (*adjudicate$hook)(unsigned, unsigned, unsigned, const float[CN][YS * XS]); static float adjudicate2(unsigned b, unsigned f, unsigned g, const float lb[CN][YS * XS]) { if (!adjudicate$hook) { if (X86_HAVE(AVX2) && X86_HAVE(FMA)) { adjudicate$hook = adjudicate$avx2; } else if (X86_HAVE(AVX)) { adjudicate$hook = adjudicate$avx; } else { adjudicate$hook = adjudicate$default; } } return adjudicate$hook(b, f, g, lb); } static float adjudicate(unsigned b, unsigned f, unsigned g, const float lb[CN][YS * XS]) { unsigned i, k, gu; float p[BN], q[BN], fu, bu, r; memset(q, 0, sizeof(q)); for (k = 0; k < CN; ++k) { gu = kGlyphs[g]; bu = lb[k][b]; fu = lb[k][f]; for (i = 0; i < BN; ++i) p[i] = (gu & (1u << i)) ? fu : bu; for (i = 0; i < BN; ++i) p[i] -= lb[k][i]; for (i = 0; i < BN; ++i) p[i] *= p[i]; for (i = 0; i < BN; ++i) q[i] += p[i]; } r = 0; for (i = 0; i < BN; ++i) q[i] = sqrtf(q[i]); for (i = 0; i < BN; ++i) r += q[i]; return r; } /** * Converts tiny bitmap graphic into unicode glyph. */ static struct Cell derasterize(unsigned char block[CN][YS * XS]) { struct Cell cell; unsigned i, n, b, f, g; float r, best, lb[CN][YS * XS]; unsigned char bf[1u << MC][2]; rgb2lin(CN * YS * XS, lb[0], block[0]); n = combinecolors(bf, block); best = -1u; cell.rune = 0; for (i = 0; i < n; ++i) { b = bf[i][0]; f = bf[i][1]; for (g = 0; g < GN; ++g) { r = adjudicate(b, f, g, lb); if (r < best) { best = r; cell.rune = kRunes[g]; cell.bg[0] = block[0][b]; cell.bg[1] = block[1][b]; cell.bg[2] = block[2][b]; cell.fg[0] = block[0][f]; cell.fg[1] = block[1][f]; cell.fg[2] = block[2][f]; if (!r) return cell; } } } return cell; } /*───────────────────────────────────────────────────────────────────────────│─╗ │ derasterize § graphics ─╬─│┼ ╚────────────────────────────────────────────────────────────────────────────│*/ /** * Turns packed 8-bit RGB graphic into ANSI UNICODE text. */ static char *RenderImage(char *v, unsigned yn, unsigned xn, const unsigned char srgb[yn][YS][xn][XS][CN]) { unsigned y, x, i, j, k; unsigned char copy[YS][XS][CN] aligned(32); unsigned char block[CN][YS * XS] aligned(32); DCHECK_ALIGNED(32, v); DCHECK_ALIGNED(32, srgb); for (y = 0; y < yn; ++y) { if (y) { *v++ = 033; *v++ = '['; *v++ = '0'; *v++ = 'm'; *v++ = '\n'; } for (x = 0; x < xn; ++x) { for (i = 0; i < YS; ++i) { memcpy(copy[i], srgb[y][i][x], XS * CN); } for (i = 0; i < YS; ++i) { for (j = 0; j < XS; ++j) { for (k = 0; k < CN; ++k) { block[k][i * XS + j] = copy[i][j][k]; } } } v = celltoa(v, derasterize(block)); } } return v; } /*───────────────────────────────────────────────────────────────────────────│─╗ │ derasterize § systems ─╬─│┼ ╚────────────────────────────────────────────────────────────────────────────│*/ static void PrintImage(unsigned yn, unsigned xn, const unsigned char rgb[yn][YS][xn][XS][CN]) { size_t size; char *v, *vt; size = yn * (xn * (32 + (2 + (1 + 3) * 3) * 2 + 1 + 3)) * 1 + 5 + 1; size = ROUNDUP(size, FRAMESIZE); CHECK_NE(MAP_FAILED, (vt = mapanon(size))); v = RenderImage(vt, yn, xn, rgb); *v++ = '\r'; *v++ = 033; *v++ = '['; *v++ = '0'; *v++ = 'm'; CHECK_NE(-1, xwrite(1, vt, v - vt)); CHECK_NE(-1, munmap(vt, size)); } /** * Determines dimensions of teletypewriter. */ static void GetTermSize(unsigned out_rows[1], unsigned out_cols[1]) { struct winsize ws; ws.ws_row = 24; ws.ws_col = 80; if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1) { ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); } out_rows[0] = ws.ws_row; out_cols[0] = ws.ws_col; } static int ReadAll(int fd, void *data, size_t size) { char *p; ssize_t rc; size_t got, n; p = data; n = size; do { if ((rc = read(fd, p, n)) == -1) return -1; assert((got = rc) || !n); p += got; n -= got; } while (n); return 0; } /** * Loads and scales image via ImageMagick `convert` command. * * @param path is filename of graphic * @param yn is desired height * @param xn is desired width * @param rgb is memory allocated by caller for image */ static void LoadFileViaImageMagick(const char *path, unsigned yn, unsigned xn, unsigned char rgb[yn][YS][xn][XS][CN]) { const char *convert; char pathbuf[PATH_MAX]; int pid, ws, fds[3] = {STDIN_FILENO, -1, STDERR_FILENO}; if (!(convert = commandv("convert", pathbuf))) { fputs("error: `convert` command not found\n" "try: apt-get install imagemagick\n", stderr); exit(EXIT_FAILURE); } CHECK_NE(-1, (pid = spawnve( 0, fds, convert, (char *const[]){"convert", path, "-resize", xasprintf("%ux%u!", xn * XS, yn * YS), "-depth", "8", "-colorspace", "sRGB", "rgb:-", NULL}, environ))); CHECK_NE(-1, ReadAll(fds[STDOUT_FILENO], rgb, yn * YS * xn * XS * CN)); CHECK_NE(-1, close(fds[STDOUT_FILENO])); CHECK_NE(-1, waitpid(pid, &ws, 0)); CHECK_EQ(0, WEXITSTATUS(ws)); } static void LoadFile(const char *path, size_t yn, size_t xn, void *rgb) { struct stat st; size_t data2size, data3size; void *map, *data, *data2, *data3; int fd, gotx, goty, channels_in_file; CHECK_NE(-1, (fd = open(path, O_RDONLY)), "%s", path); CHECK_NE(-1, fstat(fd, &st)); CHECK_GT(st.st_size, 0); CHECK_LE(st.st_size, INT_MAX); /* LOGIFNEG1(fadvise(fd, 0, 0, MADV_WILLNEED | MADV_SEQUENTIAL)); */ CHECK_NE(MAP_FAILED, (map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0))); CHECK_NOTNULL((data = stbi_load_from_memory(map, st.st_size, &gotx, &goty, &channels_in_file, CN)), "%s", path); CHECK_NE(-1, munmap(map, st.st_size)); CHECK_NE(-1, close(fd)); #if 1 stbir_resize_uint8(data, gotx, goty, 0, rgb, xn * XS, yn * YS, 0, CN); #else CHECK_EQ(CN, 3); data2size = ROUNDUP(sizeof(float) * goty * gotx * CN, FRAMESIZE); data3size = ROUNDUP(sizeof(float) * yn * YS * xn * XS * CN, FRAMESIZE); CHECK_NE(MAP_FAILED, (data2 = mapanon(data2size))); CHECK_NE(MAP_FAILED, (data3 = mapanon(data3size))); rgb2lin(goty * gotx * CN, data2, data); lanczos3(yn * YS, xn * XS, data3, goty, gotx, data2, gotx * 3); rgb2std(yn * YS * xn * XS * CN, rgb, data3); CHECK_NE(-1, munmap(data2, data2size)); CHECK_NE(-1, munmap(data3, data3size)); #endif free(data); } static int ParseNumberOption(const char *arg) { long x; x = strtol(arg, NULL, 0); if (!(1 <= x && x <= INT_MAX)) { fprintf(stderr, "invalid flexidecimal: %s\n\n", arg); exit(EXIT_FAILURE); } return x; } static void PrintUsage(int rc, FILE *f) { fputs(HELPTEXT, f); exit(rc); } static void GetOpts(int argc, char *argv[]) { int opt; while ((opt = getopt(argc, argv, "?Hmx:y:w:h:")) != -1) { switch (opt) { case 'w': case 'x': x_ = ParseNumberOption(optarg); break; case 'h': case 'y': y_ = ParseNumberOption(optarg); break; case 'm': m_ = 1; break; case '?': case 'H': PrintUsage(EXIT_SUCCESS, stdout); default: PrintUsage(EX_USAGE, stderr); } } } int main(int argc, char *argv[]) { int i; void *rgb; size_t size; char *option; unsigned yd, xd; __fast_math(); showcrashreports(); GetOpts(argc, argv); // if sizes are given, 2 cases: // - positive values: use that as the target size // - negative values: add, for ex to offset the command prompt size GetTermSize(&yd, &xd); if (y_ <= 0) { y_ += yd; } if (x_ <= 0) { x_ += xd; } // FIXME: on the conversion stage should do 2Y because of halfblocks // printf( "filename >%s<\tx >%d<\ty >%d<\n\n", filename, x_, y_); size = y_ * YS * x_ * XS * CN; CHECK_NE(MAP_FAILED, (rgb = mapanon(ROUNDUP(size, FRAMESIZE)))); for (i = optind; i < argc; ++i) { if (!argv[i]) continue; if (m_) { LoadFileViaImageMagick(argv[i], y_, x_, rgb); } else { LoadFile(argv[i], y_, x_, rgb); } PrintImage(y_, x_, rgb); } munmap(rgb, ROUNDUP(size, FRAMESIZE)); return 0; }