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