package be.nikiroo.utils.ui; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; /** * This class converts an {@link Image} into a textual representation that can * be displayed to the user in a TUI. * * @author niki */ public class ImageTextAwt { private Image image; private Dimension size; private String text; private boolean ready; private Mode mode; private boolean invert; /** * The rendering modes supported by this {@link ImageTextAwt} to convert * {@link Image}s into text. * * @author niki * */ public enum Mode { /** * Use 5 different "colours" which are actually Unicode * {@link Character}s representing * */ DITHERING, /** * Use "block" Unicode {@link Character}s up to quarter blocks, thus in * effect doubling the resolution both in vertical and horizontal space. * Note that since 2 {@link Character}s next to each other are square, * we will use 4 blocks per 2 blocks for w/h resolution. */ DOUBLE_RESOLUTION, /** * Use {@link Character}s from both {@link Mode#DOUBLE_RESOLUTION} and * {@link Mode#DITHERING}. */ DOUBLE_DITHERING, /** * Only use ASCII {@link Character}s. */ ASCII, } /** * Create a new {@link ImageTextAwt} with the given parameters. Defaults to * {@link Mode#DOUBLE_DITHERING} and no colour inversion. * * @param image * the source {@link Image} * @param size * the final text size to target */ public ImageTextAwt(Image image, Dimension size) { this(image, size, Mode.DOUBLE_DITHERING, false); } /** * Create a new {@link ImageTextAwt} with the given parameters. * * @param image * the source {@link Image} * @param size * the final text size to target * @param mode * the mode of conversion * @param invert * TRUE to invert colours rendering */ public ImageTextAwt(Image image, Dimension size, Mode mode, boolean invert) { setImage(image); setSize(size); setMode(mode); setColorInvert(invert); } /** * Change the source {@link Image}. * * @param image * the new {@link Image} */ public void setImage(Image image) { this.text = null; this.ready = false; this.image = image; } /** * Change the target size of this {@link ImageTextAwt}. * * @param size * the new size */ public void setSize(Dimension size) { this.text = null; this.ready = false; this.size = size; } /** * Change the image-to-text mode. * * @param mode * the new {@link Mode} */ public void setMode(Mode mode) { this.mode = mode; this.text = null; this.ready = false; } /** * Set the colour-invert mode. * * @param invert * TRUE to inverse the colours */ public void setColorInvert(boolean invert) { this.invert = invert; this.text = null; this.ready = false; } /** * Check if the colours are inverted. * * @return TRUE if the colours are inverted */ public boolean isColorInvert() { return invert; } /** * Return the textual representation of the included {@link Image}. * * @return the {@link String} representation */ public String getText() { if (text == null) { if (image == null || size == null || size.width == 0 || size.height == 0) { return ""; } int mult = 1; if (mode == Mode.DOUBLE_RESOLUTION || mode == Mode.DOUBLE_DITHERING) { mult = 2; } Dimension srcSize = getSize(image); srcSize = new Dimension(srcSize.width * 2, srcSize.height); int x = 0; int y = 0; int w = size.width * mult; int h = size.height * mult; // Default = original ratio or original size if none if (w < 0 || h < 0) { if (w < 0 && h < 0) { w = srcSize.width * mult; h = srcSize.height * mult; } else { double ratioSrc = (double) srcSize.width / (double) srcSize.height; if (w < 0) { w = (int) Math.round(h * ratioSrc); } else { h = (int) Math.round(w / ratioSrc); } } } // Fail safe: we consider this to be too much if (w > 1000 || h > 1000) { return "[IMAGE TOO BIG]"; } BufferedImage buff = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics gfx = buff.getGraphics(); double ratioAsked = (double) (w) / (double) (h); double ratioSrc = (double) srcSize.height / (double) srcSize.width; double ratio = ratioAsked * ratioSrc; if (srcSize.width < srcSize.height) { h = (int) Math.round(ratio * h); y = (buff.getHeight() - h) / 2; } else { w = (int) Math.round(w / ratio); x = (buff.getWidth() - w) / 2; } if (gfx.drawImage(image, x, y, w, h, new ImageObserver() { @Override public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { ImageTextAwt.this.ready = true; return true; } })) { ready = true; } while (!ready) { try { Thread.sleep(100); } catch (InterruptedException e) { } } gfx.dispose(); StringBuilder builder = new StringBuilder(); for (int row = 0; row + (mult - 1) < buff.getHeight(); row += mult) { if (row > 0) { builder.append('\n'); } for (int col = 0; col + (mult - 1) < buff.getWidth(); col += mult) { if (mult == 1) { char car = ' '; float brightness = getBrightness(buff.getRGB(col, row)); if (mode == Mode.DITHERING) car = getDitheringChar(brightness, " ░▒▓█"); if (mode == Mode.ASCII) car = getDitheringChar(brightness, " .-+=o8#"); builder.append(car); } else if (mult == 2) { builder.append(getBlockChar( // buff.getRGB(col, row),// buff.getRGB(col + 1, row),// buff.getRGB(col, row + 1),// buff.getRGB(col + 1, row + 1),// mode == Mode.DOUBLE_DITHERING// )); } } } text = builder.toString(); } return text; } @Override public String toString() { return getText(); } /** * Return the size of the given {@link Image}. * * @param img * the image to measure * * @return the size */ static private Dimension getSize(Image img) { Dimension size = null; while (size == null) { int w = img.getWidth(null); int h = img.getHeight(null); if (w > -1 && h > -1) { size = new Dimension(w, h); } else { try { Thread.sleep(100); } catch (InterruptedException e) { } } } return size; } /** * Return the {@link Character} corresponding to the given brightness level * from the evenly-separated given {@link Character}s. * * @param brightness * the brightness level * @param cars * the {@link Character}s to choose from, from less bright to * most bright; MUST contain at least one * {@link Character} * * @return the {@link Character} to use */ private char getDitheringChar(float brightness, String cars) { int index = Math.round(brightness * (cars.length() - 1)); return cars.charAt(index); } /** * Return the {@link Character} corresponding to the 4 given colours in * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode. * * @param upperleft * the upper left colour * @param upperright * the upper right colour * @param lowerleft * the lower left colour * @param lowerright * the lower right colour * @param dithering * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for * {@link Mode#DOUBLE_RESOLUTION} * * @return the {@link Character} to use */ private char getBlockChar(int upperleft, int upperright, int lowerleft, int lowerright, boolean dithering) { int choice = 0; if (getBrightness(upperleft) > 0.5f) { choice += 1; } if (getBrightness(upperright) > 0.5f) { choice += 2; } if (getBrightness(lowerleft) > 0.5f) { choice += 4; } if (getBrightness(lowerright) > 0.5f) { choice += 8; } switch (choice) { case 0: return ' '; case 1: return '▘'; case 2: return '▝'; case 3: return '▀'; case 4: return '▖'; case 5: return '▌'; case 6: return '▞'; case 7: return '▛'; case 8: return '▗'; case 9: return '▚'; case 10: return '▐'; case 11: return '▜'; case 12: return '▄'; case 13: return '▙'; case 14: return '▟'; case 15: if (dithering) { float avg = 0; avg += getBrightness(upperleft); avg += getBrightness(upperright); avg += getBrightness(lowerleft); avg += getBrightness(lowerright); avg /= 4; // Since all the quarters are > 0.5, avg is between 0.5 and 1.0 // So, expand the range of the value avg = (avg - 0.5f) * 2; // Do not use the " " char, as it would make a // "all quarters > 0.5" pixel go black return getDitheringChar(avg, "░▒▓█"); } return '█'; } return ' '; } /** * Temporary array used so not to create a lot of new ones. */ private float[] tmp = new float[4]; /** * Return the brightness value to use from the given ARGB colour. * * @param argb * the argb colour * * @return the brightness to sue for computations */ private float getBrightness(int argb) { if (invert) { return 1 - rgb2hsb(argb, tmp)[2]; } return rgb2hsb(argb, tmp)[2]; } /** * Convert the given ARGB colour in HSL/HSB, either into the supplied array * or into a new one if array is NULL. * *

* ARGB pixels are given in 0xAARRGGBB format, while the returned array will * contain Hue, Saturation, Lightness/Brightness, Alpha, in this order. H, * S, L and A are all ranging from 0 to 1 (indeed, H is in 1/360th). *

* pixel * * @param argb * the ARGB colour pixel to convert * @param array * the array to convert into or NULL to create a new one * * @return the array containing the HSL/HSB converted colour */ static float[] rgb2hsb(int argb, float[] array) { int a, r, g, b; a = ((argb & 0xff000000) >> 24); r = ((argb & 0x00ff0000) >> 16); g = ((argb & 0x0000ff00) >> 8); b = ((argb & 0x000000ff)); if (array == null) { array = new float[4]; } Color.RGBtoHSB(r, g, b, array); array[3] = a; return array; // // other implementation: // // float a, r, g, b; // a = ((argb & 0xff000000) >> 24) / 255.0f; // r = ((argb & 0x00ff0000) >> 16) / 255.0f; // g = ((argb & 0x0000ff00) >> 8) / 255.0f; // b = ((argb & 0x000000ff)) / 255.0f; // // float rgbMin, rgbMax; // rgbMin = Math.min(r, Math.min(g, b)); // rgbMax = Math.max(r, Math.max(g, b)); // // float l; // l = (rgbMin + rgbMax) / 2; // // float s; // if (rgbMin == rgbMax) { // s = 0; // } else { // if (l <= 0.5) { // s = (rgbMax - rgbMin) / (rgbMax + rgbMin); // } else { // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin); // } // } // // float h; // if (r > g && r > b) { // h = (g - b) / (rgbMax - rgbMin); // } else if (g > b) { // h = 2.0f + (b - r) / (rgbMax - rgbMin); // } else { // h = 4.0f + (r - g) / (rgbMax - rgbMin); // } // h /= 6; // from 0 to 1 // // return new float[] { h, s, l, a }; // // // // natural mode: // // // // int aa = (int) Math.round(100 * a); // // int hh = (int) (360 * h); // // if (hh < 0) // // hh += 360; // // int ss = (int) Math.round(100 * s); // // int ll = (int) Math.round(100 * l); // // // // return new int[] { hh, ss, ll, aa }; } }