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