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