TTerminalWindow sixel support wip
authorKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 4 Aug 2019 12:52:47 +0000 (07:52 -0500)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Sun, 4 Aug 2019 12:52:47 +0000 (07:52 -0500)
src/jexer/TTerminalWindow.java
src/jexer/backend/ECMA48Terminal.java
src/jexer/tterminal/ECMA48.java
src/jexer/tterminal/Sixel.java [new file with mode: 0644]

index b36e86b62dfca47b0d6f95864f19c3b6b1d1cf15..818a52f6eb6bbccc20dfc45042cad9653ba1b7be 100644 (file)
  */
 package jexer;
 
-import java.awt.image.BufferedImage;
 import java.awt.Font;
 import java.awt.FontMetrics;
 import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
 
 import java.io.InputStream;
 import java.io.IOException;
@@ -761,6 +761,22 @@ public class TTerminalWindow extends TScrollableWindow
 
         // Add shortcut text
         newStatusBar(i18n.getString("statusBarRunning"));
+
+        // Pass the correct text cell width/height to the emulator
+        int textWidth = 16;
+        int textHeight = 20;
+        if (getScreen() instanceof SwingTerminal) {
+            SwingTerminal terminal = (SwingTerminal) getScreen();
+
+            textWidth = terminal.getTextWidth();
+            textHeight = terminal.getTextHeight();
+        } else if (getScreen() instanceof ECMA48Terminal) {
+            ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
+            textWidth = terminal.getTextWidth();
+            textHeight = terminal.getTextHeight();
+        }
+        emulator.setTextWidth(textWidth);
+        emulator.setTextHeight(textHeight);
     }
 
     /**
@@ -954,6 +970,14 @@ public class TTerminalWindow extends TScrollableWindow
         } else if (getScreen() instanceof ECMA48Terminal) {
             ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
 
+            if (!terminal.hasSixel()) {
+                // The backend does not have sixel support, draw this as text
+                // and bail out.
+                putCharXY(x, y, cell);
+                putCharXY(x + 1, y, ' ', cell);
+                return;
+            }
+
             textWidth = terminal.getTextWidth();
             textHeight = terminal.getTextHeight();
             cursorBlinkVisible = blinkState;
index 6085554904320675139608a67329e0bbe33428fe..08010cef42c569cd9ea796111424fad63171c78e 100644 (file)
@@ -3012,6 +3012,15 @@ public class ECMA48Terminal extends LogicalScreen
         return (startSixel(x, y) + sb.toString() + endSixel());
     }
 
+    /**
+     * Get the sixel support flag.
+     *
+     * @return true if this terminal is emitting sixel
+     */
+    public boolean hasSixel() {
+        return sixel;
+    }
+
     // ------------------------------------------------------------------------
     // End sixel output support -----------------------------------------------
     // ------------------------------------------------------------------------
index eb13c0b4d35e43176e86568a65e6ef936042b58f..f2b485eca078e949b75868575b6869a05e01b4c6 100644 (file)
@@ -28,6 +28,8 @@
  */
 package jexer.tterminal;
 
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
 import java.io.BufferedOutputStream;
 import java.io.CharArrayWriter;
 import java.io.InputStream;
@@ -41,6 +43,7 @@ import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 
 import jexer.TKeypress;
