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