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