1 package be
.nikiroo
.utils
.ui
;
4 import java
.awt
.Dimension
;
5 import java
.awt
.Graphics
;
7 import java
.awt
.image
.BufferedImage
;
8 import java
.awt
.image
.ImageObserver
;
11 * This class converts an {@link Image} into a textual representation that can
12 * be displayed to the user in a TUI.
16 public class ImageTextAwt
{
18 private Dimension size
;
20 private boolean ready
;
22 private boolean invert
;
25 * The rendering modes supported by this {@link ImageTextAwt} to convert
26 * {@link Image}s into text.
33 * Use 5 different "colours" which are actually Unicode
34 * {@link Character}s representing
36 * <li>space (blank)</li>
37 * <li>low shade (░)</li>
38 * <li>medium shade (▒)</li>
39 * <li>high shade (▓)</li>
40 * <li>full block (█)</li>
45 * Use "block" Unicode {@link Character}s up to quarter blocks, thus in
46 * effect doubling the resolution both in vertical and horizontal space.
47 * Note that since 2 {@link Character}s next to each other are square,
48 * we will use 4 blocks per 2 blocks for w/h resolution.
52 * Use {@link Character}s from both {@link Mode#DOUBLE_RESOLUTION} and
53 * {@link Mode#DITHERING}.
57 * Only use ASCII {@link Character}s.
63 * Create a new {@link ImageTextAwt} with the given parameters. Defaults to
64 * {@link Mode#DOUBLE_DITHERING} and no colour inversion.
67 * the source {@link Image}
69 * the final text size to target
71 public ImageTextAwt(Image image
, Dimension size
) {
72 this(image
, size
, Mode
.DOUBLE_DITHERING
, false);
76 * Create a new {@link ImageTextAwt} with the given parameters.
79 * the source {@link Image}
81 * the final text size to target
83 * the mode of conversion
85 * TRUE to invert colours rendering
87 public ImageTextAwt(Image image
, Dimension size
, Mode mode
, boolean invert
) {
91 setColorInvert(invert
);
95 * Change the source {@link Image}.
98 * the new {@link Image}
100 public void setImage(Image image
) {
107 * Change the target size of this {@link ImageTextAwt}.
112 public void setSize(Dimension size
) {
119 * Change the image-to-text mode.
122 * the new {@link Mode}
124 public void setMode(Mode mode
) {
131 * Set the colour-invert mode.
134 * TRUE to inverse the colours
136 public void setColorInvert(boolean invert
) {
137 this.invert
= invert
;
143 * Check if the colours are inverted.
145 * @return TRUE if the colours are inverted
147 public boolean isColorInvert() {
152 * Return the textual representation of the included {@link Image}.
154 * @return the {@link String} representation
156 public String
getText() {
158 if (image
== null || size
== null || size
.width
== 0
159 || size
.height
== 0) {
164 if (mode
== Mode
.DOUBLE_RESOLUTION
|| mode
== Mode
.DOUBLE_DITHERING
) {
168 Dimension srcSize
= getSize(image
);
169 srcSize
= new Dimension(srcSize
.width
* 2, srcSize
.height
);
173 int w
= size
.width
* mult
;
174 int h
= size
.height
* mult
;
176 // Default = original ratio or original size if none
177 if (w
< 0 || h
< 0) {
178 if (w
< 0 && h
< 0) {
179 w
= srcSize
.width
* mult
;
180 h
= srcSize
.height
* mult
;
182 double ratioSrc
= (double) srcSize
.width
183 / (double) srcSize
.height
;
185 w
= (int) Math
.round(h
* ratioSrc
);
187 h
= (int) Math
.round(w
/ ratioSrc
);
192 // Fail safe: we consider this to be too much
193 if (w
> 1000 || h
> 1000) {
194 return "[IMAGE TOO BIG]";
197 BufferedImage buff
= new BufferedImage(w
, h
,
198 BufferedImage
.TYPE_INT_ARGB
);
200 Graphics gfx
= buff
.getGraphics();
202 double ratioAsked
= (double) (w
) / (double) (h
);
203 double ratioSrc
= (double) srcSize
.height
/ (double) srcSize
.width
;
204 double ratio
= ratioAsked
* ratioSrc
;
205 if (srcSize
.width
< srcSize
.height
) {
206 h
= (int) Math
.round(ratio
* h
);
207 y
= (buff
.getHeight() - h
) / 2;
209 w
= (int) Math
.round(w
/ ratio
);
210 x
= (buff
.getWidth() - w
) / 2;
213 if (gfx
.drawImage(image
, x
, y
, w
, h
, new ImageObserver() {
215 public boolean imageUpdate(Image img
, int infoflags
, int x
,
216 int y
, int width
, int height
) {
217 ImageTextAwt
.this.ready
= true;
227 } catch (InterruptedException e
) {
233 StringBuilder builder
= new StringBuilder();
235 for (int row
= 0; row
+ (mult
- 1) < buff
.getHeight(); row
+= mult
) {
237 builder
.append('\n');
240 for (int col
= 0; col
+ (mult
- 1) < buff
.getWidth(); col
+= mult
) {
243 float brightness
= getBrightness(buff
.getRGB(col
, row
));
244 if (mode
== Mode
.DITHERING
)
245 car
= getDitheringChar(brightness
, " ░▒▓█");
246 if (mode
== Mode
.ASCII
)
247 car
= getDitheringChar(brightness
, " .-+=o8#");
250 } else if (mult
== 2) {
251 builder
.append(getBlockChar( //
252 buff
.getRGB(col
, row
),//
253 buff
.getRGB(col
+ 1, row
),//
254 buff
.getRGB(col
, row
+ 1),//
255 buff
.getRGB(col
+ 1, row
+ 1),//
256 mode
== Mode
.DOUBLE_DITHERING
//
262 text
= builder
.toString();
269 public String
toString() {
274 * Return the size of the given {@link Image}.
277 * the image to measure
281 static private Dimension
getSize(Image img
) {
282 Dimension size
= null;
283 while (size
== null) {
284 int w
= img
.getWidth(null);
285 int h
= img
.getHeight(null);
286 if (w
> -1 && h
> -1) {
287 size
= new Dimension(w
, h
);
291 } catch (InterruptedException e
) {
300 * Return the {@link Character} corresponding to the given brightness level
301 * from the evenly-separated given {@link Character}s.
304 * the brightness level
306 * the {@link Character}s to choose from, from less bright to
307 * most bright; <b>MUST</b> contain at least one
310 * @return the {@link Character} to use
312 private char getDitheringChar(float brightness
, String cars
) {
313 int index
= Math
.round(brightness
* (cars
.length() - 1));
314 return cars
.charAt(index
);
318 * Return the {@link Character} corresponding to the 4 given colours in
319 * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode.
322 * the upper left colour
324 * the upper right colour
326 * the lower left colour
328 * the lower right colour
330 * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for
331 * {@link Mode#DOUBLE_RESOLUTION}
333 * @return the {@link Character} to use
335 private char getBlockChar(int upperleft
, int upperright
, int lowerleft
,
336 int lowerright
, boolean dithering
) {
339 if (getBrightness(upperleft
) > 0.5f
) {
342 if (getBrightness(upperright
) > 0.5f
) {
345 if (getBrightness(lowerleft
) > 0.5f
) {
348 if (getBrightness(lowerright
) > 0.5f
) {
386 avg
+= getBrightness(upperleft
);
387 avg
+= getBrightness(upperright
);
388 avg
+= getBrightness(lowerleft
);
389 avg
+= getBrightness(lowerright
);
392 // Since all the quarters are > 0.5, avg is between 0.5 and 1.0
393 // So, expand the range of the value
394 avg
= (avg
- 0.5f
) * 2;
396 // Do not use the " " char, as it would make a
397 // "all quarters > 0.5" pixel go black
398 return getDitheringChar(avg
, "░▒▓█");
408 * Temporary array used so not to create a lot of new ones.
410 private float[] tmp
= new float[4];
413 * Return the brightness value to use from the given ARGB colour.
418 * @return the brightness to sue for computations
420 private float getBrightness(int argb
) {
422 return 1 - rgb2hsb(argb
, tmp
)[2];
425 return rgb2hsb(argb
, tmp
)[2];
429 * Convert the given ARGB colour in HSL/HSB, either into the supplied array
430 * or into a new one if array is NULL.
433 * ARGB pixels are given in 0xAARRGGBB format, while the returned array will
434 * contain Hue, Saturation, Lightness/Brightness, Alpha, in this order. H,
435 * S, L and A are all ranging from 0 to 1 (indeed, H is in 1/360th).
440 * the ARGB colour pixel to convert
442 * the array to convert into or NULL to create a new one
444 * @return the array containing the HSL/HSB converted colour
446 static float[] rgb2hsb(int argb
, float[] array
) {
448 a
= ((argb
& 0xff000000) >> 24);
449 r
= ((argb
& 0x00ff0000) >> 16);
450 g
= ((argb
& 0x0000ff00) >> 8);
451 b
= ((argb
& 0x000000ff));
454 array
= new float[4];
457 Color
.RGBtoHSB(r
, g
, b
, array
);
463 // // other implementation:
466 // a = ((argb & 0xff000000) >> 24) / 255.0f;
467 // r = ((argb & 0x00ff0000) >> 16) / 255.0f;
468 // g = ((argb & 0x0000ff00) >> 8) / 255.0f;
469 // b = ((argb & 0x000000ff)) / 255.0f;
471 // float rgbMin, rgbMax;
472 // rgbMin = Math.min(r, Math.min(g, b));
473 // rgbMax = Math.max(r, Math.max(g, b));
476 // l = (rgbMin + rgbMax) / 2;
479 // if (rgbMin == rgbMax) {
483 // s = (rgbMax - rgbMin) / (rgbMax + rgbMin);
485 // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin);
490 // if (r > g && r > b) {
491 // h = (g - b) / (rgbMax - rgbMin);
492 // } else if (g > b) {
493 // h = 2.0f + (b - r) / (rgbMax - rgbMin);
495 // h = 4.0f + (r - g) / (rgbMax - rgbMin);
497 // h /= 6; // from 0 to 1
499 // return new float[] { h, s, l, a };
501 // // // natural mode:
503 // // int aa = (int) Math.round(100 * a);
504 // // int hh = (int) (360 * h);
507 // // int ss = (int) Math.round(100 * s);
508 // // int ll = (int) Math.round(100 * l);
510 // // return new int[] { hh, ss, ll, aa };