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