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