Initial checkin
This commit is contained in:
		
							
								
								
									
										301
									
								
								src/main/java/TerminalImageViewer.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								src/main/java/TerminalImageViewer.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,301 @@ | ||||
| import java.awt.Graphics2D; | ||||
| import java.awt.image.BufferedImage; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.net.URL; | ||||
| import java.util.Arrays; | ||||
|  | ||||
| import javax.imageio.ImageIO; | ||||
|  | ||||
| public class TerminalImageViewer { | ||||
|  | ||||
|   /** | ||||
|    * Main method, handles command line arguments and loads and scales images. | ||||
|    */ | ||||
|   public static void main(String[] args) throws IOException { | ||||
|     if (args.length == 0) { | ||||
|       System.out.println("Image file name required. Use -w to set the width in characters (default: 80)."); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     int w = 80 * 4; | ||||
|  | ||||
|     for (int i = 0; i < args.length; i++) { | ||||
|       String name = args[i]; | ||||
|       if (name.equals("-w")) { | ||||
|         w = 4 * Integer.parseInt(args[++i]); | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       BufferedImage original; | ||||
|       if (name.startsWith("http://") || name.startsWith("https://")) { | ||||
|         URL url = new URL(name); | ||||
|         original = ImageIO.read(url); | ||||
|       } else { | ||||
|         original = ImageIO.read(new File(args[0])); | ||||
|       } | ||||
|  | ||||
|       int ow = original.getWidth(); | ||||
|       int oh = original.getHeight(); | ||||
|       int h = oh * w / ow; | ||||
|  | ||||
|       BufferedImage image = original; | ||||
|       if (w != ow) { | ||||
|         image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); | ||||
|         Graphics2D graphics = image.createGraphics(); | ||||
|         graphics.drawImage(original, 0, 0, w, h, null); | ||||
|       } | ||||
|  | ||||
|       ImageData imageData = new ImageData(w, h); | ||||
|       byte[] data = imageData.data; | ||||
|       int[] rgbArray = new int[w]; | ||||
|       for (int y = 0; y < image.getHeight(); y++) { | ||||
|         image.getRGB(0, y, image.getWidth(), 1, rgbArray, 0, w); | ||||
|         int pos = y * w * 4; | ||||
|         for (int x = 0; x < w; x++) { | ||||
|           int rgb = rgbArray[x]; | ||||
|           data[pos++] = (byte) (rgb >> 16); | ||||
|           data[pos++] = (byte) (rgb >> 8); | ||||
|           data[pos++] = (byte) rgb; | ||||
|           pos++; | ||||
|         } | ||||
|       } | ||||
|       System.out.println(imageData.dump()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * ANSI control code helpers | ||||
|    */ | ||||
|   static class Ansi { | ||||
|     public static final String RESET = "\u001b[0m"; | ||||
|  | ||||
|     public static String fgColor(int r, int g, int b) { | ||||
|       return "\u001b[38;2;" + (r & 255) + ";" + (g & 255) + ";" + (b & 255) + "m"; | ||||
|     } | ||||
|  | ||||
|     public static String bgColor(int r, int g, int b) { | ||||
|       return "\u001b[48;2;" + (r & 255) + ";" + (g & 255) + ";" + (b & 255) + "m"; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Converts 4x8 RGB pixel to a unicode character and a foreground and background character. | ||||
|    * Uses a variation of the median cut algorithm to determine a two-color palette for the | ||||
|    * character, then creates a corresponding bitmap for the partial image covered by the | ||||
|    * character and finds the best match in the character bitmap table. | ||||
|    */ | ||||
|   static class BlockChar { | ||||
|  | ||||
|     /** | ||||
|      * Assumed bitmaps of the supported characters | ||||
|      */ | ||||
|     static int[] BITMAPS = new int[] { | ||||
|         0x00000000, ' ', | ||||
|  | ||||
|         0x000ff000, '\u2501', | ||||
|         0x000cc000, '\u2578', | ||||
|         0x00033000, '\u257a', | ||||
|  | ||||
|         0xffff0000, '\u2580',  // upper 1/2 | ||||
|  | ||||
|         0x0000000f, '\u2581',  // lower 1/8 | ||||
|         0x000000ff, '\u2582',  // lower 1/4 | ||||
|         0x00000fff, '\u2583', | ||||
|         0x0000ffff, '\u2584',  // lower 1/2 | ||||
|         0x000fffff, '\u2585', | ||||
|         0x00ffffff, '\u2586',  // lower 3/4 | ||||
|         0x0fffffff, '\u2587', | ||||
|         0xffffffff, '\u2588',  // full | ||||
|  | ||||
|         0xeeeeeeee, '\u258a',  // left 3/4 | ||||
|         0xcccccccc, '\u258c',  // left 1/2 | ||||
|         0x88888888, '\u258e',  // left 1/4 | ||||
|  | ||||
|         0x0000cccc, '\u2596',  // quadrant lower left | ||||
|         0x00003333, '\u2597',  // quadrant lower right | ||||
|         0xcccc0000, '\u2598',  // quadrant upper left | ||||
|         0xccccffff, '\u2599',  // ... | ||||
|         0xcccc3333, '\u259a', | ||||
|         0xffffcccc, '\u259b', | ||||
|         0xffff3333, '\u259c', | ||||
|         0x33330000, '\u259d', | ||||
|         0x3333cccc, '\u259e', | ||||
|         0x3333ffff, '\u259f', | ||||
|  | ||||
|         0x0006ff60, '\u25cf',  // Black circle | ||||
|  | ||||
|         0x000137f0, '\u25e2',  // Triangles | ||||
|         0x0008cef0, '\u25e3', | ||||
|         0x000fec80, '\u25e4', | ||||
|         0x000f7310, '\u25e5' | ||||
|     }; | ||||
|  | ||||
|     /** Minimum value for each color channel. */ | ||||
|     int[] min = new int[3]; | ||||
|  | ||||
|     /**  Maximum value for each color channel. */ | ||||
|     int[] max = new int[3]; | ||||
|  | ||||
|     /** Red, green and blue components of the selected background color. */ | ||||
|     int[] bgColor = new int[3]; | ||||
|  | ||||
|     /** Red, green and blue components of the selected background color. */ | ||||
|     int[] fgColor = new int[3]; | ||||
|  | ||||
|     /** The selected character. */ | ||||
|     char character; | ||||
|  | ||||
|     /** | ||||
|      * Converts a set of pixels to a unicode character and a background and foreground color. | ||||
|      * data contains the rgba values, p0 is the start point in data and scanWidth the number | ||||
|      * of bytes in each row of data. | ||||
|      */ | ||||
|     void load(byte[] data, int p0, int scanWidth) { | ||||
|       Arrays.fill(min, 255); | ||||
|       Arrays.fill(max, 0); | ||||
|       Arrays.fill(bgColor, 0); | ||||
|       Arrays.fill(fgColor, 0); | ||||
|  | ||||
|       // Determine the minimum and maximum value for each color channel | ||||
|       int pos = p0; | ||||
|       for (int y = 0; y < 8; y++) { | ||||
|         for (int x = 0; x < 4; x++) { | ||||
|           for (int i = 0; i < 3; i++) { | ||||
|             int d = data[pos++] & 255; | ||||
|             min[i] = Math.min(min[i], d); | ||||
|             max[i] = Math.max(max[i], d); | ||||
|           } | ||||
|           pos++;  // Alpha | ||||
|         } | ||||
|         pos += scanWidth - 16; | ||||
|       } | ||||
|  | ||||
|       // Determine the color channel with the greatest range. | ||||
|       int splitIndex = 0; | ||||
|       int bestSplit = 0; | ||||
|       for (int i = 0; i < 3; i++) { | ||||
|         if (max[i] - min[i] > bestSplit) { | ||||
|           bestSplit = max[i] - min[i]; | ||||
|           splitIndex = i; | ||||
|         } | ||||
|       } | ||||
|       // We just split at the middle of the interval instead of computing the median. | ||||
|       int splitValue = min[splitIndex] + bestSplit / 2; | ||||
|  | ||||
|       // Compute a bitmap using the given split and sum the color values for both buckets. | ||||
|       int bits = 0; | ||||
|       int fgCount = 0; | ||||
|       int bgCount = 0; | ||||
|  | ||||
|       pos = p0; | ||||
|       for (int y = 0; y < 8; y++) { | ||||
|         for (int x = 0; x < 4; x++) { | ||||
|           bits = bits << 1; | ||||
|           int[] avg; | ||||
|           if ((data[pos + splitIndex] & 255) > splitValue) { | ||||
|             avg = fgColor; | ||||
|             bits |= 1; | ||||
|             fgCount++; | ||||
|           } else { | ||||
|             avg = bgColor; | ||||
|             bgCount++; | ||||
|           } | ||||
|           for (int i = 0; i < 3; i++) { | ||||
|             avg[i] += data[pos++] & 255; | ||||
|           } | ||||
|           pos++;  // Alpha | ||||
|         } | ||||
|         pos += scanWidth - 16; | ||||
|       } | ||||
|  | ||||
|       // Calculate the average color value for each bucket | ||||
|       for (int i = 0; i < 3; i++) { | ||||
|         if (bgCount != 0) { | ||||
|           bgColor[i] /= bgCount; | ||||
|         } | ||||
|         if (fgCount != 0) { | ||||
|           fgColor[i] /= fgCount; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Find the best bitmap match by counting the bits that don't match, including | ||||
|       // the inverted bitmaps. | ||||
|       int bestDiff = Integer.MAX_VALUE; | ||||
|       boolean invert = false; | ||||
|       for (int i = 0; i < BITMAPS.length; i += 2) { | ||||
|         int diff = Integer.bitCount(BITMAPS[i] ^ bits); | ||||
|         if (diff < bestDiff) { | ||||
|           character = (char) BITMAPS[i + 1]; | ||||
|           bestDiff = diff; | ||||
|           invert = false; | ||||
|         } | ||||
|         diff = Integer.bitCount((~BITMAPS[i]) ^ bits); | ||||
|         if (diff < bestDiff) { | ||||
|           character = (char) BITMAPS[i + 1]; | ||||
|           bestDiff = diff; | ||||
|           invert = true; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // If the match is quite bad, use a shade image instead. | ||||
|       if (bestDiff > 12) { | ||||
|         invert = false; | ||||
|         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 (invert) { | ||||
|         int[] tmp = bgColor; | ||||
|         bgColor = fgColor; | ||||
|         fgColor = tmp; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Roughly modeled after the corresponding HTML 5 class. | ||||
|    */ | ||||
|   static class ImageData { | ||||
|     public final int width; | ||||
|     public final int height; | ||||
|     public final byte[] data; | ||||
|  | ||||
|     public ImageData(int width, int height) { | ||||
|       this.width = width; | ||||
|       this.height = height; | ||||
|       this.data = new byte[width * height * 4]; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Convert the image to an Ansi control character string setting the colors | ||||
|      */ | ||||
|     public String dump() { | ||||
|       StringBuilder sb = new StringBuilder(); | ||||
|       BlockChar blockChar = new BlockChar(); | ||||
|  | ||||
|       for (int y = 0; y < height - 7; y += 8) { | ||||
|         int pos = y * width * 4; | ||||
|         String lastFg = ""; | ||||
|         String lastBg = ""; | ||||
|         for (int x = 0; x < width - 3; x += 4) { | ||||
|           blockChar.load(data, pos, width * 4); | ||||
|           String fg = Ansi.fgColor(blockChar.fgColor[0], blockChar.fgColor[1], blockChar.fgColor[2]); | ||||
|           String bg = Ansi.bgColor(blockChar.bgColor[0], blockChar.bgColor[1], blockChar.bgColor[2]); | ||||
|           if (!fg.equals(lastFg)) { | ||||
|             sb.append(fg); | ||||
|             lastFg = fg; | ||||
|           } | ||||
|           if (!bg.equals(lastBg)) { | ||||
|             sb.append(bg); | ||||
|             lastBg = bg; | ||||
|           } | ||||
|           sb.append(blockChar.character); | ||||
|           pos += 16; | ||||
|         } | ||||
|         sb.append(Ansi.RESET).append("\n"); | ||||
|       } | ||||
|       return sb.toString(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Stefan Haustein
					Stefan Haustein