This commit is contained in:
stefan.haustein@gmail.com 2017-07-11 21:26:10 +02:00
commit d543b3b2c9
2 changed files with 86 additions and 78 deletions

View file

@ -1,6 +1,6 @@
# TerminalImageViewer # TerminalImageViewer
Small Java program to display images in a (modern) terminal using RGB ANSI codes and unicode block graphic characters Small Java\* program to display images in a (modern) terminal using RGB ANSI codes and unicode block graphic characters.
Algorithm (for each 4x8 pixel cell mapped to a unicode block graphics character): Algorithm (for each 4x8 pixel cell mapped to a unicode block graphics character):
@ -9,8 +9,9 @@ Algorithm (for each 4x8 pixel cell mapped to a unicode block graphics character)
3. Average the colors above and below and create a corresponding bitmap for the cell 3. Average the colors above and below and create a corresponding bitmap for the cell
3. Compare the bitmap to the assumed bitmaps for various unicode block graphics characters 3. Compare the bitmap to the assumed bitmaps for various unicode block graphics characters
\*) **C++ port** available at at https://github.com/stefanhaustein/tiv
Usage: ## Usage
``` ```
javac TerminalImageViewer.java javac TerminalImageViewer.java
@ -19,6 +20,13 @@ java TerminalImageViewer [-w <width-in-characters>] <image-filename-or-url>
``` ```
## Common problems
- If you see strange horizontal lines, the characters don't fully fill the character cell. Remove additional line spacing in your terminal app
- Wrong colors? Try -256 to use a 256 color palette instead of 24 bit colors or -grayscale for grayscale.
## Examples
![Examples](http://i.imgur.com/8UyGjg8.png) ![Examples](http://i.imgur.com/8UyGjg8.png)
If multiple images match the filename spec, thumbnails are shown. If multiple images match the filename spec, thumbnails are shown.

View file

@ -32,7 +32,7 @@ public class TerminalImageViewer {
"Image file name required.\n\n" + "Image file name required.\n\n" +
" - Use -w and -h to set the maximum width and height in characters (defaults: 80, 24).\n" + " - Use -w and -h to set the maximum width and height in characters (defaults: 80, 24).\n" +
" - Use -256 for 256 color mode, -grayscale for grayscale and -stdin to obtain file names from stdin.\n" + " - Use -256 for 256 color mode, -grayscale for grayscale and -stdin to obtain file names from stdin.\n" +
" - When multiple files are supplied, -c sets the number of images per row (default: 4)."); " - When multiple files are supplied, -c sets the number of images per row (default: 4).");
return; return;
} }
@ -76,7 +76,7 @@ public class TerminalImageViewer {
} else if (start == args.length - 1 && (isUrl(args[start]) || !new File(args[start]).isDirectory())) { } else if (start == args.length - 1 && (isUrl(args[start]) || !new File(args[start]).isDirectory())) {
convert(args[start], maxWidth, maxHeight); convert(args[start], maxWidth, maxHeight);
} else { } else {
// Directory-style rendering. // Directory-style rendering.
int index = 0; int index = 0;
int cw = (maxWidth - 2 * (columns - 1) * 4) / (4 * columns); int cw = (maxWidth - 2 * (columns - 1) * 4) / (4 * columns);
int tw = cw * 4; int tw = cw * 4;
@ -102,7 +102,7 @@ public class TerminalImageViewer {
sb.setLength(sl - 2); sb.setLength(sl - 2);
sb.append(" "); sb.append(" ");
} catch (Exception e) { } catch (Exception e) {
// Probably no image; ignore. // Probably no image; ignore.
} }
} }
dump(image, mode); dump(image, mode);
@ -165,7 +165,7 @@ public class TerminalImageViewer {
/** /**
* ANSI control code helpers * ANSI control code helpers
*/ */
static class Ansi { static class Ansi {
public static final String RESET = "\u001b[0m"; public static final String RESET = "\u001b[0m";
@ -182,7 +182,7 @@ public class TerminalImageViewer {
int index = Arrays.binarySearch(options, v); int index = Arrays.binarySearch(options, v);
if (index < 0) { if (index < 0) {
index = -index - 1; index = -index - 1;
// need to check [index] and [index - 1] // need to check [index] and [index - 1]
if (index == options.length) { if (index == options.length) {
index = options.length - 1; index = options.length - 1;
} else if (index > 0) { } else if (index > 0) {
@ -232,7 +232,7 @@ public class TerminalImageViewer {
0.3 * sqr(grayQ-r) + 0.59 * sqr(grayQ-g) + 0.11 * sqr(grayQ-b)) { 0.3 * sqr(grayQ-r) + 0.59 * sqr(grayQ-g) + 0.11 * sqr(grayQ-b)) {
colorIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx; colorIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx;
} else { } else {
colorIndex = 232 + grayIdx; // 1..24 -> 232..255 colorIndex = 232 + grayIdx; // 1..24 -> 232..255
} }
return (bg ? "\u001B[48;5;" : "\u001B[38;5;") + colorIndex + "m"; return (bg ? "\u001B[48;5;" : "\u001B[38;5;") + colorIndex + "m";
} }
@ -252,94 +252,94 @@ public class TerminalImageViewer {
static int[] BITMAPS = new int[] { static int[] BITMAPS = new int[] {
0x00000000, '\u00a0', 0x00000000, '\u00a0',
// Block graphics // Block graphics
// 0xffff0000, '\u2580', // upper 1/2; redundant with inverse lower 1/2 // 0xffff0000, '\u2580', // upper 1/2; redundant with inverse lower 1/2
0x0000000f, '\u2581', // lower 1/8 0x0000000f, '\u2581', // lower 1/8
0x000000ff, '\u2582', // lower 1/4 0x000000ff, '\u2582', // lower 1/4
0x00000fff, '\u2583', 0x00000fff, '\u2583',
0x0000ffff, '\u2584', // lower 1/2 0x0000ffff, '\u2584', // lower 1/2
0x000fffff, '\u2585', 0x000fffff, '\u2585',
0x00ffffff, '\u2586', // lower 3/4 0x00ffffff, '\u2586', // lower 3/4
0x0fffffff, '\u2587', 0x0fffffff, '\u2587',
// 0xffffffff, '\u2588', // full; redundant with inverse space // 0xffffffff, '\u2588', // full; redundant with inverse space
0xeeeeeeee, '\u258a', // left 3/4 0xeeeeeeee, '\u258a', // left 3/4
0xcccccccc, '\u258c', // left 1/2 0xcccccccc, '\u258c', // left 1/2
0x88888888, '\u258e', // left 1/4 0x88888888, '\u258e', // left 1/4
0x0000cccc, '\u2596', // quadrant lower left 0x0000cccc, '\u2596', // quadrant lower left
0x00003333, '\u2597', // quadrant lower right 0x00003333, '\u2597', // quadrant lower right
0xcccc0000, '\u2598', // quadrant upper left 0xcccc0000, '\u2598', // quadrant upper left
// 0xccccffff, '\u2599', // 3/4 redundant with inverse 1/4 // 0xccccffff, '\u2599', // 3/4 redundant with inverse 1/4
0xcccc3333, '\u259a', // diagonal 1/2 0xcccc3333, '\u259a', // diagonal 1/2
// 0xffffcccc, '\u259b', // 3/4 redundant // 0xffffcccc, '\u259b', // 3/4 redundant
// 0xffff3333, '\u259c', // 3/4 redundant // 0xffff3333, '\u259c', // 3/4 redundant
0x33330000, '\u259d', // quadrant upper right 0x33330000, '\u259d', // quadrant upper right
// 0x3333cccc, '\u259e', // 3/4 redundant // 0x3333cccc, '\u259e', // 3/4 redundant
// 0x3333ffff, '\u259f', // 3/4 redundant // 0x3333ffff, '\u259f', // 3/4 redundant
// Line drawing subset: no double lines, no complex light lines // Line drawing subset: no double lines, no complex light lines
// Simple light lines duplicated because there is no center pixel int the 4x8 matrix // Simple light lines duplicated because there is no center pixel int the 4x8 matrix
0x000ff000, '\u2501', // Heavy horizontal 0x000ff000, '\u2501', // Heavy horizontal
0x66666666, '\u2503', // Heavy vertical 0x66666666, '\u2503', // Heavy vertical
0x00077666, '\u250f', // Heavy down and right 0x00077666, '\u250f', // Heavy down and right
0x000ee666, '\u2513', // Heavy down and left 0x000ee666, '\u2513', // Heavy down and left
0x66677000, '\u2517', // Heavy up and right 0x66677000, '\u2517', // Heavy up and right
0x666ee000, '\u251b', // Heavy up and left 0x666ee000, '\u251b', // Heavy up and left
0x66677666, '\u2523', // Heavy vertical and right 0x66677666, '\u2523', // Heavy vertical and right
0x666ee666, '\u252b', // Heavy vertical and left 0x666ee666, '\u252b', // Heavy vertical and left
0x000ff666, '\u2533', // Heavy down and horizontal 0x000ff666, '\u2533', // Heavy down and horizontal
0x666ff000, '\u253b', // Heavy up and horizontal 0x666ff000, '\u253b', // Heavy up and horizontal
0x666ff666, '\u254b', // Heavy cross 0x666ff666, '\u254b', // Heavy cross
0x000cc000, '\u2578', // Bold horizontal left 0x000cc000, '\u2578', // Bold horizontal left
0x00066000, '\u2579', // Bold horizontal up 0x00066000, '\u2579', // Bold horizontal up
0x00033000, '\u257a', // Bold horizontal right 0x00033000, '\u257a', // Bold horizontal right
0x00066000, '\u257b', // Bold horizontal down 0x00066000, '\u257b', // Bold horizontal down
0x06600660, '\u254f', // Heavy double dash vertical 0x06600660, '\u254f', // Heavy double dash vertical
0x000f0000, '\u2500', // Light horizontal 0x000f0000, '\u2500', // Light horizontal
0x0000f000, '\u2500', // 0x0000f000, '\u2500', //
0x44444444, '\u2502', // Light vertical 0x44444444, '\u2502', // Light vertical
0x22222222, '\u2502', 0x22222222, '\u2502',
0x000e0000, '\u2574', // light left 0x000e0000, '\u2574', // light left
0x0000e000, '\u2574', // light left 0x0000e000, '\u2574', // light left
0x44440000, '\u2575', // light up 0x44440000, '\u2575', // light up
0x22220000, '\u2575', // light up 0x22220000, '\u2575', // light up
0x00030000, '\u2576', // light right 0x00030000, '\u2576', // light right
0x00003000, '\u2576', // light right 0x00003000, '\u2576', // light right
0x00004444, '\u2575', // light down 0x00004444, '\u2575', // light down
0x00002222, '\u2575', // light down 0x00002222, '\u2575', // light down
// Misc technical // Misc technical
0x44444444, '\u23a2', // [ extension 0x44444444, '\u23a2', // [ extension
0x22222222, '\u23a5', // ] extension 0x22222222, '\u23a5', // ] extension
//12345678 //12345678
0x0f000000, '\u23ba', // Horizontal scanline 1 0x0f000000, '\u23ba', // Horizontal scanline 1
0x00f00000, '\u23bb', // Horizontal scanline 3 0x00f00000, '\u23bb', // Horizontal scanline 3
0x00000f00, '\u23bc', // Horizontal scanline 7 0x00000f00, '\u23bc', // Horizontal scanline 7
0x000000f0, '\u23bd', // Horizontal scanline 9 0x000000f0, '\u23bd', // Horizontal scanline 9
// Geometrical shapes. Tricky because some of them are too wide. // Geometrical shapes. Tricky because some of them are too wide.
// 0x00ffff00, '\u25fe', // Black medium small square // 0x00ffff00, '\u25fe', // Black medium small square
0x00066000, '\u25aa', // Black small square 0x00066000, '\u25aa', // Black small square
/* /*
0x11224488, '\u2571', // diagonals 0x11224488, '\u2571', // diagonals
0x88442211, '\u2572', 0x88442211, '\u2572',
0x99666699, '\u2573', 0x99666699, '\u2573',
0x000137f0, '\u25e2', // Triangles 0x000137f0, '\u25e2', // Triangles
0x0008cef0, '\u25e3', 0x0008cef0, '\u25e3',
0x000fec80, '\u25e4', 0x000fec80, '\u25e4',
0x000f7310, '\u25e5' 0x000f7310, '\u25e5'
@ -372,7 +372,7 @@ public class TerminalImageViewer {
Arrays.fill(bgColor, 0); Arrays.fill(bgColor, 0);
Arrays.fill(fgColor, 0); Arrays.fill(fgColor, 0);
// Determine the minimum and maximum value for each color channel // Determine the minimum and maximum value for each color channel
int pos = p0; int pos = p0;
for (int y = 0; y < 8; y++) { for (int y = 0; y < 8; y++) {
for (int x = 0; x < 4; x++) { for (int x = 0; x < 4; x++) {
@ -381,12 +381,12 @@ public class TerminalImageViewer {
min[i] = Math.min(min[i], d); min[i] = Math.min(min[i], d);
max[i] = Math.max(max[i], d); max[i] = Math.max(max[i], d);
} }
pos++; // Alpha pos++; // Alpha
} }
pos += scanWidth - 16; pos += scanWidth - 16;
} }
// Determine the color channel with the greatest range. // Determine the color channel with the greatest range.
int splitIndex = 0; int splitIndex = 0;
int bestSplit = 0; int bestSplit = 0;
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
@ -395,10 +395,10 @@ public class TerminalImageViewer {
splitIndex = i; splitIndex = i;
} }
} }
// We just split at the middle of the interval instead of computing the median. // We just split at the middle of the interval instead of computing the median.
int splitValue = min[splitIndex] + bestSplit / 2; int splitValue = min[splitIndex] + bestSplit / 2;
// Compute a bitmap using the given split and sum the color values for both buckets. // Compute a bitmap using the given split and sum the color values for both buckets.
int bits = 0; int bits = 0;
int fgCount = 0; int fgCount = 0;
int bgCount = 0; int bgCount = 0;
@ -419,12 +419,12 @@ public class TerminalImageViewer {
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
avg[i] += data[pos++] & 255; avg[i] += data[pos++] & 255;
} }
pos++; // Alpha pos++; // Alpha
} }
pos += scanWidth - 16; pos += scanWidth - 16;
} }
// Calculate the average color value for each bucket // Calculate the average color value for each bucket
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
if (bgCount != 0) { if (bgCount != 0) {
bgColor[i] /= bgCount; bgColor[i] /= bgCount;
@ -434,8 +434,8 @@ public class TerminalImageViewer {
} }
} }
// Find the best bitmap match by counting the bits that don't match, including // Find the best bitmap match by counting the bits that don't match, including
// the inverted bitmaps. // the inverted bitmaps.
int bestDiff = Integer.MAX_VALUE; int bestDiff = Integer.MAX_VALUE;
boolean invert = false; boolean invert = false;
for (int i = 0; i < BITMAPS.length; i += 2) { for (int i = 0; i < BITMAPS.length; i += 2) {
@ -453,13 +453,13 @@ public class TerminalImageViewer {
} }
} }
// If the match is quite bad, use a shade image instead. // If the match is quite bad, use a shade image instead.
if (bestDiff > 10) { if (bestDiff > 10) {
invert = false; invert = false;
character = " \u2591\u2592\u2593\u2588".charAt(Math.min(4, fgCount * 5 / 32)); character = " \u2591\u2592\u2593\u2588".charAt(Math.min(4, fgCount * 5 / 32));
} }
// If we use an inverted character, we need to swap the colors. // If we use an inverted character, we need to swap the colors.
if (invert) { if (invert) {
int[] tmp = bgColor; int[] tmp = bgColor;
bgColor = fgColor; bgColor = fgColor;