new StringUtils: formatNumber
[fanfix.git] / src / be / nikiroo / utils / ui / ImageTextAwt.java
1 package be.nikiroo.utils.ui;
2
3 import java.awt.Color;
4 import java.awt.Dimension;
5 import java.awt.Graphics;
6 import java.awt.Image;
7 import java.awt.image.BufferedImage;
8 import java.awt.image.ImageObserver;
9
10 /**
11 * This class converts an {@link Image} into a textual representation that can
12 * be displayed to the user in a TUI.
13 *
14 * @author niki
15 */
16 public class ImageTextAwt {
17 private Image image;
18 private Dimension size;
19 private String text;
20 private boolean ready;
21 private Mode mode;
22 private boolean invert;
23
24 /**
25 * The rendering modes supported by this {@link ImageTextAwt} to convert
26 * {@link Image}s into text.
27 *
28 * @author niki
29 *
30 */
31 public enum Mode {
32 /**
33 * Use 5 different "colours" which are actually Unicode
34 * {@link Character}s representing
35 * <ul>
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>
41 * </ul>
42 */
43 DITHERING,
44 /**
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.
49 */
50 DOUBLE_RESOLUTION,
51 /**
52 * Use {@link Character}s from both {@link Mode#DOUBLE_RESOLUTION} and
53 * {@link Mode#DITHERING}.
54 */
55 DOUBLE_DITHERING,
56 /**
57 * Only use ASCII {@link Character}s.
58 */
59 ASCII,
60 }
61
62 /**
63 * Create a new {@link ImageTextAwt} with the given parameters. Defaults to
64 * {@link Mode#DOUBLE_DITHERING} and no colour inversion.
65 *
66 * @param image
67 * the source {@link Image}
68 * @param size
69 * the final text size to target
70 */
71 public ImageTextAwt(Image image, Dimension size) {
72 this(image, size, Mode.DOUBLE_DITHERING, false);
73 }
74
75 /**
76 * Create a new {@link ImageTextAwt} with the given parameters.
77 *
78 * @param image
79 * the source {@link Image}
80 * @param size
81 * the final text size to target
82 * @param mode
83 * the mode of conversion
84 * @param invert
85 * TRUE to invert colours rendering
86 */
87 public ImageTextAwt(Image image, Dimension size, Mode mode, boolean invert) {
88 setImage(image);
89 setSize(size);
90 setMode(mode);
91 setColorInvert(invert);
92 }
93
94 /**
95 * Change the source {@link Image}.
96 *
97 * @param image
98 * the new {@link Image}
99 */
100 public void setImage(Image image) {
101 this.text = null;
102 this.ready = false;
103 this.image = image;
104 }
105
106 /**
107 * Change the target size of this {@link ImageTextAwt}.
108 *
109 * @param size
110 * the new size
111 */
112 public void setSize(Dimension size) {
113 this.text = null;
114 this.ready = false;
115 this.size = size;
116 }
117
118 /**
119 * Change the image-to-text mode.
120 *
121 * @param mode
122 * the new {@link Mode}
123 */
124 public void setMode(Mode mode) {
125 this.mode = mode;
126 this.text = null;
127 this.ready = false;
128 }
129
130 /**
131 * Set the colour-invert mode.
132 *
133 * @param invert
134 * TRUE to inverse the colours
135 */
136 public void setColorInvert(boolean invert) {
137 this.invert = invert;
138 this.text = null;
139 this.ready = false;
140 }
141
142 /**
143 * Check if the colours are inverted.
144 *
145 * @return TRUE if the colours are inverted
146 */
147 public boolean isColorInvert() {
148 return invert;
149 }
150
151 /**
152 * Return the textual representation of the included {@link Image}.
153 *
154 * @return the {@link String} representation
155 */
156 public String getText() {
157 if (text == null) {
158 if (image == null || size == null || size.width == 0
159 || size.height == 0) {
160 return "";
161 }
162
163 int mult = 1;
164 if (mode == Mode.DOUBLE_RESOLUTION || mode == Mode.DOUBLE_DITHERING) {
165 mult = 2;
166 }
167
168 Dimension srcSize = getSize(image);
169 srcSize = new Dimension(srcSize.width * 2, srcSize.height);
170 int x = 0;
171 int y = 0;
172
173 int w = size.width * mult;
174 int h = size.height * mult;
175
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;
181 } else {
182 double ratioSrc = (double) srcSize.width
183 / (double) srcSize.height;
184 if (w < 0) {
185 w = (int) Math.round(h * ratioSrc);
186 } else {
187 h = (int) Math.round(w / ratioSrc);
188 }
189 }
190 }
191
192 // Fail safe: we consider this to be too much
193 if (w > 1000 || h > 1000) {
194 return "[IMAGE TOO BIG]";
195 }
196
197 BufferedImage buff = new BufferedImage(w, h,
198 BufferedImage.TYPE_INT_ARGB);
199
200 Graphics gfx = buff.getGraphics();
201
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;
208 } else {
209 w = (int) Math.round(w / ratio);
210 x = (buff.getWidth() - w) / 2;
211 }
212
213 if (gfx.drawImage(image, x, y, w, h, new ImageObserver() {
214 @Override
215 public boolean imageUpdate(Image img, int infoflags, int x,
216 int y, int width, int height) {
217 ImageTextAwt.this.ready = true;
218 return true;
219 }
220 })) {
221 ready = true;
222 }
223
224 while (!ready) {
225 try {
226 Thread.sleep(100);
227 } catch (InterruptedException e) {
228 }
229 }
230
231 gfx.dispose();
232
233 StringBuilder builder = new StringBuilder();
234
235 for (int row = 0; row + (mult - 1) < buff.getHeight(); row += mult) {
236 if (row > 0) {
237 builder.append('\n');
238 }
239
240 for (int col = 0; col + (mult - 1) < buff.getWidth(); col += mult) {
241 if (mult == 1) {
242 char car = ' ';
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#");
248
249 builder.append(car);
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//
257 ));
258 }
259 }
260 }
261
262 text = builder.toString();
263 }
264
265 return text;
266 }
267
268 @Override
269 public String toString() {
270 return getText();
271 }
272
273 /**
274 * Return the size of the given {@link Image}.
275 *
276 * @param img
277 * the image to measure
278 *
279 * @return the size
280 */
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);
288 } else {
289 try {
290 Thread.sleep(100);
291 } catch (InterruptedException e) {
292 }
293 }
294 }
295
296 return size;
297 }
298
299 /**
300 * Return the {@link Character} corresponding to the given brightness level
301 * from the evenly-separated given {@link Character}s.
302 *
303 * @param brightness
304 * the brightness level
305 * @param cars
306 * the {@link Character}s to choose from, from less bright to
307 * most bright; <b>MUST</b> contain at least one
308 * {@link Character}
309 *
310 * @return the {@link Character} to use
311 */
312 private char getDitheringChar(float brightness, String cars) {
313 int index = Math.round(brightness * (cars.length() - 1));
314 return cars.charAt(index);
315 }
316
317 /**
318 * Return the {@link Character} corresponding to the 4 given colours in
319 * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode.
320 *
321 * @param upperleft
322 * the upper left colour
323 * @param upperright
324 * the upper right colour
325 * @param lowerleft
326 * the lower left colour
327 * @param lowerright
328 * the lower right colour
329 * @param dithering
330 * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for
331 * {@link Mode#DOUBLE_RESOLUTION}
332 *
333 * @return the {@link Character} to use
334 */
335 private char getBlockChar(int upperleft, int upperright, int lowerleft,
336 int lowerright, boolean dithering) {
337 int choice = 0;
338
339 if (getBrightness(upperleft) > 0.5f) {
340 choice += 1;
341 }
342 if (getBrightness(upperright) > 0.5f) {
343 choice += 2;
344 }
345 if (getBrightness(lowerleft) > 0.5f) {
346 choice += 4;
347 }
348 if (getBrightness(lowerright) > 0.5f) {
349 choice += 8;
350 }
351
352 switch (choice) {
353 case 0:
354 return ' ';
355 case 1:
356 return '▘';
357 case 2:
358 return '▝';
359 case 3:
360 return '▀';
361 case 4:
362 return '▖';
363 case 5:
364 return '▌';
365 case 6:
366 return '▞';
367 case 7:
368 return '▛';
369 case 8:
370 return '▗';
371 case 9:
372 return '▚';
373 case 10:
374 return '▐';
375 case 11:
376 return '▜';
377 case 12:
378 return '▄';
379 case 13:
380 return '▙';
381 case 14:
382 return '▟';
383 case 15:
384 if (dithering) {
385 float avg = 0;
386 avg += getBrightness(upperleft);
387 avg += getBrightness(upperright);
388 avg += getBrightness(lowerleft);
389 avg += getBrightness(lowerright);
390 avg /= 4;
391
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;
395
396 // Do not use the " " char, as it would make a
397 // "all quarters > 0.5" pixel go black
398 return getDitheringChar(avg, "░▒▓█");
399 }
400
401 return '█';
402 }
403
404 return ' ';
405 }
406
407 /**
408 * Temporary array used so not to create a lot of new ones.
409 */
410 private float[] tmp = new float[4];
411
412 /**
413 * Return the brightness value to use from the given ARGB colour.
414 *
415 * @param argb
416 * the argb colour
417 *
418 * @return the brightness to sue for computations
419 */
420 private float getBrightness(int argb) {
421 if (invert) {
422 return 1 - rgb2hsb(argb, tmp)[2];
423 }
424
425 return rgb2hsb(argb, tmp)[2];
426 }
427
428 /**
429 * Convert the given ARGB colour in HSL/HSB, either into the supplied array
430 * or into a new one if array is NULL.
431 *
432 * <p>
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).
436 * </p>
437 * pixel
438 *
439 * @param argb
440 * the ARGB colour pixel to convert
441 * @param array
442 * the array to convert into or NULL to create a new one
443 *
444 * @return the array containing the HSL/HSB converted colour
445 */
446 static float[] rgb2hsb(int argb, float[] array) {
447 int a, r, g, b;
448 a = ((argb & 0xff000000) >> 24);
449 r = ((argb & 0x00ff0000) >> 16);
450 g = ((argb & 0x0000ff00) >> 8);
451 b = ((argb & 0x000000ff));
452
453 if (array == null) {
454 array = new float[4];
455 }
456
457 Color.RGBtoHSB(r, g, b, array);
458
459 array[3] = a;
460
461 return array;
462
463 // // other implementation:
464 //
465 // float a, r, g, b;
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;
470 //
471 // float rgbMin, rgbMax;
472 // rgbMin = Math.min(r, Math.min(g, b));
473 // rgbMax = Math.max(r, Math.max(g, b));
474 //
475 // float l;
476 // l = (rgbMin + rgbMax) / 2;
477 //
478 // float s;
479 // if (rgbMin == rgbMax) {
480 // s = 0;
481 // } else {
482 // if (l <= 0.5) {
483 // s = (rgbMax - rgbMin) / (rgbMax + rgbMin);
484 // } else {
485 // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin);
486 // }
487 // }
488 //
489 // float h;
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);
494 // } else {
495 // h = 4.0f + (r - g) / (rgbMax - rgbMin);
496 // }
497 // h /= 6; // from 0 to 1
498 //
499 // return new float[] { h, s, l, a };
500 //
501 // // // natural mode:
502 // //
503 // // int aa = (int) Math.round(100 * a);
504 // // int hh = (int) (360 * h);
505 // // if (hh < 0)
506 // // hh += 360;
507 // // int ss = (int) Math.round(100 * s);
508 // // int ll = (int) Math.round(100 * l);
509 // //
510 // // return new int[] { hh, ss, ll, aa };
511 }
512 }