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