|
| 1 | +package terminal; |
| 2 | + |
| 3 | +import javax.imageio.ImageIO; |
| 4 | +import java.awt.*; |
| 5 | +import java.awt.image.BufferedImage; |
| 6 | +import java.io.File; |
| 7 | + |
| 8 | +/** |
| 9 | + * TerminalImageRenderer |
| 10 | + * |
| 11 | + * Renders a JPEG/PNG/BMP image to a GNOME terminal using |
| 12 | + * Unicode half-block characters and 24-bit ANSI TrueColor. |
| 13 | + * |
| 14 | + * Usage: |
| 15 | + * javac TerminalImageRenderer.java |
| 16 | + * java TerminalImageRenderer path/to/image.png [width] |
| 17 | + * |
| 18 | + * If width is omitted, defaults to 80 characters. |
| 19 | + */ |
| 20 | +public class TerminalImageRenderer |
| 21 | +{ |
| 22 | + |
| 23 | + // ANSI escape sequences |
| 24 | + private static final String ESC = "\u001b["; |
| 25 | + private static final String RESET = ESC + "0m"; |
| 26 | + |
| 27 | + public static void main(String[] args) throws Exception |
| 28 | + { |
| 29 | + if (args.length < 1) { |
| 30 | + System.err.println("Usage: java TerminalImageRenderer <image> [width]"); |
| 31 | + System.exit(1); |
| 32 | + } |
| 33 | + |
| 34 | + String path = args[0]; |
| 35 | + int targetWidth = (args.length >= 2) ? Integer.parseInt(args[1]) : 80; |
| 36 | + |
| 37 | + BufferedImage img = ImageIO.read(new File(path)); |
| 38 | + |
| 39 | + if (img == null) { |
| 40 | + System.err.println("Could not read image: " + path); |
| 41 | + System.exit(1); |
| 42 | + } |
| 43 | + |
| 44 | + BufferedImage scaled = scaleToWidth(img, targetWidth); |
| 45 | + renderHalfBlocksTrueColor(scaled); |
| 46 | + System.out.print(RESET); |
| 47 | + } |
| 48 | + |
| 49 | + /** |
| 50 | + * Scale image to target terminal width, preserving aspect ratio. |
| 51 | + * Height is scaled accordingly; we later compress two vertical pixels |
| 52 | + * into one terminal row using half-block characters. |
| 53 | + */ |
| 54 | + private static BufferedImage scaleToWidth(BufferedImage src, int targetWidth) |
| 55 | + { |
| 56 | + int srcWidth = src.getWidth(); |
| 57 | + int srcHeight = src.getHeight(); |
| 58 | + |
| 59 | + if (srcWidth <= targetWidth) { |
| 60 | + return src; // no scaling needed |
| 61 | + } |
| 62 | + |
| 63 | + double scale = (double) targetWidth / srcWidth; |
| 64 | + int newWidth = targetWidth; |
| 65 | + int newHeight = (int) Math.round(srcHeight * scale); |
| 66 | + |
| 67 | + BufferedImage dst = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); |
| 68 | + Graphics2D g = dst.createGraphics(); |
| 69 | + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); |
| 70 | + g.drawImage(src, 0, 0, newWidth, newHeight, null); |
| 71 | + g.dispose(); |
| 72 | + return dst; |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Render using Unicode upper half-block (▀) with foreground color |
| 77 | + * for the top pixel and background color for the bottom pixel. |
| 78 | + * |
| 79 | + * Each terminal cell represents two vertical pixels: |
| 80 | + * - top pixel -> ANSI 38;2;r;g;b (foreground) |
| 81 | + * - bottom pixel -> ANSI 48;2;r;g;b (background) |
| 82 | + */ |
| 83 | + private static void renderHalfBlocksTrueColor(BufferedImage img) |
| 84 | + { |
| 85 | + int width = img.getWidth(); |
| 86 | + int height = img.getHeight(); |
| 87 | + |
| 88 | + // Ensure even height for pairing top/bottom pixels |
| 89 | + if (height % 2 != 0) { |
| 90 | + height -= 1; |
| 91 | + } |
| 92 | + |
| 93 | + StringBuilder sb = new StringBuilder(); |
| 94 | + |
| 95 | + for (int y = 0; y < height; y += 2) { |
| 96 | + for (int x = 0; x < width; x++) { |
| 97 | + int topRGB = img.getRGB(x, y); |
| 98 | + int bottomRGB = img.getRGB(x, y + 1); |
| 99 | + |
| 100 | + Color top = new Color(topRGB); |
| 101 | + Color bottom = new Color(bottomRGB); |
| 102 | + |
| 103 | + // ANSI TrueColor: foreground (top), background (bottom) |
| 104 | + sb.append(ESC) |
| 105 | + .append("38;2;") |
| 106 | + .append(top.getRed()).append(";") |
| 107 | + .append(top.getGreen()).append(";") |
| 108 | + .append(top.getBlue()).append("m"); |
| 109 | + |
| 110 | + sb.append(ESC) |
| 111 | + .append("48;2;") |
| 112 | + .append(bottom.getRed()).append(";") |
| 113 | + .append(bottom.getGreen()).append(";") |
| 114 | + .append(bottom.getBlue()).append("m"); |
| 115 | + |
| 116 | + // Upper half block character |
| 117 | + sb.append('▀'); |
| 118 | + } |
| 119 | + sb.append(RESET).append('\n'); |
| 120 | + } |
| 121 | + |
| 122 | + System.out.print(sb.toString()); |
| 123 | + } |
| 124 | +} |
0 commit comments