2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.tterminal
;
31 import java
.awt
.Color
;
32 import java
.awt
.Graphics2D
;
33 import java
.awt
.image
.BufferedImage
;
34 import java
.util
.HashMap
;
37 * Sixel parses a buffer of sixel image data into a BufferedImage.
41 // ------------------------------------------------------------------------
42 // Constants --------------------------------------------------------------
43 // ------------------------------------------------------------------------
46 * Parser character scan states.
48 private enum ScanState
{
55 // ------------------------------------------------------------------------
56 // Variables --------------------------------------------------------------
57 // ------------------------------------------------------------------------
60 * If true, enable debug messages.
62 private static boolean DEBUG
= false;
65 * Number of pixels to increment when we need more horizontal room.
67 private static int WIDTH_INCREASE
= 400;
70 * Number of pixels to increment when we need more vertical room.
72 private static int HEIGHT_INCREASE
= 400;
75 * Maximum width in pixels.
77 private static int MAX_WIDTH
= 1000;
80 * Maximum height in pixels.
82 private static int MAX_HEIGHT
= 1000;
85 * Current scanning state.
87 private ScanState scanState
= ScanState
.GROUND
;
90 * Parameters being collected.
92 private int [] params
= new int[5];
95 * Current parameter being collected.
97 private int paramsI
= 0;
100 * The sixel palette colors specified.
102 private HashMap
<Integer
, Color
> palette
;
105 * The buffer to parse.
107 private String buffer
;
110 * The image being drawn to.
112 private BufferedImage image
;
115 * The real width of image.
117 private int width
= 0;
120 * The real height of image.
122 private int height
= 0;
125 * The width of image provided in the raster attribute.
127 private int rasterWidth
= 0;
130 * The height of image provided in the raster attribute.
132 private int rasterHeight
= 0;
137 private int repeatCount
= -1;
140 * The current drawing x position.
145 * The maximum y drawn to. This will set the final image height.
150 * The current drawing color.
152 private Color color
= Color
.BLACK
;
155 * If set, abort processing this image.
157 private boolean abort
= false;
159 // ------------------------------------------------------------------------
160 // Constructors -----------------------------------------------------------
161 // ------------------------------------------------------------------------
164 * Public constructor.
166 * @param buffer the sixel data to parse
167 * @param palette palette to use, or null for a private palette
169 public Sixel(final String buffer
, final HashMap
<Integer
, Color
> palette
) {
170 this.buffer
= buffer
;
171 if (palette
== null) {
172 this.palette
= new HashMap
<Integer
, Color
>();
174 this.palette
= palette
;
178 // ------------------------------------------------------------------------
179 // Sixel ------------------------------------------------------------------
180 // ------------------------------------------------------------------------
185 * @return the sixel data as an image.
187 public BufferedImage
getImage() {
188 if (buffer
!= null) {
189 for (int i
= 0; (i
< buffer
.length()) && (abort
== false); i
++) {
190 consume(buffer
.charAt(i
));
198 if ((width
> 0) && (height
> 0) && (image
!= null)) {
200 System.err.println(String.format("%d %d %d %d", width, y + 1,
201 rasterWidth, rasterHeight));
204 if ((rasterWidth
> width
) || (rasterHeight
> y
+ 1)) {
205 resizeImage(Math
.max(width
, rasterWidth
),
206 Math
.max(y
+ 1, rasterHeight
));
208 return image
.getSubimage(0, 0, width
, y
+ 1);
214 * Resize image to a new size.
216 * @param newWidth new width of image
217 * @param newHeight new height of image
219 private void resizeImage(final int newWidth
, final int newHeight
) {
220 BufferedImage newImage
= new BufferedImage(newWidth
, newHeight
,
221 BufferedImage
.TYPE_INT_ARGB
);
229 System
.err
.println("resizeImage(); old " + image
.getWidth() + "x" +
230 image
.getHeight() + " new " + newWidth
+ "x" + newHeight
);
233 Graphics2D gr
= newImage
.createGraphics();
234 gr
.drawImage(image
, 0, 0, image
.getWidth(), image
.getHeight(), null);
240 * Clear the parameters and flags.
242 private void toGround() {
244 for (int i
= 0; i
< params
.length
; i
++) {
247 scanState
= ScanState
.GROUND
;
252 * Get a color parameter value, with a default.
254 * @param position parameter index. 0 is the first parameter.
255 * @param defaultValue value to use if colorParams[position] doesn't exist
256 * @return parameter value
258 private int getParam(final int position
, final int defaultValue
) {
259 if (position
> paramsI
) {
262 return params
[position
];
266 * Get a color parameter value, clamped to within min/max.
268 * @param position parameter index. 0 is the first parameter.
269 * @param defaultValue value to use if colorParams[position] doesn't exist
270 * @param minValue minimum value inclusive
271 * @param maxValue maximum value inclusive
272 * @return parameter value
274 private int getParam(final int position
, final int defaultValue
,
275 final int minValue
, final int maxValue
) {
277 assert (minValue
<= maxValue
);
278 int value
= getParam(position
, defaultValue
);
279 if (value
< minValue
) {
282 if (value
> maxValue
) {
289 * Add sixel data to the image.
291 * @param ch the character of sixel data
293 private void addSixel(final char ch
) {
294 int n
= ((int) ch
- 63);
296 if (DEBUG
&& (color
== null)) {
297 System
.err
.println("color is null?!");
298 System
.err
.println(buffer
);
301 int rgb
= color
.getRGB();
302 int rep
= (repeatCount
== -1 ?
1 : repeatCount
);
305 System
.err
.println("addSixel() rep " + rep
+ " char " +
306 Integer
.toHexString(n
) + " color " + color
);
312 // The raster attributes was not provided.
313 resizeImage(WIDTH_INCREASE
, HEIGHT_INCREASE
);
316 if (x
+ rep
> image
.getWidth()) {
317 // Resize the image, give us another max(rep, WIDTH_INCREASE)
318 // pixels of horizontal length.
319 resizeImage(image
.getWidth() + Math
.max(rep
, WIDTH_INCREASE
),
323 // If nothing will be drawn, just advance x.
329 if (width
> MAX_WIDTH
) {
336 for (int i
= 0; i
< rep
; i
++) {
337 if ((n
& 0x01) != 0) {
339 image
.setRGB(x
, height
+ dy
, rgb
);
341 if ((n
& 0x02) != 0) {
343 image
.setRGB(x
, height
+ dy
, rgb
);
345 if ((n
& 0x04) != 0) {
347 image
.setRGB(x
, height
+ dy
, rgb
);
349 if ((n
& 0x08) != 0) {
351 image
.setRGB(x
, height
+ dy
, rgb
);
353 if ((n
& 0x10) != 0) {
355 image
.setRGB(x
, height
+ dy
, rgb
);
357 if ((n
& 0x20) != 0) {
359 image
.setRGB(x
, height
+ dy
, rgb
);
361 if (height
+ dy
> y
) {
369 if (width
> MAX_WIDTH
) {
372 if (y
+ 1 > MAX_HEIGHT
) {
378 * Process a color palette change.
380 private void setPalette() {
381 int idx
= getParam(0, 0);
384 Color newColor
= palette
.get(idx
);
385 if (newColor
!= null) {
389 System
.err
.println("COLOR " + idx
+ " NOT FOUND");
395 System
.err
.println("set color " + idx
+ " " + color
);
400 int type
= getParam(1, 0);
401 float red
= (float) (getParam(2, 0, 0, 100) / 100.0);
402 float green
= (float) (getParam(3, 0, 0, 100) / 100.0);
403 float blue
= (float) (getParam(4, 0, 0, 100) / 100.0);
406 Color newColor
= new Color(red
, green
, blue
);
407 palette
.put(idx
, newColor
);
409 System
.err
.println("Palette color " + idx
+ " --> " + newColor
);
413 System
.err
.println("UNKNOWN COLOR TYPE " + type
+ ": " + type
+
414 " " + idx
+ " R " + red
+ " G " + green
+ " B " + blue
);
420 * Parse the raster attributes.
422 private void parseRaster() {
423 int pan
= getParam(0, 0); // Aspect ratio numerator
424 int pad
= getParam(1, 0); // Aspect ratio denominator
425 int pah
= getParam(2, 0); // Horizontal width
426 int pav
= getParam(3, 0); // Vertical height
428 if ((pan
== pad
) && (pah
> 0) && (pav
> 0)) {
431 if ((rasterWidth
<= MAX_WIDTH
) && (rasterHeight
<= MAX_HEIGHT
)) {
432 resizeImage(rasterWidth
, rasterHeight
);
442 * Run this input character through the sixel state machine.
444 * @param ch character from the remote side
446 private void consume(char ch
) {
449 // System.err.printf("Sixel.consume() %c STATE = %s\n", ch, scanState);
451 // Between decimal 63 (inclusive) and 127 (exclusive) --> pixels
452 if ((ch
>= 63) && (ch
< 127)) {
453 if (scanState
== ScanState
.COLOR
) {
456 if (scanState
== ScanState
.RASTER
) {
466 // Next color is here, parse what we had before.
467 if (scanState
== ScanState
.COLOR
) {
471 if (scanState
== ScanState
.RASTER
) {
475 scanState
= ScanState
.COLOR
;
481 if (scanState
== ScanState
.COLOR
) {
485 if (scanState
== ScanState
.RASTER
) {
489 scanState
= ScanState
.REPEAT
;
495 if (scanState
== ScanState
.COLOR
) {
499 if (scanState
== ScanState
.RASTER
) {
507 if (height
+ 6 > image
.getHeight()) {
508 // Resize the image, give us another HEIGHT_INCREASE
509 // pixels of vertical length.
510 resizeImage(image
.getWidth(),
511 image
.getHeight() + HEIGHT_INCREASE
);
517 if (scanState
== ScanState
.COLOR
) {
521 if (scanState
== ScanState
.RASTER
) {
530 if (scanState
== ScanState
.COLOR
) {
534 scanState
= ScanState
.RASTER
;
541 // Unknown character.
543 System
.err
.println("UNKNOWN CHAR: " + ch
);
548 // 30-39, 3B --> param
549 if ((ch
>= '0') && (ch
<= '9')) {
550 params
[paramsI
] *= 10;
551 params
[paramsI
] += (ch
- '0');
554 if (paramsI
< params
.length
- 1) {
561 // 30-39, 3B --> param
562 if ((ch
>= '0') && (ch
<= '9')) {
563 params
[paramsI
] *= 10;
564 params
[paramsI
] += (ch
- '0');
567 if (paramsI
< params
.length
- 1) {
574 if ((ch
>= '0') && (ch
<= '9')) {
575 if (repeatCount
== -1) {
576 repeatCount
= (ch
- '0');
579 repeatCount
+= (ch
- '0');