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