@@ -136,6 +139,7 @@ public class ECMA48 implements Runnable {
         DCS_PARAM,
         DCS_PASSTHROUGH,
         DCS_IGNORE,
+        DCS_SIXEL,
         SOSPMAPC_STRING,
         OSC_STRING,
         VT52_DIRECT_CURSOR_ADDRESS
@@ -458,6 +462,21 @@ public class ECMA48 implements Runnable {
      */
     private List<Integer> colors88;
 
+    /**
+     * Sixel collection buffer.
+     */
+    private StringBuilder sixelParseBuffer;
+
+    /**
+     * The width of a character cell in pixels.
+     */
+    private int textWidth = 16;
+
+    /**
+     * The height of a character cell in pixels.
+     */
+    private int textHeight = 20;
+
     /**
      * DECSC/DECRC save/restore a subset of the total state.  This class
      * encapsulates those specific flags/modes.
@@ -4609,7 +4628,7 @@ public class ECMA48 implements Runnable {
     private void consume(char ch) {
 
         // DEBUG
-        // System.err.printf("%c", ch);
+        // System.err.printf("%c STATE = %s\n", ch, scanState);
 
         // Special case for VT10x: 7-bit characters only
         if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) {
@@ -4631,9 +4650,11 @@ public class ECMA48 implements Runnable {
         if (ch == 0x1B) {
             if ((type == DeviceType.XTERM)
                 && ((scanState == ScanState.OSC_STRING)
+                    || (scanState == ScanState.DCS_SIXEL)
                     || (scanState == ScanState.SOSPMAPC_STRING))
             ) {
                 // Xterm can pass ESCAPE to its OSC sequence.
+                // Xterm can pass ESCAPE to its DCS sequence.
                 // Jexer can pass ESCAPE to its PM sequence.
             } else if ((scanState != ScanState.DCS_ENTRY)
                 && (scanState != ScanState.DCS_INTERMEDIATE)
@@ -4641,7 +4662,6 @@ public class ECMA48 implements Runnable {
                 && (scanState != ScanState.DCS_PARAM)
                 && (scanState != ScanState.DCS_PASSTHROUGH)
             ) {
-
                 scanState = ScanState.ESCAPE;
                 return;
             }
@@ -6353,8 +6373,12 @@ public class ECMA48 implements Runnable {
                 scanState = ScanState.DCS_IGNORE;
             }
 
-            // 0x40-7E goes to DCS_PASSTHROUGH
-            if ((ch >= 0x40) && (ch <= 0x7E)) {
+            // 0x71 goes to DCS_SIXEL
+            if (ch == 0x71) {
+                sixelParseBuffer = new StringBuilder();
+                scanState = ScanState.DCS_SIXEL;
+            } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+                // 0x40-7E goes to DCS_PASSTHROUGH
                 scanState = ScanState.DCS_PASSTHROUGH;
             }
             return;
@@ -6434,8 +6458,12 @@ public class ECMA48 implements Runnable {
                 scanState = ScanState.DCS_IGNORE;
             }
 
-            // 0x40-7E goes to DCS_PASSTHROUGH
-            if ((ch >= 0x40) && (ch <= 0x7E)) {
+            // 0x71 goes to DCS_SIXEL
+            if (ch == 0x71) {
+                sixelParseBuffer = new StringBuilder();
+                scanState = ScanState.DCS_SIXEL;
+            } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+                // 0x40-7E goes to DCS_PASSTHROUGH
                 scanState = ScanState.DCS_PASSTHROUGH;
             }
             return;
@@ -6487,6 +6515,48 @@ public class ECMA48 implements Runnable {
 
             return;
 
+        case DCS_SIXEL:
+            // 0x9C goes to GROUND
+            if (ch == 0x9C) {
+                parseSixel();
+                toGround();
+            }
+
+            // 0x1B 0x5C goes to GROUND
+            if (ch == 0x1B) {
+                collect(ch);
+            }
+            if (ch == 0x5C) {
+                if ((collectBuffer.length() > 0)
+                    && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+                ) {
+                    parseSixel();
+                    toGround();
+                }
+            }
+
+            // 00-17, 19, 1C-1F, 20-7E   --> put
+            if (ch <= 0x17) {
+                sixelParseBuffer.append(ch);
+                return;
+            }
+            if (ch == 0x19) {
+                sixelParseBuffer.append(ch);
+                return;
+            }
+            if ((ch >= 0x1C) && (ch <= 0x1F)) {
+                sixelParseBuffer.append(ch);
+                return;
+            }
+            if ((ch >= 0x20) && (ch <= 0x7E)) {
+                sixelParseBuffer.append(ch);
+                return;
+            }
+
+            // 7F                        --> ignore
+
+            return;
+
         case SOSPMAPC_STRING:
             // 00-17, 19, 1C-1F, 20-7F --> ignore
 
@@ -6573,4 +6643,121 @@ public class ECMA48 implements Runnable {
         return hideMousePointer;
     }
 
+    // ------------------------------------------------------------------------
+    // Sixel support ----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Set the width of a character cell in pixels.
+     *
+     * @param textWidth the width in pixels of a character cell
+     */
+    public void setTextWidth(final int textWidth) {
+        this.textWidth = textWidth;
+    }
+
+    /**
+     * Set the height of a character cell in pixels.
+     *
+     * @param textHeight the height in pixels of a character cell
+     */
+    public void setTextHeight(final int textHeight) {
+        this.textHeight = textHeight;
+    }
+
+    /**
+     * Parse a sixel string into a bitmap image, and overlay that image onto
+     * the text cells.
+     */
+    private void parseSixel() {
+        System.err.println("parseSixel(): '" + sixelParseBuffer.toString() +
+            "'");
+
+        Sixel sixel = new Sixel(sixelParseBuffer.toString());
+        BufferedImage image = sixel.getImage();
+
+        System.err.println("parseSixel(): image " + image);
+
+        if (image == null) {
+            // Sixel data was malformed in some way, bail out.
+            return;
+        }
+
+        /*
+         * Procedure:
+         *
+         * Break up the image into text cell sized pieces as a new array of
+         * Cells.
+         *
+         * Note original column position x0.
+         *
+         * For each cell:
+         *
+         * 1. Advance (printCharacter(' ')) for horizontal increment, or
+         *    index (linefeed() + cursorPosition(y, x0)) for vertical
+         *    increment.
+         *
+         * 2. Set (x, y) cell image data.
+         *
+         * 3. For the right and bottom edges:
+         *
+         *   a. Render the text to pixels using Terminus font.
+         *
+         *   b. Blit the image on top of the text, using alpha channel.
+         */
+        int cellColumns = image.getWidth() / textWidth;
+        if (cellColumns * textWidth < image.getWidth()) {
+            cellColumns++;
+        }
+        int cellRows = image.getHeight() / textHeight;
+        if (cellRows * textHeight < image.getHeight()) {
+            cellRows++;
+        }
+
+        // Break the image up into an array of cells.
+        Cell [][] cells = new Cell[cellColumns][cellRows];
+
+        for (int x = 0; x < cellColumns; x++) {
+            for (int y = 0; y < cellRows; y++) {
+
+                int width = textWidth;
+                if ((x + 1) * textWidth > image.getWidth()) {
+                    width = image.getWidth() - (x * textWidth);
+                }
+                int height = textHeight;
+                if ((y + 1) * textHeight > image.getHeight()) {
+                    height = image.getHeight() - (y * textHeight);
+                }
+
+                Cell cell = new Cell();
+                cell.setImage(image.getSubimage(x * textWidth,
+                        y * textHeight, width, height));
+
+                cells[x][y] = cell;
+            }
+        }
+
+        int x0 = currentState.cursorX;
+        for (int y = 0; y < cellRows; y++) {
+            for (int x = 0; x < cellColumns; x++) {
+                printCharacter(' ');
+                cursorLeft(1, false);
+                if ((x == cellColumns - 1) || (y == cellRows - 1)) {
+                    // TODO: render text of current cell first, then image
+                    // over it.  For now, just copy the cell.
+                    DisplayLine line = display.get(currentState.cursorY);
+                    line.replace(currentState.cursorX, cells[x][y]);
+                } else {
+                    // Copy the image cell into the display.
+                    DisplayLine line = display.get(currentState.cursorY);
+                    line.replace(currentState.cursorX, cells[x][y]);
+                }
+                cursorRight(1, false);
+            }
+            linefeed();
+            cursorPosition(currentState.cursorY, x0);
+        }
+
+    }
+
 }
diff --git a/src/jexer/tterminal/Sixel.java b/src/jexer/tterminal/Sixel.java
new file mode 100644 (file)
index 0000000..8d8429b
--- /dev/null
@@ -0,0 +1,523 @@
+/*
+ * 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,
+        QUOTE,
+        COLOR_ENTRY,
+        COLOR_PARAM,
+        COLOR_PIXELS,
+        SIXEL_REPEAT,
+    }
+
+    // ------------------------------------------------------------------------
+    // Variables --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * If true, enable debug messages.
+     */
+    private static boolean DEBUG = true;
+
+    /**
+     * 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;
+
+    /**
+     * Current scanning state.
+     */
+    private ScanState scanState = ScanState.GROUND;
+
+    /**
+     * Parameter characters being collected.
+     */
+    private ArrayList<Integer> colorParams;
+
+    /**
+     * 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 repeat count.
+     */
+    private int repeatCount = -1;
+
+    /**
+     * The current drawing x position.
+     */
+    private int x = 0;
+
+    /**
+     * The current drawing color.
+     */
+    private Color color = Color.BLACK;
+
+    // ------------------------------------------------------------------------
+    // Constructors -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Public constructor.
+     *
+     * @param buffer the sixel data to parse
+     */
+    public Sixel(final String buffer) {
+        this.buffer = buffer;
+        colorParams = new ArrayList<Integer>();
+        palette = new HashMap<Integer, Color>();
+        image = new BufferedImage(200, 100, BufferedImage.TYPE_INT_ARGB);
+        for (int i = 0; i < buffer.length(); i++) {
+            consume(buffer.charAt(i));
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // Sixel ------------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Get the image.
+     *
+     * @return the sixel data as an image.
+     */
+    public BufferedImage getImage() {
+        if ((width > 0) && (height > 0)) {
+            return image.getSubimage(0, 0, width, height);
+        }
+        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);
+
+        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() {
+        colorParams.clear();
+        scanState = ScanState.GROUND;
+        repeatCount = -1;
+    }
+
+    /**
+     * Save a byte into the color parameters buffer.
+     *
+     * @param ch byte to save
+     */
+    private void param(final byte ch) {
+        if (colorParams.size() == 0) {
+            colorParams.add(Integer.valueOf(0));
+        }
+        Integer n = colorParams.get(colorParams.size() - 1);
+        if ((ch >= '0') && (ch <= '9')) {
+            n *= 10;
+            n += (ch - '0');
+            colorParams.set(colorParams.size() - 1, n);
+        }
+
+        if ((ch == ';') && (colorParams.size() < 16)) {
+            colorParams.add(Integer.valueOf(0));
+        }
+    }
+
+    /**
+     * 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 getColorParam(final int position, final int defaultValue) {
+        if (colorParams.size() < position + 1) {
+            return defaultValue;
+        }
+        return colorParams.get(position).intValue();
+    }
+
+    /**
+     * 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 getColorParam(final int position, final int defaultValue,
+        final int minValue, final int maxValue) {
+
+        assert (minValue <= maxValue);
+        int value = getColorParam(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);
+        int rgb = color.getRGB();
+        int rep = (repeatCount == -1 ? 1 : repeatCount);
+
+        if (DEBUG) {
+            System.err.println("addSixel() rep " + rep + " char " +
+                Integer.toHexString(n) + " color " + color);
+        }
+
+        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;
+            }
+            return;
+        }
+
+        for (int i = 0; i < rep; i++) {
+            if ((n & 0x01) == 0x01) {
+                image.setRGB(x, height, rgb);
+            }
+            if ((n & 0x02) == 0x02) {
+                image.setRGB(x, height + 1, rgb);
+            }
+            if ((n & 0x04) == 0x04) {
+                image.setRGB(x, height + 2, rgb);
+            }
+            if ((n & 0x08) == 0x08) {
+                image.setRGB(x, height + 3, rgb);
+            }
+            if ((n & 0x10) == 0x10) {
+                image.setRGB(x, height + 4, rgb);
+            }
+            if ((n & 0x20) == 0x20) {
+                image.setRGB(x, height + 5, rgb);
+            }
+            x++;
+            if (x > width) {
+                width++;
+                assert (x == width);
+            }
+        }
+    }
+
+    /**
+     * Process a color palette change.
+     */
+    private void setPalette() {
+        int idx = getColorParam(0, 0);
+
+        if (colorParams.size() == 1) {
+            Color newColor = palette.get(idx);
+            if (newColor != null) {
+                color = newColor;
+            }
+
+            if (DEBUG) {
+                System.err.println("set color: " + color);
+            }
+            return;
+        }
+
+        int type = getColorParam(1, 0);
+        float red   = (float) (getColorParam(2, 0, 0, 100) / 100.0);
+        float green = (float) (getColorParam(3, 0, 0, 100) / 100.0);
+        float blue  = (float) (getColorParam(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);
+            }
+        }
+    }
+
+    /**
+     * 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);
+
+        switch (scanState) {
+
+        case GROUND:
+            switch (ch) {
+            case '#':
+                scanState = ScanState.COLOR_ENTRY;
+                return;
+            case '\"':
+                scanState = ScanState.QUOTE;
+                return;
+            default:
+                break;
+            }
+
+            if (ch == '!') {
+                // Repeat count
+                scanState = ScanState.SIXEL_REPEAT;
+            }
+            if (ch == '-') {
+                if (height + 6 < image.getHeight()) {
+                    // Resize the image, give us another HEIGHT_INCREASE
+                    // pixels of vertical length.
+                    resizeImage(image.getWidth(),
+                        image.getHeight() + HEIGHT_INCREASE);
+                }
+                height += 6;
+                x = 0;
+            }
+
+            if (ch == '$') {
+                x = 0;
+            }
+            return;
+
+        case QUOTE:
+            switch (ch) {
+            case '#':
+                scanState = ScanState.COLOR_ENTRY;
+                return;
+            default:
+                break;
+            }
+
+            // Ignore everything else in the quote header.
+            return;
+
+        case COLOR_ENTRY:
+            // Between decimal 63 (inclusive) and 189 (exclusive) --> pixels
+            if ((ch >= 63) && (ch < 189)) {
+                addSixel(ch);
+                return;
+            }
+
+            // 30-39, 3B           --> param, then switch to COLOR_PARAM
+            if ((ch >= '0') && (ch <= '9')) {
+                param((byte) ch);
+                scanState = ScanState.COLOR_PARAM;
+            }
+            if (ch == ';') {
+                param((byte) ch);
+                scanState = ScanState.COLOR_PARAM;
+            }
+
+            if (ch == '#') {
+                // Next color is here, parse what we had before.
+                setPalette();
+                toGround();
+            }
+
+            if (ch == '!') {
+                setPalette();
+                toGround();
+
+                // Repeat count
+                scanState = ScanState.SIXEL_REPEAT;
+            }
+            if (ch == '-') {
+                setPalette();
+                toGround();
+
+                if (height + 6 < image.getHeight()) {
+                    // Resize the image, give us another HEIGHT_INCREASE
+                    // pixels of vertical length.
+                    resizeImage(image.getWidth(),
+                        image.getHeight() + HEIGHT_INCREASE);
+                }
+                height += 6;
+                x = 0;
+            }
+
+            if (ch == '$') {
+                setPalette();
+                toGround();
+
+                x = 0;
+            }
+            return;
+
+        case COLOR_PARAM:
+
+            // Between decimal 63 (inclusive) and 189 (exclusive) --> pixels
+            if ((ch >= 63) && (ch < 189)) {
+                addSixel(ch);
+                return;
+            }
+
+            // 30-39, 3B           --> param, then switch to COLOR_PARAM
+            if ((ch >= '0') && (ch <= '9')) {
+                param((byte) ch);
+            }
+            if (ch == ';') {
+                param((byte) ch);
+            }
+
+            if (ch == '#') {
+                // Next color is here, parse what we had before.
+                setPalette();
+                toGround();
+                scanState = ScanState.COLOR_ENTRY;
+            }
+
+            if (ch == '!') {
+                setPalette();
+                toGround();
+
+                // Repeat count
+                scanState = ScanState.SIXEL_REPEAT;
+            }
+            if (ch == '-') {
+                setPalette();
+                toGround();
+
+                if (height + 6 < image.getHeight()) {
+                    // Resize the image, give us another HEIGHT_INCREASE
+                    // pixels of vertical length.
+                    resizeImage(image.getWidth(),
+                        image.getHeight() + HEIGHT_INCREASE);
+                }
+                height += 6;
+                x = 0;
+            }
+
+            if (ch == '$') {
+                setPalette();
+                toGround();
+
+                x = 0;
+            }
+            return;
+
+        case SIXEL_REPEAT:
+
+            // Between decimal 63 (inclusive) and 189 (exclusive) --> pixels
+            if ((ch >= 63) && (ch < 189)) {
+                addSixel(ch);
+                toGround();
+            }
+
+            if ((ch >= '0') && (ch <= '9')) {
+                if (repeatCount == -1) {
+                    repeatCount = (int) (ch - '0');
+                } else {
+                    repeatCount *= 10;
+                    repeatCount += (int) (ch - '0');
+                }
+            }
+
+            if (ch == '#') {
+                // Next color.
+                toGround();
+                scanState = ScanState.COLOR_ENTRY;
+            }
+
+            return;
+        }
+
+    }
+
+}