Change build scripts
[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) {
6e4b1db4
NR
228 char car = ' ';
229 float brightness = getBrightness(buff.getRGB(col, row));
25232463 230 if (mode == Mode.DITHERING)
6e4b1db4
NR
231 car = getDitheringChar(brightness, " ░▒▓█");
232 if (mode == Mode.ASCII)
233 car = getDitheringChar(brightness, " .-+=o8#");
234
235 builder.append(car);
25232463
NR
236 } else if (mult == 2) {
237 builder.append(getBlockChar( //
238 buff.getRGB(col, row),//
239 buff.getRGB(col + 1, row),//
240 buff.getRGB(col, row + 1),//
241 buff.getRGB(col + 1, row + 1),//
242 mode == Mode.DOUBLE_DITHERING//
243 ));
244 }
f04d8b1c
NR
245 }
246 }
247
248 text = builder.toString();
249 }
250
251 return text;
252 }
253
254 @Override
255 public String toString() {
256 return getText();
257 }
258
20ce79bb
NR
259 /**
260 * Return the size of the given {@link Image}.
261 *
262 * @param img
263 * the image to measure
264 *
265 * @return the size
266 */
f04d8b1c
NR
267 static private TerminalSize getSize(Image img) {
268 TerminalSize size = null;
269 while (size == null) {
270 int w = img.getWidth(null);
271 int h = img.getHeight(null);
272 if (w > -1 && h > -1) {
273 size = new TerminalSize(w, h);
274 } else {
275 try {
276 Thread.sleep(100);
277 } catch (InterruptedException e) {
278 }
279 }
280 }
281
282 return size;
283 }
284
20ce79bb 285 /**
6e4b1db4
NR
286 * Return the {@link Character} corresponding to the given brightness level
287 * from the evenly-separated given {@link Character}s.
20ce79bb 288 *
6e4b1db4
NR
289 * @param brightness
290 * the brightness level
291 * @param cars
292 * the {@link Character}s to choose from, from less bright to
293 * most bright; <b>MUST</b> contain at least one
294 * {@link Character}
20ce79bb
NR
295 *
296 * @return the {@link Character} to use
297 */
6e4b1db4
NR
298 private char getDitheringChar(float brightness, String cars) {
299 int index = Math.round(brightness * (cars.length() - 1));
300 return cars.charAt(index);
25232463
NR
301 }
302
20ce79bb
NR
303 /**
304 * Return the {@link Character} corresponding to the 4 given colours in
305 * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode.
306 *
307 * @param upperleft
308 * the upper left colour
309 * @param upperright
310 * the upper right colour
311 * @param lowerleft
312 * the lower left colour
313 * @param lowerright
314 * the lower right colour
315 * @param dithering
316 * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for
317 * {@link Mode#DOUBLE_RESOLUTION}
318 *
319 * @return the {@link Character} to use
320 */
25232463
NR
321 private char getBlockChar(int upperleft, int upperright, int lowerleft,
322 int lowerright, boolean dithering) {
f04d8b1c 323 int choice = 0;
20ce79bb 324 if (getBrightness(upperleft) > 0.5f)
f04d8b1c 325 choice += 1;
20ce79bb 326 if (getBrightness(upperright) > 0.5f)
f04d8b1c 327 choice += 2;
20ce79bb 328 if (getBrightness(lowerleft) > 0.5f)
f04d8b1c 329 choice += 4;
20ce79bb 330 if (getBrightness(lowerright) > 0.5f)
f04d8b1c
NR
331 choice += 8;
332
333 switch (choice) {
334 case 0:
335 return ' ';
336 case 1:
337 return '▘';
338 case 2:
339 return '▝';
340 case 3:
341 return '▀';
342 case 4:
343 return '▖';
344 case 5:
345 return '▌';
346 case 6:
347 return '▞';
348 case 7:
349 return '▛';
350 case 8:
351 return '▗';
352 case 9:
353 return '▚';
354 case 10:
355 return '▐';
356 case 11:
357 return '▜';
358 case 12:
359 return '▄';
360 case 13:
361 return '▙';
362 case 14:
363 return '▟';
364 case 15:
25232463
NR
365 if (dithering) {
366 float avg = 0;
20ce79bb
NR
367 avg += getBrightness(upperleft);
368 avg += getBrightness(upperright);
369 avg += getBrightness(lowerleft);
370 avg += getBrightness(lowerright);
25232463
NR
371 avg /= 4;
372
6e4b1db4 373 return getDitheringChar(avg, " ░▒▓█");
25232463
NR
374 } else {
375 return '█';
376 }
f04d8b1c
NR
377 }
378
379 return ' ';
380 }
381
20ce79bb
NR
382 /**
383 * Temporary array used so not to create a lot of new ones.
384 */
385 private float[] tmp = new float[4];
386
387 /**
388 * Return the brightness value to use from the given ARGB colour.
389 *
390 * @param argb
391 * the argb colour
392 *
393 * @return the brightness to sue for computations
394 */
395 private float getBrightness(int argb) {
25232463
NR
396 if (invert)
397 return 1 - rgb2hsb(argb, tmp)[2];
398 return rgb2hsb(argb, tmp)[2];
399 }
f04d8b1c 400
20ce79bb
NR
401 /**
402 * Convert the given ARGB colour in HSL/HSB, either into the supplied array
403 * or into a new one if array is NULL.
404 *
405 * <p>
406 * ARGB pixels are given in 0xAARRGGBB format, while the returned array will
407 * contain Hue, Saturation, Lightness/Brightness, Alpha, in this order. H,
408 * S, L and A are all ranging from 0 to 1 (indeed, H is in 1/360th).
409 * </p>
410 * pixel
411 *
412 * @param argb
413 * the ARGB colour pixel to convert
414 * @param array
415 * the array to convert into or NULL to create a new one
416 *
417 * @return the array containing the HSL/HSB converted colour
418 */
25232463
NR
419 static float[] rgb2hsb(int argb, float[] array) {
420 int a, r, g, b;
421 a = ((argb & 0xff000000) >> 24);
422 r = ((argb & 0x00ff0000) >> 16);
423 g = ((argb & 0x0000ff00) >> 8);
424 b = ((argb & 0x000000ff));
f04d8b1c 425
25232463
NR
426 if (array == null)
427 array = new float[4];
428 Color.RGBtoHSB(r, g, b, array);
f04d8b1c 429
25232463 430 array[3] = a;
f04d8b1c 431
25232463 432 return array;
f04d8b1c 433
25232463
NR
434 // // other implementation:
435 //
436 // float a, r, g, b;
437 // a = ((argb & 0xff000000) >> 24) / 255.0f;
438 // r = ((argb & 0x00ff0000) >> 16) / 255.0f;
439 // g = ((argb & 0x0000ff00) >> 8) / 255.0f;
440 // b = ((argb & 0x000000ff)) / 255.0f;
441 //
442 // float rgbMin, rgbMax;
443 // rgbMin = Math.min(r, Math.min(g, b));
444 // rgbMax = Math.max(r, Math.max(g, b));
445 //
446 // float l;
447 // l = (rgbMin + rgbMax) / 2;
448 //
449 // float s;
450 // if (rgbMin == rgbMax) {
451 // s = 0;
452 // } else {
453 // if (l <= 0.5) {
454 // s = (rgbMax - rgbMin) / (rgbMax + rgbMin);
455 // } else {
456 // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin);
457 // }
458 // }
459 //
460 // float h;
461 // if (r > g && r > b) {
462 // h = (g - b) / (rgbMax - rgbMin);
463 // } else if (g > b) {
464 // h = 2.0f + (b - r) / (rgbMax - rgbMin);
465 // } else {
466 // h = 4.0f + (r - g) / (rgbMax - rgbMin);
467 // }
468 // h /= 6; // from 0 to 1
469 //
470 // return new float[] { h, s, l, a };
471 //
472 // // // natural mode:
473 // //
474 // // int aa = (int) Math.round(100 * a);
475 // // int hh = (int) (360 * h);
476 // // if (hh < 0)
477 // // hh += 360;
478 // // int ss = (int) Math.round(100 * s);
479 // // int ll = (int) Math.round(100 * l);
480 // //
481 // // return new int[] { hh, ss, ll, aa };
f04d8b1c
NR
482 }
483}