Merge branch 'upstream-sep2019-tcombo' into subtree
[fanfix.git] / src / jexer / tterminal / Sixel.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.tterminal;
30
31 import java.awt.Color;
32 import java.awt.Graphics2D;
33 import java.awt.image.BufferedImage;
34 import java.util.ArrayList;
35 import java.util.HashMap;
36
37 /**
38 * Sixel parses a buffer of sixel image data into a BufferedImage.
39 */
40 public class Sixel {
41
42 // ------------------------------------------------------------------------
43 // Constants --------------------------------------------------------------
44 // ------------------------------------------------------------------------
45
46 /**
47 * Parser character scan states.
48 */
49 private enum ScanState {
50 GROUND,
51 RASTER,
52 COLOR,
53 REPEAT,
54 }
55
56 // ------------------------------------------------------------------------
57 // Variables --------------------------------------------------------------
58 // ------------------------------------------------------------------------
59
60 /**
61 * If true, enable debug messages.
62 */
63 private static boolean DEBUG = false;
64
65 /**
66 * Number of pixels to increment when we need more horizontal room.
67 */
68 private static int WIDTH_INCREASE = 400;
69
70 /**
71 * Number of pixels to increment when we need more vertical room.
72 */
73 private static int HEIGHT_INCREASE = 400;
74
75 /**
76 * Maximum width in pixels.
77 */
78 private static int MAX_WIDTH = 1000;
79
80 /**
81 * Maximum height in pixels.
82 */
83 private static int MAX_HEIGHT = 1000;
84
85 /**
86 * Current scanning state.
87 */
88 private ScanState scanState = ScanState.GROUND;
89
90 /**
91 * Parameters being collected.
92 */
93 private int [] params = new int[5];
94
95 /**
96 * Current parameter being collected.
97 */
98 private int paramsI = 0;
99
100 /**
101 * The sixel palette colors specified.
102 */
103 private HashMap<Integer, Color> palette;
104
105 /**
106 * The buffer to parse.
107 */
108 private String buffer;
109
110 /**
111 * The image being drawn to.
112 */
113 private BufferedImage image;
114
115 /**
116 * The real width of image.
117 */
118 private int width = 0;
119
120 /**
121 * The real height of image.
122 */
123 private int height = 0;
124
125 /**
126 * The width of image provided in the raster attribute.
127 */
128 private int rasterWidth = 0;
129
130 /**
131 * The height of image provided in the raster attribute.
132 */
133 private int rasterHeight = 0;
134
135 /**
136 * The repeat count.
137 */
138 private int repeatCount = -1;
139
140 /**
141 * The current drawing x position.
142 */
143 private int x = 0;
144
145 /**
146 * The maximum y drawn to. This will set the final image height.
147 */
148 private int y = 0;
149
150 /**
151 * The current drawing color.
152 */
153 private Color color = Color.BLACK;
154
155 /**
156 * If set, abort processing this image.
157 */
158 private boolean abort = false;
159
160 // ------------------------------------------------------------------------
161 // Constructors -----------------------------------------------------------
162 // ------------------------------------------------------------------------
163
164 /**
165 * Public constructor.
166 *
167 * @param buffer the sixel data to parse
168 * @param palette palette to use, or null for a private palette
169 */
170 public Sixel(final String buffer, final HashMap<Integer, Color> palette) {
171 this.buffer = buffer;
172 if (palette == null) {
173 this.palette = new HashMap<Integer, Color>();
174 } else {
175 this.palette = palette;
176 }
177 }
178
179 // ------------------------------------------------------------------------
180 // Sixel ------------------------------------------------------------------
181 // ------------------------------------------------------------------------
182
183 /**
184 * Get the image.
185 *
186 * @return the sixel data as an image.
187 */
188 public BufferedImage getImage() {
189 if (buffer != null) {
190 for (int i = 0; (i < buffer.length()) && (abort == false); i++) {
191 consume(buffer.charAt(i));
192 }
193 buffer = null;
194 }
195 if (abort == true) {
196 return null;
197 }
198
199 if ((width > 0) && (height > 0) && (image != null)) {
200 /*
201 System.err.println(String.format("%d %d %d %d", width, y + 1,
202 rasterWidth, rasterHeight));
203 */
204
205 if ((rasterWidth > width) || (rasterHeight > y + 1)) {
206 resizeImage(Math.max(width, rasterWidth),
207 Math.max(y + 1, rasterHeight));
208 }
209 return image.getSubimage(0, 0, width, y + 1);
210 }
211 return null;
212 }
213
214 /**
215 * Resize image to a new size.
216 *
217 * @param newWidth new width of image
218 * @param newHeight new height of image
219 */
220 private void resizeImage(final int newWidth, final int newHeight) {
221 BufferedImage newImage = new BufferedImage(newWidth, newHeight,
222 BufferedImage.TYPE_INT_ARGB);
223
224 if (image == null) {
225 image = newImage;
226 return;
227 }
228
229 if (DEBUG) {
230 System.err.println("resizeImage(); old " + image.getWidth() + "x" +
231 image.getHeight() + " new " + newWidth + "x" + newHeight);
232 }
233
234 Graphics2D gr = newImage.createGraphics();
235 gr.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
236 gr.dispose();
237 image = newImage;
238 }
239
240 /**
241 * Clear the parameters and flags.
242 */
243 private void toGround() {
244 paramsI = 0;
245 for (int i = 0; i < params.length; i++) {
246 params[i] = 0;
247 }
248 scanState = ScanState.GROUND;
249 repeatCount = -1;
250 }
251
252 /**
253 * Get a color parameter value, with a default.
254 *
255 * @param position parameter index. 0 is the first parameter.
256 * @param defaultValue value to use if colorParams[position] doesn't exist
257 * @return parameter value
258 */
259 private int getParam(final int position, final int defaultValue) {
260 if (position > paramsI) {
261 return defaultValue;
262 }
263 return params[position];
264 }
265
266 /**
267 * Get a color parameter value, clamped to within min/max.
268 *
269 * @param position parameter index. 0 is the first parameter.
270 * @param defaultValue value to use if colorParams[position] doesn't exist
271 * @param minValue minimum value inclusive
272 * @param maxValue maximum value inclusive
273 * @return parameter value
274 */
275 private int getParam(final int position, final int defaultValue,
276 final int minValue, final int maxValue) {
277
278 assert (minValue <= maxValue);
279 int value = getParam(position, defaultValue);
280 if (value < minValue) {
281 value = minValue;
282 }
283 if (value > maxValue) {
284 value = maxValue;
285 }
286 return value;
287 }
288
289 /**
290 * Add sixel data to the image.
291 *
292 * @param ch the character of sixel data
293 */
294 private void addSixel(final char ch) {
295 int n = ((int) ch - 63);
296
297 if (DEBUG && (color == null)) {
298 System.err.println("color is null?!");
299 System.err.println(buffer);
300 }
301
302 int rgb = color.getRGB();
303 int rep = (repeatCount == -1 ? 1 : repeatCount);
304
305 if (DEBUG) {
306 System.err.println("addSixel() rep " + rep + " char " +
307 Integer.toHexString(n) + " color " + color);
308 }
309
310 assert (n >= 0);
311
312 if (image == null) {
313 // The raster attributes was not provided.
314 resizeImage(WIDTH_INCREASE, HEIGHT_INCREASE);
315 }
316
317 if (x + rep > image.getWidth()) {
318 // Resize the image, give us another max(rep, WIDTH_INCREASE)
319 // pixels of horizontal length.
320 resizeImage(image.getWidth() + Math.max(rep, WIDTH_INCREASE),
321 image.getHeight());
322 }
323
324 // If nothing will be drawn, just advance x.
325 if (n == 0) {
326 x += rep;
327 if (x > width) {
328 width = x;
329 }
330 if (width > MAX_WIDTH) {
331 abort = true;
332 }
333 return;
334 }
335
336 int dy = 0;
337 for (int i = 0; i < rep; i++) {
338 if ((n & 0x01) != 0) {
339 dy = 0;
340 image.setRGB(x, height + dy, rgb);
341 }
342 if ((n & 0x02) != 0) {
343 dy = 1;
344 image.setRGB(x, height + dy, rgb);
345 }
346 if ((n & 0x04) != 0) {
347 dy = 2;
348 image.setRGB(x, height + dy, rgb);
349 }
350 if ((n & 0x08) != 0) {
351 dy = 3;
352 image.setRGB(x, height + dy, rgb);
353 }
354 if ((n & 0x10) != 0) {
355 dy = 4;
356 image.setRGB(x, height + dy, rgb);
357 }
358 if ((n & 0x20) != 0) {
359 dy = 5;
360 image.setRGB(x, height + dy, rgb);
361 }
362 if (height + dy > y) {
363 y = height + dy;
364 }
365 x++;
366 }
367 if (x > width) {
368 width = x;
369 }
370 if (width > MAX_WIDTH) {
371 abort = true;
372 }
373 if (y + 1 > MAX_HEIGHT) {
374 abort = true;
375 }
376 }
377
378 /**
379 * Process a color palette change.
380 */
381 private void setPalette() {
382 int idx = getParam(0, 0);
383
384 if (paramsI == 0) {
385 Color newColor = palette.get(idx);
386 if (newColor != null) {
387 color = newColor;
388 } else {
389 if (DEBUG) {
390 System.err.println("COLOR " + idx + " NOT FOUND");
391 }
392 color = Color.BLACK;
393 }
394
395 if (DEBUG) {
396 System.err.println("set color " + idx + " " + color);
397 }
398 return;
399 }
400
401 int type = getParam(1, 0);
402 float red = (float) (getParam(2, 0, 0, 100) / 100.0);
403 float green = (float) (getParam(3, 0, 0, 100) / 100.0);
404 float blue = (float) (getParam(4, 0, 0, 100) / 100.0);
405
406 if (type == 2) {
407 Color newColor = new Color(red, green, blue);
408 palette.put(idx, newColor);
409 if (DEBUG) {
410 System.err.println("Palette color " + idx + " --> " + newColor);
411 }
412 } else {
413 if (DEBUG) {
414 System.err.println("UNKNOWN COLOR TYPE " + type + ": " + type +
415 " " + idx + " R " + red + " G " + green + " B " + blue);
416 }
417 }
418 }
419
420 /**
421 * Parse the raster attributes.
422 */
423 private void parseRaster() {
424 int pan = getParam(0, 0); // Aspect ratio numerator
425 int pad = getParam(1, 0); // Aspect ratio denominator
426 int pah = getParam(2, 0); // Horizontal width
427 int pav = getParam(3, 0); // Vertical height
428
429 if ((pan == pad) && (pah > 0) && (pav > 0)) {
430 rasterWidth = pah;
431 rasterHeight = pav;
432 if ((rasterWidth <= MAX_WIDTH) && (rasterHeight <= MAX_HEIGHT)) {
433 resizeImage(rasterWidth, rasterHeight);
434 } else {
435 abort = true;
436 }
437 } else {
438 abort = true;
439 }
440 }
441
442 /**
443 * Run this input character through the sixel state machine.
444 *
445 * @param ch character from the remote side
446 */
447 private void consume(char ch) {
448
449 // DEBUG
450 // System.err.printf("Sixel.consume() %c STATE = %s\n", ch, scanState);
451
452 // Between decimal 63 (inclusive) and 127 (exclusive) --> pixels
453 if ((ch >= 63) && (ch < 127)) {
454 if (scanState == ScanState.COLOR) {
455 setPalette();
456 }
457 if (scanState == ScanState.RASTER) {
458 parseRaster();
459 toGround();
460 }
461 addSixel(ch);
462 toGround();
463 return;
464 }
465
466 if (ch == '#') {
467 // Next color is here, parse what we had before.
468 if (scanState == ScanState.COLOR) {
469 setPalette();
470 toGround();
471 }
472 if (scanState == ScanState.RASTER) {
473 parseRaster();
474 toGround();
475 }
476 scanState = ScanState.COLOR;
477 return;
478 }
479
480 if (ch == '!') {
481 // Repeat count
482 if (scanState == ScanState.COLOR) {
483 setPalette();
484 toGround();
485 }
486 if (scanState == ScanState.RASTER) {
487 parseRaster();
488 toGround();
489 }
490 scanState = ScanState.REPEAT;
491 repeatCount = 0;
492 return;
493 }
494
495 if (ch == '-') {
496 if (scanState == ScanState.COLOR) {
497 setPalette();
498 toGround();
499 }
500 if (scanState == ScanState.RASTER) {
501 parseRaster();
502 toGround();
503 }
504
505 height += 6;
506 x = 0;
507
508 if (height + 6 > image.getHeight()) {
509 // Resize the image, give us another HEIGHT_INCREASE
510 // pixels of vertical length.
511 resizeImage(image.getWidth(),
512 image.getHeight() + HEIGHT_INCREASE);
513 }
514 return;
515 }
516
517 if (ch == '$') {
518 if (scanState == ScanState.COLOR) {
519 setPalette();
520 toGround();
521 }
522 if (scanState == ScanState.RASTER) {
523 parseRaster();
524 toGround();
525 }
526 x = 0;
527 return;
528 }
529
530 if (ch == '"') {
531 if (scanState == ScanState.COLOR) {
532 setPalette();
533 toGround();
534 }
535 scanState = ScanState.RASTER;
536 return;
537 }
538
539 switch (scanState) {
540
541 case GROUND:
542 // Unknown character.
543 if (DEBUG) {
544 System.err.println("UNKNOWN CHAR: " + ch);
545 }
546 return;
547
548 case RASTER:
549 // 30-39, 3B --> param
550 if ((ch >= '0') && (ch <= '9')) {
551 params[paramsI] *= 10;
552 params[paramsI] += (ch - '0');
553 }
554 if (ch == ';') {
555 if (paramsI < params.length - 1) {
556 paramsI++;
557 }
558 }
559 return;
560
561 case COLOR:
562 // 30-39, 3B --> param
563 if ((ch >= '0') && (ch <= '9')) {
564 params[paramsI] *= 10;
565 params[paramsI] += (ch - '0');
566 }
567 if (ch == ';') {
568 if (paramsI < params.length - 1) {
569 paramsI++;
570 }
571 }
572 return;
573
574 case REPEAT:
575 if ((ch >= '0') && (ch <= '9')) {
576 if (repeatCount == -1) {
577 repeatCount = (int) (ch - '0');
578 } else {
579 repeatCount *= 10;
580 repeatCount += (int) (ch - '0');
581 }
582 }
583 return;
584
585 }
586
587 }
588
589 }