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