Remove timage list
[nikiroo-utils.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
251 if (DEBUG && (color == null)) {
252 System.err.println("color is null?!");
253 System.err.println(buffer);
254 }
255
256 int rgb = color.getRGB();
257 int rep = (repeatCount == -1 ? 1 : repeatCount);
258
259 if (DEBUG) {
260 System.err.println("addSixel() rep " + rep + " char " +
261 Integer.toHexString(n) + " color " + color);
262 }
263
264 assert (n >= 0);
265
266 if (x + rep > image.getWidth()) {
267 // Resize the image, give us another max(rep, WIDTH_INCREASE)
268 // pixels of horizontal length.
269 resizeImage(image.getWidth() + Math.max(rep, WIDTH_INCREASE),
270 image.getHeight());
271 }
272
273 // If nothing will be drawn, just advance x.
274 if (n == 0) {
275 x += rep;
276 if (x > width) {
277 width = x;
278 }
279 return;
280 }
281
282 for (int i = 0; i < rep; i++) {
283 if ((n & 0x01) != 0) {
284 image.setRGB(x, height + 0, rgb);
285 }
286 if ((n & 0x02) != 0) {
287 image.setRGB(x, height + 1, rgb);
288 }
289 if ((n & 0x04) != 0) {
290 image.setRGB(x, height + 2, rgb);
291 }
292 if ((n & 0x08) != 0) {
293 image.setRGB(x, height + 3, rgb);
294 }
295 if ((n & 0x10) != 0) {
296 image.setRGB(x, height + 4, rgb);
297 }
298 if ((n & 0x20) != 0) {
299 image.setRGB(x, height + 5, rgb);
300 }
301 x++;
302 if (x > width) {
303 width++;
304 assert (x == width);
305 }
306 }
307 }
308
309 /**
310 * Process a color palette change.
311 */
312 private void setPalette() {
313 int idx = getColorParam(0, 0);
314
315 if (colorParams.size() == 1) {
316 Color newColor = palette.get(idx);
317 if (newColor != null) {
318 color = newColor;
319 } else {
320 if (DEBUG) {
321 System.err.println("COLOR " + idx + " NOT FOUND");
322 }
323 color = Color.BLACK;
324 }
325
326 if (DEBUG) {
327 System.err.println("set color " + idx + " " + color);
328 }
329 return;
330 }
331
332 int type = getColorParam(1, 0);
333 float red = (float) (getColorParam(2, 0, 0, 100) / 100.0);
334 float green = (float) (getColorParam(3, 0, 0, 100) / 100.0);
335 float blue = (float) (getColorParam(4, 0, 0, 100) / 100.0);
336
337 if (type == 2) {
338 Color newColor = new Color(red, green, blue);
339 palette.put(idx, newColor);
340 if (DEBUG) {
341 System.err.println("Palette color " + idx + " --> " + newColor);
342 }
343 } else {
344 if (DEBUG) {
345 System.err.println("UNKNOWN COLOR TYPE " + type + ": " + type +
346 " " + idx + " R " + red + " G " + green + " B " + blue);
347 }
348 }
349 }
350
351 /**
352 * Run this input character through the sixel state machine.
353 *
354 * @param ch character from the remote side
355 */
356 private void consume(char ch) {
357
358 // DEBUG
359 // System.err.printf("Sixel.consume() %c STATE = %s\n", ch, scanState);
360
361 // Between decimal 63 (inclusive) and 127 (exclusive) --> pixels
362 if ((ch >= 63) && (ch < 127)) {
363 if (scanState == ScanState.COLOR) {
364 setPalette();
365 toGround();
366 }
367 addSixel(ch);
368 toGround();
369 return;
370 }
371
372 if (ch == '#') {
373 // Next color is here, parse what we had before.
374 if (scanState == ScanState.COLOR) {
375 setPalette();
376 toGround();
377 }
378 scanState = ScanState.COLOR;
379 return;
380 }
381
382 if (ch == '!') {
383 // Repeat count
384 if (scanState == ScanState.COLOR) {
385 setPalette();
386 toGround();
387 }
388 scanState = ScanState.REPEAT;
389 repeatCount = 0;
390 return;
391 }
392
393 if (ch == '-') {
394 if (scanState == ScanState.COLOR) {
395 setPalette();
396 toGround();
397 }
398
399 if (height + 6 < image.getHeight()) {
400 // Resize the image, give us another HEIGHT_INCREASE
401 // pixels of vertical length.
402 resizeImage(image.getWidth(),
403 image.getHeight() + HEIGHT_INCREASE);
404 }
405 height += 6;
406 x = 0;
407 return;
408 }
409
410 if (ch == '$') {
411 if (scanState == ScanState.COLOR) {
412 setPalette();
413 toGround();
414 }
415 x = 0;
416 return;
417 }
418
419 if (ch == '"') {
420 if (scanState == ScanState.COLOR) {
421 setPalette();
422 toGround();
423 }
424 scanState = ScanState.QUOTE;
425 return;
426 }
427
428 switch (scanState) {
429
430 case GROUND:
431 // Unknown character.
432 if (DEBUG) {
433 System.err.println("UNKNOWN CHAR: " + ch);
434 }
435 return;
436
437 case QUOTE:
438 // Ignore everything else in the quote header.
439 return;
440
441 case COLOR:
442 // 30-39, 3B --> param
443 if ((ch >= '0') && (ch <= '9')) {
444 param((byte) ch);
445 }
446 if (ch == ';') {
447 param((byte) ch);
448 }
449 return;
450
451 case REPEAT:
452 if ((ch >= '0') && (ch <= '9')) {
453 if (repeatCount == -1) {
454 repeatCount = (int) (ch - '0');
455 } else {
456 repeatCount *= 10;
457 repeatCount += (int) (ch - '0');
458 }
459 }
460 return;
461
462 }
463
464 }
465
466 }