1 package be
.nikiroo
.jvcard
.tui
;
4 import java
.awt
.Graphics
;
6 import java
.awt
.image
.BufferedImage
;
7 import java
.awt
.image
.ImageObserver
;
9 import com
.googlecode
.lanterna
.TerminalSize
;
12 * This class converts an {@link Image} into a textual representation that can
13 * be displayed to the user in a TUI.
18 public class ImageText
{
20 private TerminalSize size
;
22 private boolean ready
;
24 private boolean invert
;
27 * Th rendering modes supported by this {@link ImageText} to convert
28 * {@link Image}s into text.
35 * Use 5 different "colours" which are actually Unicode
36 * {@link Character}s representing
38 * <li>space (blank)</li>
39 * <li>low shade (░)</li>
40 * <li>medium shade (▒)</li>
41 * <li>high shade (▓)</li>
42 * <li>full block (█)</li>
47 * Use "block" Unicode {@link Character}s up to quarter blocks, thus in
48 * effect doubling the resolution both in vertical and horizontal space.
49 * Note that since 2 {@link Character}s next to each other are square,
50 * we will use 4 blocks per 2 blocks for w/h resolution.
54 * Use {@link Character}s from both {@link Mode#DOUBLE_RESOLUTION} and
55 * {@link Mode#DITHERING}.
59 * Only use ASCII {@link Character}s.
65 * Create a new {@link ImageText} with the given parameters. Defaults to
66 * {@link Mode#DOUBLE_DITHERING} and no colour inversion.
69 * the source {@link Image}
71 * the final text size to target
73 public ImageText(Image image
, TerminalSize size
) {
74 this(image
, size
, Mode
.DOUBLE_DITHERING
, false);
78 * Create a new {@link ImageText} with the given parameters.
81 * the source {@link Image}
83 * the final text size to target
85 * the mode of conversion
87 * TRUE to invert colours rendering
89 public ImageText(Image image
, TerminalSize size
, Mode mode
, boolean invert
) {
93 setColorInvert(invert
);
97 * Change the source {@link Image}.
100 * the new {@link Image}
102 public void setImage(Image image
) {
109 * Change the target size of this {@link ImageText}.
114 public void setSize(TerminalSize size
) {
121 * Change the image-to-text mode.
124 * the new {@link Mode}
126 public void setMode(Mode mode
) {
133 * Set the colour-invert mode.
136 * TRUE to inverse the colours
138 public void setColorInvert(boolean invert
) {
139 this.invert
= invert
;
145 * Check if the colours are inverted.
147 * @return TRUE if the colours are inverted
149 public boolean isColorInvert() {
154 * Return the textual representation of the included {@link Image}.
156 * @return the {@link String} representation
158 public String
getText() {
160 if (image
== null || size
== null || size
.getColumns() == 0
161 || size
.getRows() == 0)
165 if (mode
== Mode
.DOUBLE_RESOLUTION
|| mode
== Mode
.DOUBLE_DITHERING
)
168 int w
= size
.getColumns() * mult
;
169 int h
= size
.getRows() * mult
;
171 BufferedImage buff
= new BufferedImage(w
, h
,
172 BufferedImage
.TYPE_INT_ARGB
);
174 Graphics gfx
= buff
.getGraphics();
176 TerminalSize srcSize
= getSize(image
);
177 srcSize
= new TerminalSize(srcSize
.getColumns() * 2,
182 if (srcSize
.getColumns() < srcSize
.getRows()) {
183 double ratio
= (double) size
.getColumns()
184 / (double) size
.getRows();
185 ratio
*= (double) srcSize
.getRows()
186 / (double) srcSize
.getColumns();
188 h
= (int) Math
.round(ratio
* h
);
189 y
= (buff
.getHeight() - h
) / 2;
191 double ratio
= (double) size
.getRows()
192 / (double) size
.getColumns();
193 ratio
*= (double) srcSize
.getColumns()
194 / (double) srcSize
.getRows();
196 w
= (int) Math
.round(ratio
* w
);
197 x
= (buff
.getWidth() - w
) / 2;
200 if (gfx
.drawImage(image
, x
, y
, w
, h
, new ImageObserver() {
202 public boolean imageUpdate(Image img
, int infoflags
, int x
,
203 int y
, int width
, int height
) {
204 ImageText
.this.ready
= true;
214 } catch (InterruptedException e
) {
220 StringBuilder builder
= new StringBuilder();
222 for (int row
= 0; row
< buff
.getHeight(); row
+= mult
) {
224 builder
.append('\n');
226 for (int col
= 0; col
< buff
.getWidth(); col
+= mult
) {
228 if (mode
== Mode
.DITHERING
)
229 builder
.append(getDitheringChar(buff
.getRGB(col
,
233 builder
.append(getAsciiChar(buff
.getRGB(col
, row
)));
234 } else if (mult
== 2) {
235 builder
.append(getBlockChar( //
236 buff
.getRGB(col
, row
),//
237 buff
.getRGB(col
+ 1, row
),//
238 buff
.getRGB(col
, row
+ 1),//
239 buff
.getRGB(col
+ 1, row
+ 1),//
240 mode
== Mode
.DOUBLE_DITHERING
//
246 text
= builder
.toString();
253 public String
toString() {
258 * Return the size of the given {@link Image}.
261 * the image to measure
265 static private TerminalSize
getSize(Image img
) {
266 TerminalSize size
= null;
267 while (size
== null) {
268 int w
= img
.getWidth(null);
269 int h
= img
.getHeight(null);
270 if (w
> -1 && h
> -1) {
271 size
= new TerminalSize(w
, h
);
275 } catch (InterruptedException e
) {
284 * Return the {@link Character} corresponding to this colour in
285 * {@link Mode#ASCII} mode.
290 * @return the {@link Character} to use
292 private char getAsciiChar(int pixel
) {
293 float brigthness
= getBrightness(pixel
);
294 if (brigthness
< 0.20) {
296 } else if (brigthness
< 0.40) {
298 } else if (brigthness
< 0.60) {
300 } else if (brigthness
< 0.80) {
308 * Return the {@link Character} corresponding to this colour in
309 * {@link Mode#DITHERING} mode.
314 * @return the {@link Character} to use
316 private char getDitheringChar(int pixel
) {
317 float brigthness
= getBrightness(pixel
);
318 if (brigthness
< 0.20) {
320 } else if (brigthness
< 0.40) {
322 } else if (brigthness
< 0.60) {
324 } else if (brigthness
< 0.80) {
332 * Return the {@link Character} corresponding to the 4 given colours in
333 * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode.
336 * the upper left colour
338 * the upper right colour
340 * the lower left colour
342 * the lower right colour
344 * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for
345 * {@link Mode#DOUBLE_RESOLUTION}
347 * @return the {@link Character} to use
349 private char getBlockChar(int upperleft
, int upperright
, int lowerleft
,
350 int lowerright
, boolean dithering
) {
352 if (getBrightness(upperleft
) > 0.5f
)
354 if (getBrightness(upperright
) > 0.5f
)
356 if (getBrightness(lowerleft
) > 0.5f
)
358 if (getBrightness(lowerright
) > 0.5f
)
395 avg
+= getBrightness(upperleft
);
396 avg
+= getBrightness(upperright
);
397 avg
+= getBrightness(lowerleft
);
398 avg
+= getBrightness(lowerright
);
403 } else if (avg
< 0.40) {
405 } else if (avg
< 0.60) {
407 } else if (avg
< 0.80) {
421 * Temporary array used so not to create a lot of new ones.
423 private float[] tmp
= new float[4];
426 * Return the brightness value to use from the given ARGB colour.
431 * @return the brightness to sue for computations
433 private float getBrightness(int argb
) {
435 return 1 - rgb2hsb(argb
, tmp
)[2];
436 return rgb2hsb(argb
, tmp
)[2];
440 * Convert the given ARGB colour in HSL/HSB, either into the supplied array
441 * or into a new one if array is NULL.
444 * ARGB pixels are given in 0xAARRGGBB format, while the returned array will
445 * contain Hue, Saturation, Lightness/Brightness, Alpha, in this order. H,
446 * S, L and A are all ranging from 0 to 1 (indeed, H is in 1/360th).
451 * the ARGB colour pixel to convert
453 * the array to convert into or NULL to create a new one
455 * @return the array containing the HSL/HSB converted colour
457 static float[] rgb2hsb(int argb
, float[] array
) {
459 a
= ((argb
& 0xff000000) >> 24);
460 r
= ((argb
& 0x00ff0000) >> 16);
461 g
= ((argb
& 0x0000ff00) >> 8);
462 b
= ((argb
& 0x000000ff));
465 array
= new float[4];
466 Color
.RGBtoHSB(r
, g
, b
, array
);
472 // // other implementation:
475 // a = ((argb & 0xff000000) >> 24) / 255.0f;
476 // r = ((argb & 0x00ff0000) >> 16) / 255.0f;
477 // g = ((argb & 0x0000ff00) >> 8) / 255.0f;
478 // b = ((argb & 0x000000ff)) / 255.0f;
480 // float rgbMin, rgbMax;
481 // rgbMin = Math.min(r, Math.min(g, b));
482 // rgbMax = Math.max(r, Math.max(g, b));
485 // l = (rgbMin + rgbMax) / 2;
488 // if (rgbMin == rgbMax) {
492 // s = (rgbMax - rgbMin) / (rgbMax + rgbMin);
494 // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin);
499 // if (r > g && r > b) {
500 // h = (g - b) / (rgbMax - rgbMin);
501 // } else if (g > b) {
502 // h = 2.0f + (b - r) / (rgbMax - rgbMin);
504 // h = 4.0f + (r - g) / (rgbMax - rgbMin);
506 // h /= 6; // from 0 to 1
508 // return new float[] { h, s, l, a };
510 // // // natural mode:
512 // // int aa = (int) Math.round(100 * a);
513 // // int hh = (int) (360 * h);
516 // // int ss = (int) Math.round(100 * s);
517 // // int ll = (int) Math.round(100 * l);
519 // // return new int[] { hh, ss, ll, aa };