Version 2.0.0 (small API change)
[nikiroo-utils.git] / src / be / nikiroo / utils / ImageText.java
1 package be.nikiroo.utils;
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 ImageText {
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 * Th rendering modes supported by this {@link ImageText} 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 ImageText} 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 ImageText(Image image, Dimension size) {
72 this(image, size, Mode.DOUBLE_DITHERING, false);
73 }
74
75 /**
76 * Create a new {@link ImageText} 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 ImageText(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 ImageText}.
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 int mult = 1;
163 if (mode == Mode.DOUBLE_RESOLUTION || mode == Mode.DOUBLE_DITHERING)
164 mult = 2;
165
166 int w = size.width * mult;
167 int h = size.height * mult;
168
169 BufferedImage buff = new BufferedImage(w, h,
170 BufferedImage.TYPE_INT_ARGB);
171
172 Graphics gfx = buff.getGraphics();
173
174 Dimension srcSize = getSize(image);
175 srcSize = new Dimension(srcSize.width * 2, srcSize.height);
176 int x = 0;
177 int y = 0;
178
179 if (srcSize.width < srcSize.height) {
180 double ratio = (double) size.width / (double) size.height;
181 ratio *= (double) srcSize.height / (double) srcSize.width;
182
183 h = (int) Math.round(ratio * h);
184 y = (buff.getHeight() - h) / 2;
185 } else {
186 double ratio = (double) size.height / (double) size.width;
187 ratio *= (double) srcSize.width / (double) srcSize.height;
188
189 w = (int) Math.round(ratio * w);
190 x = (buff.getWidth() - w) / 2;
191 }
192
193 if (gfx.drawImage(image, x, y, w, h, new ImageObserver() {
194 public boolean imageUpdate(Image img, int infoflags, int x,
195 int y, int width, int height) {
196 ImageText.this.ready = true;
197 return true;
198 }
199 })) {
200 ready = true;
201 }
202
203 while (!ready) {
204 try {
205 Thread.sleep(100);
206 } catch (InterruptedException e) {
207 }
208 }
209
210 gfx.dispose();
211
212 StringBuilder builder = new StringBuilder();
213
214 for (int row = 0; row < buff.getHeight(); row += mult) {
215 if (row > 0)
216 builder.append('\n');
217
218 for (int col = 0; col < buff.getWidth(); col += mult) {
219 if (mult == 1) {
220 char car = ' ';
221 float brightness = getBrightness(buff.getRGB(col, row));
222 if (mode == Mode.DITHERING)
223 car = getDitheringChar(brightness, " ░▒▓█");
224 if (mode == Mode.ASCII)
225 car = getDitheringChar(brightness, " .-+=o8#");
226
227 builder.append(car);
228 } else if (mult == 2) {
229 builder.append(getBlockChar( //
230 buff.getRGB(col, row),//
231 buff.getRGB(col + 1, row),//
232 buff.getRGB(col, row + 1),//
233 buff.getRGB(col + 1, row + 1),//
234 mode == Mode.DOUBLE_DITHERING//
235 ));
236 }
237 }
238 }
239
240 text = builder.toString();
241 }
242
243 return text;
244 }
245
246 @Override
247 public String toString() {
248 return getText();
249 }
250
251 /**
252 * Return the size of the given {@link Image}.
253 *
254 * @param img
255 * the image to measure
256 *
257 * @return the size
258 */
259 static private Dimension getSize(Image img) {
260 Dimension size = null;
261 while (size == null) {
262 int w = img.getWidth(null);
263 int h = img.getHeight(null);
264 if (w > -1 && h > -1) {
265 size = new Dimension(w, h);
266 } else {
267 try {
268 Thread.sleep(100);
269 } catch (InterruptedException e) {
270 }
271 }
272 }
273
274 return size;
275 }
276
277 /**
278 * Return the {@link Character} corresponding to the given brightness level
279 * from the evenly-separated given {@link Character}s.
280 *
281 * @param brightness
282 * the brightness level
283 * @param cars
284 * the {@link Character}s to choose from, from less bright to
285 * most bright; <b>MUST</b> contain at least one
286 * {@link Character}
287 *
288 * @return the {@link Character} to use
289 */
290 private char getDitheringChar(float brightness, String cars) {
291 int index = Math.round(brightness * (cars.length() - 1));
292 return cars.charAt(index);
293 }
294
295 /**
296 * Return the {@link Character} corresponding to the 4 given colours in
297 * {@link Mode#DOUBLE_RESOLUTION} or {@link Mode#DOUBLE_DITHERING} mode.
298 *
299 * @param upperleft
300 * the upper left colour
301 * @param upperright
302 * the upper right colour
303 * @param lowerleft
304 * the lower left colour
305 * @param lowerright
306 * the lower right colour
307 * @param dithering
308 * TRUE to use {@link Mode#DOUBLE_DITHERING}, FALSE for
309 * {@link Mode#DOUBLE_RESOLUTION}
310 *
311 * @return the {@link Character} to use
312 */
313 private char getBlockChar(int upperleft, int upperright, int lowerleft,
314 int lowerright, boolean dithering) {
315 int choice = 0;
316 if (getBrightness(upperleft) > 0.5f)
317 choice += 1;
318 if (getBrightness(upperright) > 0.5f)
319 choice += 2;
320 if (getBrightness(lowerleft) > 0.5f)
321 choice += 4;
322 if (getBrightness(lowerright) > 0.5f)
323 choice += 8;
324
325 switch (choice) {
326 case 0:
327 return ' ';
328 case 1:
329 return '▘';
330 case 2:
331 return '▝';
332 case 3:
333 return '▀';
334 case 4:
335 return '▖';
336 case 5:
337 return '▌';
338 case 6:
339 return '▞';
340 case 7:
341 return '▛';
342 case 8:
343 return '▗';
344 case 9:
345 return '▚';
346 case 10:
347 return '▐';
348 case 11:
349 return '▜';
350 case 12:
351 return '▄';
352 case 13:
353 return '▙';
354 case 14:
355 return '▟';
356 case 15:
357 if (dithering) {
358 float avg = 0;
359 avg += getBrightness(upperleft);
360 avg += getBrightness(upperright);
361 avg += getBrightness(lowerleft);
362 avg += getBrightness(lowerright);
363 avg /= 4;
364
365 return getDitheringChar(avg, " ░▒▓█");
366 } else {
367 return '█';
368 }
369 }
370
371 return ' ';
372 }
373
374 /**
375 * Temporary array used so not to create a lot of new ones.
376 */
377 private float[] tmp = new float[4];
378
379 /**
380 * Return the brightness value to use from the given ARGB colour.
381 *
382 * @param argb
383 * the argb colour
384 *
385 * @return the brightness to sue for computations
386 */
387 private float getBrightness(int argb) {
388 if (invert)
389 return 1 - rgb2hsb(argb, tmp)[2];
390 return rgb2hsb(argb, tmp)[2];
391 }
392
393 /**
394 * Convert the given ARGB colour in HSL/HSB, either into the supplied array
395 * or into a new one if array is NULL.
396 *
397 * <p>
398 * ARGB pixels are given in 0xAARRGGBB format, while the returned array will
399 * contain Hue, Saturation, Lightness/Brightness, Alpha, in this order. H,
400 * S, L and A are all ranging from 0 to 1 (indeed, H is in 1/360th).
401 * </p>
402 * pixel
403 *
404 * @param argb
405 * the ARGB colour pixel to convert
406 * @param array
407 * the array to convert into or NULL to create a new one
408 *
409 * @return the array containing the HSL/HSB converted colour
410 */
411 static float[] rgb2hsb(int argb, float[] array) {
412 int a, r, g, b;
413 a = ((argb & 0xff000000) >> 24);
414 r = ((argb & 0x00ff0000) >> 16);
415 g = ((argb & 0x0000ff00) >> 8);
416 b = ((argb & 0x000000ff));
417
418 if (array == null)
419 array = new float[4];
420 Color.RGBtoHSB(r, g, b, array);
421
422 array[3] = a;
423
424 return array;
425
426 // // other implementation:
427 //
428 // float a, r, g, b;
429 // a = ((argb & 0xff000000) >> 24) / 255.0f;
430 // r = ((argb & 0x00ff0000) >> 16) / 255.0f;
431 // g = ((argb & 0x0000ff00) >> 8) / 255.0f;
432 // b = ((argb & 0x000000ff)) / 255.0f;
433 //
434 // float rgbMin, rgbMax;
435 // rgbMin = Math.min(r, Math.min(g, b));
436 // rgbMax = Math.max(r, Math.max(g, b));
437 //
438 // float l;
439 // l = (rgbMin + rgbMax) / 2;
440 //
441 // float s;
442 // if (rgbMin == rgbMax) {
443 // s = 0;
444 // } else {
445 // if (l <= 0.5) {
446 // s = (rgbMax - rgbMin) / (rgbMax + rgbMin);
447 // } else {
448 // s = (rgbMax - rgbMin) / (2.0f - rgbMax - rgbMin);
449 // }
450 // }
451 //
452 // float h;
453 // if (r > g && r > b) {
454 // h = (g - b) / (rgbMax - rgbMin);
455 // } else if (g > b) {
456 // h = 2.0f + (b - r) / (rgbMax - rgbMin);
457 // } else {
458 // h = 4.0f + (r - g) / (rgbMax - rgbMin);
459 // }
460 // h /= 6; // from 0 to 1
461 //
462 // return new float[] { h, s, l, a };
463 //
464 // // // natural mode:
465 // //
466 // // int aa = (int) Math.round(100 * a);
467 // // int hh = (int) (360 * h);
468 // // if (hh < 0)
469 // // hh += 360;
470 // // int ss = (int) Math.round(100 * s);
471 // // int ll = (int) Math.round(100 * l);
472 // //
473 // // return new int[] { hh, ss, ll, aa };
474 }
475 }