Commit | Line | Data |
---|---|---|
5fc7bf09 KL |
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; | |
5fc7bf09 KL |
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, | |
8dd530ff | 50 | RASTER, |
69a8c368 KL |
51 | COLOR, |
52 | REPEAT, | |
5fc7bf09 KL |
53 | } |
54 | ||
55 | // ------------------------------------------------------------------------ | |
56 | // Variables -------------------------------------------------------------- | |
57 | // ------------------------------------------------------------------------ | |
58 | ||
59 | /** | |
60 | * If true, enable debug messages. | |
61 | */ | |
69a8c368 | 62 | private static boolean DEBUG = false; |
5fc7bf09 KL |
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 | ||
686d4da2 KL |
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 | ||
5fc7bf09 KL |
84 | /** |
85 | * Current scanning state. | |
86 | */ | |
87 | private ScanState scanState = ScanState.GROUND; | |
88 | ||
89 | /** | |
ab215e38 | 90 | * Parameters being collected. |
5fc7bf09 | 91 | */ |
ab215e38 KL |
92 | private int [] params = new int[5]; |
93 | ||
94 | /** | |
95 | * Current parameter being collected. | |
96 | */ | |
97 | private int paramsI = 0; | |
5fc7bf09 KL |
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 | ||
8dd530ff KL |
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 | ||
5fc7bf09 KL |
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 | ||
74bbd9bc KL |
144 | /** |
145 | * The maximum y drawn to. This will set the final image height. | |
146 | */ | |
147 | private int y = 0; | |
148 | ||
5fc7bf09 KL |
149 | /** |
150 | * The current drawing color. | |
151 | */ | |
152 | private Color color = Color.BLACK; | |
153 | ||
686d4da2 KL |
154 | /** |
155 | * If set, abort processing this image. | |
156 | */ | |
157 | private boolean abort = false; | |
158 | ||
5fc7bf09 KL |
159 | // ------------------------------------------------------------------------ |
160 | // Constructors ----------------------------------------------------------- | |
161 | // ------------------------------------------------------------------------ | |
162 | ||
163 | /** | |
164 | * Public constructor. | |
165 | * | |
166 | * @param buffer the sixel data to parse | |
686d4da2 | 167 | * @param palette palette to use, or null for a private palette |
5fc7bf09 | 168 | */ |
686d4da2 | 169 | public Sixel(final String buffer, final HashMap<Integer, Color> palette) { |
5fc7bf09 | 170 | this.buffer = buffer; |
686d4da2 KL |
171 | if (palette == null) { |
172 | this.palette = new HashMap<Integer, Color>(); | |
173 | } else { | |
174 | this.palette = palette; | |
5fc7bf09 KL |
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() { | |
686d4da2 KL |
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 | ||
8dd530ff KL |
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 | } | |
74bbd9bc | 208 | return image.getSubimage(0, 0, width, y + 1); |
5fc7bf09 KL |
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 | ||
8dd530ff KL |
223 | if (image == null) { |
224 | image = newImage; | |
225 | return; | |
226 | } | |
227 | ||
03ae544a KL |
228 | if (DEBUG) { |
229 | System.err.println("resizeImage(); old " + image.getWidth() + "x" + | |
230 | image.getHeight() + " new " + newWidth + "x" + newHeight); | |
231 | } | |
232 | ||
5fc7bf09 KL |
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() { | |
ab215e38 KL |
243 | paramsI = 0; |
244 | for (int i = 0; i < params.length; i++) { | |
245 | params[i] = 0; | |
246 | } | |
5fc7bf09 KL |
247 | scanState = ScanState.GROUND; |
248 | repeatCount = -1; | |
249 | } | |
250 | ||
5fc7bf09 KL |
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 | */ | |
8dd530ff | 258 | private int getParam(final int position, final int defaultValue) { |
ab215e38 | 259 | if (position > paramsI) { |
5fc7bf09 KL |
260 | return defaultValue; |
261 | } | |
ab215e38 | 262 | return params[position]; |
5fc7bf09 KL |
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 | */ | |
8dd530ff | 274 | private int getParam(final int position, final int defaultValue, |
5fc7bf09 KL |
275 | final int minValue, final int maxValue) { |
276 | ||
277 | assert (minValue <= maxValue); | |
8dd530ff | 278 | int value = getParam(position, defaultValue); |
5fc7bf09 KL |
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); | |
e6469faa KL |
295 | |
296 | if (DEBUG && (color == null)) { | |
297 | System.err.println("color is null?!"); | |
298 | System.err.println(buffer); | |
299 | } | |
300 | ||
5fc7bf09 KL |
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 | ||
69a8c368 KL |
309 | assert (n >= 0); |
310 | ||
8dd530ff KL |
311 | if (image == null) { |
312 | // The raster attributes was not provided. | |
313 | resizeImage(WIDTH_INCREASE, HEIGHT_INCREASE); | |
314 | } | |
315 | ||
5fc7bf09 KL |
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 | } | |
686d4da2 KL |
329 | if (width > MAX_WIDTH) { |
330 | abort = true; | |
331 | } | |
5fc7bf09 KL |
332 | return; |
333 | } | |
334 | ||
ab215e38 | 335 | int dy = 0; |
5fc7bf09 | 336 | for (int i = 0; i < rep; i++) { |
69a8c368 | 337 | if ((n & 0x01) != 0) { |
ab215e38 KL |
338 | dy = 0; |
339 | image.setRGB(x, height + dy, rgb); | |
5fc7bf09 | 340 | } |
69a8c368 | 341 | if ((n & 0x02) != 0) { |
ab215e38 KL |
342 | dy = 1; |
343 | image.setRGB(x, height + dy, rgb); | |
5fc7bf09 | 344 | } |
69a8c368 | 345 | if ((n & 0x04) != 0) { |
ab215e38 KL |
346 | dy = 2; |
347 | image.setRGB(x, height + dy, rgb); | |
5fc7bf09 | 348 | } |
69a8c368 | 349 | if ((n & 0x08) != 0) { |
ab215e38 KL |
350 | dy = 3; |
351 | image.setRGB(x, height + dy, rgb); | |
5fc7bf09 | 352 | } |
69a8c368 | 353 | if ((n & 0x10) != 0) { |
ab215e38 KL |
354 | dy = 4; |
355 | image.setRGB(x, height + dy, rgb); | |
5fc7bf09 | 356 | } |
69a8c368 | 357 | if ((n & 0x20) != 0) { |
ab215e38 KL |
358 | dy = 5; |
359 | image.setRGB(x, height + dy, rgb); | |
5fc7bf09 | 360 | } |
ab215e38 KL |
361 | if (height + dy > y) { |
362 | y = height + dy; | |
5fc7bf09 | 363 | } |
ab215e38 KL |
364 | x++; |
365 | } | |
366 | if (x > width) { | |
367 | width = x; | |
5fc7bf09 | 368 | } |
686d4da2 KL |
369 | if (width > MAX_WIDTH) { |
370 | abort = true; | |
371 | } | |
372 | if (y + 1 > MAX_HEIGHT) { | |
373 | abort = true; | |
374 | } | |
5fc7bf09 KL |
375 | } |
376 | ||
377 | /** | |
378 | * Process a color palette change. | |
379 | */ | |
380 | private void setPalette() { | |
8dd530ff | 381 | int idx = getParam(0, 0); |
5fc7bf09 | 382 | |
ab215e38 | 383 | if (paramsI == 0) { |
5fc7bf09 KL |
384 | Color newColor = palette.get(idx); |
385 | if (newColor != null) { | |
386 | color = newColor; | |
69a8c368 | 387 | } else { |
e6469faa KL |
388 | if (DEBUG) { |
389 | System.err.println("COLOR " + idx + " NOT FOUND"); | |
390 | } | |
391 | color = Color.BLACK; | |
5fc7bf09 KL |
392 | } |
393 | ||
394 | if (DEBUG) { | |
69a8c368 | 395 | System.err.println("set color " + idx + " " + color); |
5fc7bf09 KL |
396 | } |
397 | return; | |
398 | } | |
399 | ||
8dd530ff KL |
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); | |
5fc7bf09 KL |
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 | } | |
69a8c368 KL |
411 | } else { |
412 | if (DEBUG) { | |
413 | System.err.println("UNKNOWN COLOR TYPE " + type + ": " + type + | |
414 | " " + idx + " R " + red + " G " + green + " B " + blue); | |
415 | } | |
5fc7bf09 KL |
416 | } |
417 | } | |
418 | ||
8dd530ff KL |
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; | |
686d4da2 KL |
431 | if ((rasterWidth <= MAX_WIDTH) && (rasterHeight <= MAX_HEIGHT)) { |
432 | resizeImage(rasterWidth, rasterHeight); | |
433 | } else { | |
434 | abort = true; | |
435 | } | |
436 | } else { | |
437 | abort = true; | |
8dd530ff KL |
438 | } |
439 | } | |
440 | ||
5fc7bf09 KL |
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 | ||
69a8c368 KL |
451 | // Between decimal 63 (inclusive) and 127 (exclusive) --> pixels |
452 | if ((ch >= 63) && (ch < 127)) { | |
453 | if (scanState == ScanState.COLOR) { | |
454 | setPalette(); | |
5fc7bf09 | 455 | } |
8dd530ff KL |
456 | if (scanState == ScanState.RASTER) { |
457 | parseRaster(); | |
458 | toGround(); | |
459 | } | |
69a8c368 KL |
460 | addSixel(ch); |
461 | toGround(); | |
5fc7bf09 | 462 | return; |
69a8c368 | 463 | } |
5fc7bf09 | 464 | |
69a8c368 KL |
465 | if (ch == '#') { |
466 | // Next color is here, parse what we had before. | |
467 | if (scanState == ScanState.COLOR) { | |
468 | setPalette(); | |
469 | toGround(); | |
5fc7bf09 | 470 | } |
8dd530ff KL |
471 | if (scanState == ScanState.RASTER) { |
472 | parseRaster(); | |
473 | toGround(); | |
474 | } | |
69a8c368 | 475 | scanState = ScanState.COLOR; |
5fc7bf09 | 476 | return; |
69a8c368 | 477 | } |
5fc7bf09 | 478 | |
69a8c368 KL |
479 | if (ch == '!') { |
480 | // Repeat count | |
481 | if (scanState == ScanState.COLOR) { | |
5fc7bf09 KL |
482 | setPalette(); |
483 | toGround(); | |
484 | } | |
8dd530ff KL |
485 | if (scanState == ScanState.RASTER) { |
486 | parseRaster(); | |
487 | toGround(); | |
488 | } | |
69a8c368 KL |
489 | scanState = ScanState.REPEAT; |
490 | repeatCount = 0; | |
491 | return; | |
492 | } | |
5fc7bf09 | 493 | |
69a8c368 KL |
494 | if (ch == '-') { |
495 | if (scanState == ScanState.COLOR) { | |
5fc7bf09 KL |
496 | setPalette(); |
497 | toGround(); | |
69a8c368 | 498 | } |
8dd530ff KL |
499 | if (scanState == ScanState.RASTER) { |
500 | parseRaster(); | |
501 | toGround(); | |
502 | } | |
5fc7bf09 | 503 | |
03ae544a KL |
504 | height += 6; |
505 | x = 0; | |
506 | ||
507 | if (height + 6 > image.getHeight()) { | |
69a8c368 KL |
508 | // Resize the image, give us another HEIGHT_INCREASE |
509 | // pixels of vertical length. | |
510 | resizeImage(image.getWidth(), | |
511 | image.getHeight() + HEIGHT_INCREASE); | |
5fc7bf09 | 512 | } |
69a8c368 KL |
513 | return; |
514 | } | |
515 | ||
516 | if (ch == '$') { | |
517 | if (scanState == ScanState.COLOR) { | |
5fc7bf09 KL |
518 | setPalette(); |
519 | toGround(); | |
5fc7bf09 | 520 | } |
8dd530ff KL |
521 | if (scanState == ScanState.RASTER) { |
522 | parseRaster(); | |
523 | toGround(); | |
524 | } | |
69a8c368 KL |
525 | x = 0; |
526 | return; | |
527 | } | |
5fc7bf09 | 528 | |
69a8c368 KL |
529 | if (ch == '"') { |
530 | if (scanState == ScanState.COLOR) { | |
5fc7bf09 KL |
531 | setPalette(); |
532 | toGround(); | |
5fc7bf09 | 533 | } |
8dd530ff | 534 | scanState = ScanState.RASTER; |
5fc7bf09 | 535 | return; |
69a8c368 | 536 | } |
5fc7bf09 | 537 | |
69a8c368 | 538 | switch (scanState) { |
5fc7bf09 | 539 | |
69a8c368 KL |
540 | case GROUND: |
541 | // Unknown character. | |
542 | if (DEBUG) { | |
543 | System.err.println("UNKNOWN CHAR: " + ch); | |
5fc7bf09 | 544 | } |
69a8c368 KL |
545 | return; |
546 | ||
8dd530ff KL |
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 | } | |
69a8c368 | 558 | return; |
5fc7bf09 | 559 | |
69a8c368 KL |
560 | case COLOR: |
561 | // 30-39, 3B --> param | |
5fc7bf09 | 562 | if ((ch >= '0') && (ch <= '9')) { |
ab215e38 KL |
563 | params[paramsI] *= 10; |
564 | params[paramsI] += (ch - '0'); | |
5fc7bf09 KL |
565 | } |
566 | if (ch == ';') { | |
ab215e38 KL |
567 | if (paramsI < params.length - 1) { |
568 | paramsI++; | |
569 | } | |
5fc7bf09 | 570 | } |
5fc7bf09 KL |
571 | return; |
572 | ||
69a8c368 | 573 | case REPEAT: |
5fc7bf09 KL |
574 | if ((ch >= '0') && (ch <= '9')) { |
575 | if (repeatCount == -1) { | |
5ca5f8e5 | 576 | repeatCount = (ch - '0'); |
5fc7bf09 KL |
577 | } else { |
578 | repeatCount *= 10; | |
5ca5f8e5 | 579 | repeatCount += (ch - '0'); |
5fc7bf09 KL |
580 | } |
581 | } | |
5fc7bf09 | 582 | return; |
69a8c368 | 583 | |
5fc7bf09 KL |
584 | } |
585 | ||
586 | } | |
587 | ||
588 | } |