X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fbe%2Fnikiroo%2Fjvcard%2Ftui%2FImageText.java;h=5945b8272898875bacd7a5390c6eec5de29c16b8;hb=a73a906356c971b080c36368e71a15d87e8b8d31;hp=80b675b6fdcbaa46a1c6e9debcb9ace3e0ea7657;hpb=f04d8b1c4c3ed29d4d23cc076f307ef455b2dcb6;p=jvcard.git diff --git a/src/be/nikiroo/jvcard/tui/ImageText.java b/src/be/nikiroo/jvcard/tui/ImageText.java index 80b675b..5945b82 100644 --- a/src/be/nikiroo/jvcard/tui/ImageText.java +++ b/src/be/nikiroo/jvcard/tui/ImageText.java @@ -1,5 +1,6 @@ package be.nikiroo.jvcard.tui; +import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.image.BufferedImage; @@ -7,58 +8,193 @@ import java.awt.image.ImageObserver; import com.googlecode.lanterna.TerminalSize; +/** + * This class converts an {@link Image} into a textual representation that can + * be displayed to the user in a TUI. + * + * @author niki + * + */ public class ImageText { private Image image; private TerminalSize size; private String text; private boolean ready; + private Mode mode; + private boolean invert; + /** + * Th rendering modes supported by this {@link ImageText} 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 ImageText} 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 ImageText(Image image, TerminalSize size) { - setImage(image, size); + this(image, size, Mode.DOUBLE_DITHERING, false); } - public void setImage(Image image) { - setImage(image, size); + /** + * Create a new {@link ImageText} 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 ImageText(Image image, TerminalSize size, Mode mode, boolean invert) { + setImage(image); + setSize(size); + setMode(mode); + setColorInvert(invert); } - public void setImage(TerminalSize size) { - setImage(image, size); + /** + * 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; } - public void setImage(Image image, TerminalSize size) { + /** + * Change the target size of this {@link ImageText}. + * + * @param size + * the new size + */ + public void setSize(TerminalSize size) { this.text = null; this.ready = false; this.size = size; - if (image != null) { - this.image = image; - } } + /** + * 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) + if (image == null || size == null || size.getColumns() == 0 + || size.getRows() == 0) return ""; - int w = size.getColumns() * 2; - int h = size.getRows() * 2; + int mult = 1; + if (mode == Mode.DOUBLE_RESOLUTION || mode == Mode.DOUBLE_DITHERING) + mult = 2; + + int w = size.getColumns() * mult; + int h = size.getRows() * mult; + BufferedImage buff = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics gfx = buff.getGraphics(); TerminalSize srcSize = getSize(image); + srcSize = new TerminalSize(srcSize.getColumns() * 2, + srcSize.getRows()); int x = 0; int y = 0; - if (srcSize.getColumns() > srcSize.getRows()) { - double ratio = (double) srcSize.getRows() + + if (srcSize.getColumns() < srcSize.getRows()) { + double ratio = (double) size.getColumns() + / (double) size.getRows(); + ratio *= (double) srcSize.getRows() / (double) srcSize.getColumns(); + h = (int) Math.round(ratio * h); y = (buff.getHeight() - h) / 2; } else { - double ratio = (double) srcSize.getColumns() + double ratio = (double) size.getRows() + / (double) size.getColumns(); + ratio *= (double) srcSize.getColumns() / (double) srcSize.getRows(); + w = (int) Math.round(ratio * w); x = (buff.getWidth() - w) / 2; - } if (gfx.drawImage(image, x, y, w, h, new ImageObserver() { @@ -81,18 +217,31 @@ public class ImageText { gfx.dispose(); - int[][] square = new int[2][2]; StringBuilder builder = new StringBuilder(); - for (int row = 0; row < buff.getHeight(); row += 2) { + + for (int row = 0; row < buff.getHeight(); row += mult) { if (row > 0) builder.append('\n'); - for (int col = 0; col < buff.getWidth(); col += 2) { - square[0][0] = buff.getRGB(col, row); - square[0][1] = buff.getRGB(col, row + 1); - square[1][0] = buff.getRGB(col + 1, row); - square[1][1] = buff.getRGB(col + 1, row + 1); - builder.append(getChar(square)); + for (int col = 0; col < 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// + )); + } } } @@ -107,6 +256,14 @@ public class ImageText { return getText(); } + /** + * Return the size of the given {@link Image}. + * + * @param img + * the image to measure + * + * @return the size + */ static private TerminalSize getSize(Image img) { TerminalSize size = null; while (size == null) { @@ -125,15 +282,52 @@ public class ImageText { return size; } - static private char getChar(int[][] square) { + /** + * 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 (rgb2hsl(square[0][0])[3] > 50) + if (getBrightness(upperleft) > 0.5f) choice += 1; - if (rgb2hsl(square[0][1])[3] > 50) + if (getBrightness(upperright) > 0.5f) choice += 2; - if (rgb2hsl(square[1][0])[3] > 50) + if (getBrightness(lowerleft) > 0.5f) choice += 4; - if (rgb2hsl(square[1][1])[3] > 50) + if (getBrightness(lowerright) > 0.5f) choice += 8; switch (choice) { @@ -168,54 +362,122 @@ public class ImageText { case 14: return '▟'; case 15: - return '█'; + if (dithering) { + float avg = 0; + avg += getBrightness(upperleft); + avg += getBrightness(upperright); + avg += getBrightness(lowerleft); + avg += getBrightness(lowerright); + avg /= 4; + + return getDitheringChar(avg, " ░▒▓█"); + } else { + return '█'; + } } return ' '; } - // return [a, h, s, l]; a/s/l: 0 to 100%, h = 0 to 359° - static int[] rgb2hsl(int argb) { - double a, r, g, b; - a = ((argb & 0xff000000) >> 24) / 255.0; - r = ((argb & 0x00ff0000) >> 16) / 255.0; - g = ((argb & 0x0000ff00) >> 8) / 255.0; - b = ((argb & 0x000000ff)) / 255.0; - - double rgbMin, rgbMax; - rgbMin = Math.min(r, Math.min(g, b)); - rgbMax = Math.max(r, Math.max(g, b)); - - double l; - l = (rgbMin + rgbMax) / 2; - - double s; - if (rgbMin == rgbMax) { - s = 0; - } else { - if (l <= 0.5) { - s = (rgbMax - rgbMin) / (rgbMax + rgbMin); - } else { - s = (rgbMax - rgbMin) / (2.0 - rgbMax - rgbMin); - } - } + /** + * Temporary array used so not to create a lot of new ones. + */ + private float[] tmp = new float[4]; - double h; - if (r > g && r > b) { - h = (g - b) / (rgbMax - rgbMin); - } else if (g > b) { - h = 2.0 + (b - r) / (rgbMax - rgbMin); - } else { - h = 4.0 + (r - g) / (rgbMax - rgbMin); - } + /** + * 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; - int aa = (int) Math.round(100 * a); - int hh = (int) (60 * h); - if (hh < 0) - hh += 360; - int ss = (int) Math.round(100 * s); - int ll = (int) Math.round(100 * l); + return array; - return new int[] { aa, hh, ss, ll }; + // // 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 }; } }