remove jar files from sources, should not be there
[jvcard.git] / src / be / nikiroo / jvcard / tui / ImageText.java
... / ...
CommitLineData
1package be.nikiroo.jvcard.tui;
2
3import java.awt.Color;
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
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 */
18public 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 /**
27 * Th rendering modes supported by this {@link ImageText} to convert
28 * {@link Image}s into text.
29 *
30 * @author niki
31 *
32 */
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
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
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
86 * @param invert
87 * TRUE to invert colours rendering
88 */
89 public ImageText(Image image, TerminalSize size, Mode mode, boolean invert) {
90 setImage(image);
91 setSize(size);
92 setMode(mode);
93 setColorInvert(invert);
94 }
95
96 /**
97 * Change the source {@link Image}.
98 *
99 * @param image
100 * the new {@link Image}
101 */
102 public void setImage(Image image) {
103 this.text = null;
104 this.ready = false;
105 this.image = image;
106 }
107
108 /**
109 * Change the target size of this {@link ImageText}.
110 *
111 * @param size
112 * the new size
113 */
114 public void setSize(TerminalSize size) {
115 this.text = null;
116 this.ready = false;
117 this.size = size;
118 }
119
120 /**
121 * Change the image-to-text mode.
122 *
123 * @param mode
124 * the new {@link Mode}
125 */
126 public void setMode(Mode mode) {
127 this.mode = mode;
128 this.text = null;
129 this.ready = false;
130 }
131
132 /**
133 * Set the colour-invert mode.
134 *
135 * @param invert
136 * TRUE to inverse the colours
137 */
138 public void setColorInvert(boolean invert) {
139 this.invert = invert;
140 this.text = null;
141 this.ready = false;
142 }
143
144 /**
145 * Check if the colours are inverted.
146 *
147 * @return TRUE if the colours are inverted
148 */
149 public boolean isColorInvert() {
150 return invert;
151 }
152
153 /**
154 * Return the textual representation of the included {@link Image}.
155 *
156 * @return the {@link String} representation
157 */
158 public String getText() {
159 if (text == null) {
160 if (image == null || size == null || size.getColumns() == 0
161 || size.getRows() == 0)
162 return "";
163
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
171 BufferedImage buff = new BufferedImage(w, h,
172 BufferedImage.TYPE_INT_ARGB);
173
174 Graphics gfx = buff.getGraphics();
175
176 TerminalSize srcSize = getSize(image);
177 srcSize = new TerminalSize(srcSize.getColumns() * 2,
178 srcSize.getRows());
179 int x = 0;
180 int y = 0;
181
182 if (srcSize.getColumns() < srcSize.getRows()) {
183 double ratio = (double) size.getColumns()
184 / (double) size.getRows();
185 ratio *= (double) srcSize.getRows()
186 / (double) srcSize.getColumns();
187
188 h = (int) Math.round(ratio * h);
189 y = (buff.getHeight() - h) / 2;
190 } else {
191 double ratio = (double) size.getRows()
192 / (double) size.getColumns();
193 ratio *= (double) srcSize.getColumns()
194 / (double) srcSize.getRows();
195
196 w = (int) Math.round(ratio * w);
197 x = (buff.getWidth() - w) / 2;
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
220 StringBuilder builder = new StringBuilder();
221
222 for (int row = 0; row < buff.getHeight(); row += mult) {
223 if (row > 0)
224 builder.append('\n');
225
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 }
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
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 */
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
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 */
292 private char getAsciiChar(int pixel) {
293 float brigthness = getBrightness(pixel);
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
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 */
316 private char getDitheringChar(int pixel) {
317 float brigthness = getBrightness(pixel);
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
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 */
349 private char getBlockChar(int upperleft, int upperright, int lowerleft,
350 int lowerright, boolean dithering) {
351 int choice = 0;
352 if (getBrightness(upperleft) > 0.5f)
353 choice += 1;
354 if (getBrightness(upperright) > 0.5f)
355 choice += 2;
356 if (getBrightness(lowerleft) > 0.5f)
357 choice += 4;
358 if (getBrightness(lowerright) > 0.5f)
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:
393 if (dithering) {
394 float avg = 0;
395 avg += getBrightness(upperleft);
396 avg += getBrightness(upperright);
397 avg += getBrightness(lowerleft);
398 avg += getBrightness(lowerright);
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 }
415 }
416
417 return ' ';
418 }
419
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) {
434 if (invert)
435 return 1 - rgb2hsb(argb, tmp)[2];
436 return rgb2hsb(argb, tmp)[2];
437 }
438
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 */
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));
463
464 if (array == null)
465 array = new float[4];
466 Color.RGBtoHSB(r, g, b, array);
467
468 array[3] = a;
469
470 return array;
471
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 };
520 }
521}