--- /dev/null
+/*
+ * Jexer - Java Text User Interface
+ *
+ * The MIT License (MIT)
+ *
+ * Copyright (C) 2019 Kevin Lamonte
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
+ */
+package jexer.tterminal;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Sixel parses a buffer of sixel image data into a BufferedImage.
+ */
+public class Sixel {
+
+ // ------------------------------------------------------------------------
+ // Constants --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Parser character scan states.
+ */
+ private enum ScanState {
+ GROUND,
+ RASTER,
+ COLOR,
+ REPEAT,
+ }
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * If true, enable debug messages.
+ */
+ private static boolean DEBUG = false;
+
+ /**
+ * Number of pixels to increment when we need more horizontal room.
+ */
+ private static int WIDTH_INCREASE = 400;
+
+ /**
+ * Number of pixels to increment when we need more vertical room.
+ */
+ private static int HEIGHT_INCREASE = 400;
+
+ /**
+ * Maximum width in pixels.
+ */
+ private static int MAX_WIDTH = 1000;
+
+ /**
+ * Maximum height in pixels.
+ */
+ private static int MAX_HEIGHT = 1000;
+
+ /**
+ * Current scanning state.
+ */
+ private ScanState scanState = ScanState.GROUND;
+
+ /**
+ * Parameters being collected.
+ */
+ private int [] params = new int[5];
+
+ /**
+ * Current parameter being collected.
+ */
+ private int paramsI = 0;
+
+ /**
+ * The sixel palette colors specified.
+ */
+ private HashMap<Integer, Color> palette;
+
+ /**
+ * The buffer to parse.
+ */
+ private String buffer;
+
+ /**
+ * The image being drawn to.
+ */
+ private BufferedImage image;
+
+ /**
+ * The real width of image.
+ */
+ private int width = 0;
+
+ /**
+ * The real height of image.
+ */
+ private int height = 0;
+
+ /**
+ * The width of image provided in the raster attribute.
+ */
+ private int rasterWidth = 0;
+
+ /**
+ * The height of image provided in the raster attribute.
+ */
+ private int rasterHeight = 0;
+
+ /**
+ * The repeat count.
+ */
+ private int repeatCount = -1;
+
+ /**
+ * The current drawing x position.
+ */
+ private int x = 0;
+
+ /**
+ * The maximum y drawn to. This will set the final image height.
+ */
+ private int y = 0;
+
+ /**
+ * The current drawing color.
+ */
+ private Color color = Color.BLACK;
+
+ /**
+ * If set, abort processing this image.
+ */
+ private boolean abort = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param buffer the sixel data to parse
+ * @param palette palette to use, or null for a private palette
+ */
+ public Sixel(final String buffer, final HashMap<Integer, Color> palette) {
+ this.buffer = buffer;
+ if (palette == null) {
+ this.palette = new HashMap<Integer, Color>();
+ } else {
+ this.palette = palette;
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Sixel ------------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Get the image.
+ *
+ * @return the sixel data as an image.
+ */
+ public BufferedImage getImage() {
+ if (buffer != null) {
+ for (int i = 0; (i < buffer.length()) && (abort == false); i++) {
+ consume(buffer.charAt(i));
+ }
+ buffer = null;
+ }
+ if (abort == true) {
+ return null;
+ }
+
+ if ((width > 0) && (height > 0) && (image != null)) {
+ /*
+ System.err.println(String.format("%d %d %d %d", width, y + 1,
+ rasterWidth, rasterHeight));
+ */
+
+ if ((rasterWidth > width) || (rasterHeight > y + 1)) {
+ resizeImage(Math.max(width, rasterWidth),
+ Math.max(y + 1, rasterHeight));
+ }
+ return image.getSubimage(0, 0, width, y + 1);
+ }
+ return null;
+ }
+
+ /**
+ * Resize image to a new size.
+ *
+ * @param newWidth new width of image
+ * @param newHeight new height of image
+ */
+ private void resizeImage(final int newWidth, final int newHeight) {
+ BufferedImage newImage = new BufferedImage(newWidth, newHeight,
+ BufferedImage.TYPE_INT_ARGB);
+
+ if (image == null) {
+ image = newImage;
+ return;
+ }
+
+ if (DEBUG) {
+ System.err.println("resizeImage(); old " + image.getWidth() + "x" +
+ image.getHeight() + " new " + newWidth + "x" + newHeight);
+ }
+
+ Graphics2D gr = newImage.createGraphics();
+ gr.drawImage(image, 0, 0, image.getWidth(), image.getHeight(), null);
+ gr.dispose();
+ image = newImage;
+ }
+
+ /**
+ * Clear the parameters and flags.
+ */
+ private void toGround() {
+ paramsI = 0;
+ for (int i = 0; i < params.length; i++) {
+ params[i] = 0;
+ }
+ scanState = ScanState.GROUND;
+ repeatCount = -1;
+ }
+
+ /**
+ * Get a color parameter value, with a default.
+ *
+ * @param position parameter index. 0 is the first parameter.
+ * @param defaultValue value to use if colorParams[position] doesn't exist
+ * @return parameter value
+ */
+ private int getParam(final int position, final int defaultValue) {
+ if (position > paramsI) {
+ return defaultValue;
+ }
+ return params[position];
+ }
+
+ /**
+ * Get a color parameter value, clamped to within min/max.
+ *
+ * @param position parameter index. 0 is the first parameter.
+ * @param defaultValue value to use if colorParams[position] doesn't exist
+ * @param minValue minimum value inclusive
+ * @param maxValue maximum value inclusive
+ * @return parameter value
+ */
+ private int getParam(final int position, final int defaultValue,
+ final int minValue, final int maxValue) {
+
+ assert (minValue <= maxValue);
+ int value = getParam(position, defaultValue);
+ if (value < minValue) {
+ value = minValue;
+ }
+ if (value > maxValue) {
+ value = maxValue;
+ }
+ return value;
+ }
+
+ /**
+ * Add sixel data to the image.
+ *
+ * @param ch the character of sixel data
+ */
+ private void addSixel(final char ch) {
+ int n = ((int) ch - 63);
+
+ if (DEBUG && (color == null)) {
+ System.err.println("color is null?!");
+ System.err.println(buffer);
+ }
+
+ int rgb = color.getRGB();
+ int rep = (repeatCount == -1 ? 1 : repeatCount);
+
+ if (DEBUG) {
+ System.err.println("addSixel() rep " + rep + " char " +
+ Integer.toHexString(n) + " color " + color);
+ }
+
+ assert (n >= 0);
+
+ if (image == null) {
+ // The raster attributes was not provided.
+ resizeImage(WIDTH_INCREASE, HEIGHT_INCREASE);
+ }
+
+ if (x + rep > image.getWidth()) {
+ // Resize the image, give us another max(rep, WIDTH_INCREASE)
+ // pixels of horizontal length.
+ resizeImage(image.getWidth() + Math.max(rep, WIDTH_INCREASE),
+ image.getHeight());
+ }
+
+ // If nothing will be drawn, just advance x.
+ if (n == 0) {
+ x += rep;
+ if (x > width) {
+ width = x;
+ }
+ if (width > MAX_WIDTH) {
+ abort = true;
+ }
+ return;
+ }
+
+ int dy = 0;
+ for (int i = 0; i < rep; i++) {
+ if ((n & 0x01) != 0) {
+ dy = 0;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x02) != 0) {
+ dy = 1;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x04) != 0) {
+ dy = 2;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x08) != 0) {
+ dy = 3;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x10) != 0) {
+ dy = 4;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if ((n & 0x20) != 0) {
+ dy = 5;
+ image.setRGB(x, height + dy, rgb);
+ }
+ if (height + dy > y) {
+ y = height + dy;
+ }
+ x++;
+ }
+ if (x > width) {
+ width = x;
+ }
+ if (width > MAX_WIDTH) {
+ abort = true;
+ }
+ if (y + 1 > MAX_HEIGHT) {
+ abort = true;
+ }
+ }
+
+ /**
+ * Process a color palette change.
+ */
+ private void setPalette() {
+ int idx = getParam(0, 0);
+
+ if (paramsI == 0) {
+ Color newColor = palette.get(idx);
+ if (newColor != null) {
+ color = newColor;
+ } else {
+ if (DEBUG) {
+ System.err.println("COLOR " + idx + " NOT FOUND");
+ }
+ color = Color.BLACK;
+ }
+
+ if (DEBUG) {
+ System.err.println("set color " + idx + " " + color);
+ }
+ return;
+ }
+
+ int type = getParam(1, 0);
+ float red = (float) (getParam(2, 0, 0, 100) / 100.0);
+ float green = (float) (getParam(3, 0, 0, 100) / 100.0);
+ float blue = (float) (getParam(4, 0, 0, 100) / 100.0);
+
+ if (type == 2) {
+ Color newColor = new Color(red, green, blue);
+ palette.put(idx, newColor);
+ if (DEBUG) {
+ System.err.println("Palette color " + idx + " --> " + newColor);
+ }
+ } else {
+ if (DEBUG) {
+ System.err.println("UNKNOWN COLOR TYPE " + type + ": " + type +
+ " " + idx + " R " + red + " G " + green + " B " + blue);
+ }
+ }
+ }
+
+ /**
+ * Parse the raster attributes.
+ */
+ private void parseRaster() {
+ int pan = getParam(0, 0); // Aspect ratio numerator
+ int pad = getParam(1, 0); // Aspect ratio denominator
+ int pah = getParam(2, 0); // Horizontal width
+ int pav = getParam(3, 0); // Vertical height
+
+ if ((pan == pad) && (pah > 0) && (pav > 0)) {
+ rasterWidth = pah;
+ rasterHeight = pav;
+ if ((rasterWidth <= MAX_WIDTH) && (rasterHeight <= MAX_HEIGHT)) {
+ resizeImage(rasterWidth, rasterHeight);
+ } else {
+ abort = true;
+ }
+ } else {
+ abort = true;
+ }
+ }
+
+ /**
+ * Run this input character through the sixel state machine.
+ *
+ * @param ch character from the remote side
+ */
+ private void consume(char ch) {
+
+ // DEBUG
+ // System.err.printf("Sixel.consume() %c STATE = %s\n", ch, scanState);
+
+ // Between decimal 63 (inclusive) and 127 (exclusive) --> pixels
+ if ((ch >= 63) && (ch < 127)) {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ addSixel(ch);
+ toGround();
+ return;
+ }
+
+ if (ch == '#') {
+ // Next color is here, parse what we had before.
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ scanState = ScanState.COLOR;
+ return;
+ }
+
+ if (ch == '!') {
+ // Repeat count
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ scanState = ScanState.REPEAT;
+ repeatCount = 0;
+ return;
+ }
+
+ if (ch == '-') {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+
+ height += 6;
+ x = 0;
+
+ if (height + 6 > image.getHeight()) {
+ // Resize the image, give us another HEIGHT_INCREASE
+ // pixels of vertical length.
+ resizeImage(image.getWidth(),
+ image.getHeight() + HEIGHT_INCREASE);
+ }
+ return;
+ }
+
+ if (ch == '$') {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ if (scanState == ScanState.RASTER) {
+ parseRaster();
+ toGround();
+ }
+ x = 0;
+ return;
+ }
+
+ if (ch == '"') {
+ if (scanState == ScanState.COLOR) {
+ setPalette();
+ toGround();
+ }
+ scanState = ScanState.RASTER;
+ return;
+ }
+
+ switch (scanState) {
+
+ case GROUND:
+ // Unknown character.
+ if (DEBUG) {
+ System.err.println("UNKNOWN CHAR: " + ch);
+ }
+ return;
+
+ case RASTER:
+ // 30-39, 3B --> param
+ if ((ch >= '0') && (ch <= '9')) {
+ params[paramsI] *= 10;
+ params[paramsI] += (ch - '0');
+ }
+ if (ch == ';') {
+ if (paramsI < params.length - 1) {
+ paramsI++;
+ }
+ }
+ return;
+
+ case COLOR:
+ // 30-39, 3B --> param
+ if ((ch >= '0') && (ch <= '9')) {
+ params[paramsI] *= 10;
+ params[paramsI] += (ch - '0');
+ }
+ if (ch == ';') {
+ if (paramsI < params.length - 1) {
+ paramsI++;
+ }
+ }
+ return;
+
+ case REPEAT:
+ if ((ch >= '0') && (ch <= '9')) {
+ if (repeatCount == -1) {
+ repeatCount = (int) (ch - '0');
+ } else {
+ repeatCount *= 10;
+ repeatCount += (int) (ch - '0');
+ }
+ }
+ return;
+
+ }
+
+ }
+
+}