+ // ------------------------------------------------------------------------
+ // iTerm2 image output support --------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create an iTerm2 images string representing a row of several cells
+ * containing bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cells the cells containing the bitmap data
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String toIterm2Image(final int x, final int y,
+ final ArrayList<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();
+
+ assert (cells != null);
+ assert (cells.size() > 0);
+ assert (cells.get(0).getImage() != null);
+
+ if (iterm2Images == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ if (iterm2Cache == null) {
+ iterm2Cache = new ImageCache(height * 10);
+ }
+
+ // Save and get rows to/from the cache that do NOT have inverted
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = iterm2Cache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(gotoXY(x, y));
+ sb.append(cachedResult);
+ return sb.toString();
+ }
+ // System.err.println("CACHE MISS");
+ }
+
+ BufferedImage image = cellsToImage(cells);
+ int fullHeight = image.getHeight();
+
+ /*
+ * From https://iterm2.com/documentation-images.html:
+ *
+ * Protocol
+ *
+ * iTerm2 extends the xterm protocol with a set of proprietary escape
+ * sequences. In general, the pattern is:
+ *
+ * ESC ] 1337 ; key = value ^G
+ *
+ * Whitespace is shown here for ease of reading: in practice, no
+ * spaces should be used.
+ *
+ * For file transfer and inline images, the code is:
+ *
+ * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
+ *
+ * The optional arguments are formatted as key=value with a semicolon
+ * between each key-value pair. They are described below:
+ *
+ * Key Description of value
+ * name base-64 encoded filename. Defaults to "Unnamed file".
+ * size File size in bytes. Optional; this is only used by the
+ * progress indicator.
+ * width Width to render. See notes below.
+ * height Height to render. See notes below.
+ * preserveAspectRatio If set to 0, then the image's inherent aspect
+ * ratio will not be respected; otherwise, it
+ * will fill the specified width and height as
+ * much as possible without stretching. Defaults
+ * to 1.
+ * inline If set to 1, the file will be displayed inline. Otherwise,
+ * it will be downloaded with no visual representation in the
+ * terminal session. Defaults to 0.
+ *
+ * The width and height are given as a number followed by a unit, or
+ * the word "auto".
+ *
+ * N: N character cells.
+ * Npx: N pixels.
+ * N%: N percent of the session's width or height.
+ * auto: The image's inherent size will be used to determine an
+ * appropriate dimension.
+ *
+ */
+
+ // File contents can be several image formats. We will use PNG.
+ ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
+ try {
+ if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(),
+ Math.min(image.getHeight(), fullHeight)),
+ "PNG", pngOutputStream)
+ ) {
+ // We failed to render image, bail out.
+ return "";
+ }
+ } catch (IOException e) {
+ // We failed to render image, bail out.
+ return "";
+ }
+
+ sb.append("\033]1337;File=");
+ /*
+ sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
+ cells.size()));
+ */
+ /*
+ sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
+ image.getWidth(), Math.min(image.getHeight(),
+ getTextHeight())));
+ */
+ sb.append("inline=1:");
+ sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
+ sb.append("\007");
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ iterm2Cache.put(cells, sb.toString());
+ }
+
+ return (gotoXY(x, y) + sb.toString());
+ }
+
+ /**
+ * Get the iTerm2 images support flag.
+ *
+ * @return true if this terminal is emitting iTerm2 images
+ */
+ public boolean hasIterm2Images() {
+ return iterm2Images;
+ }
+
+ // ------------------------------------------------------------------------
+ // End iTerm2 image output support ----------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Jexer image output support ---------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create a Jexer images string representing a row of several cells
+ * containing bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cells the cells containing the bitmap data
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String toJexerImage(final int x, final int y,
+ final ArrayList<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();
+
+ assert (cells != null);
+ assert (cells.size() > 0);
+ assert (cells.get(0).getImage() != null);
+
+ if (jexerImageOption == JexerImageOption.DISABLED) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ if (jexerCache == null) {
+ jexerCache = new ImageCache(height * 10);
+ }
+
+ // Save and get rows to/from the cache that do NOT have inverted
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = jexerCache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(gotoXY(x, y));
+ sb.append(cachedResult);
+ return sb.toString();
+ }
+ // System.err.println("CACHE MISS");
+ }
+
+ BufferedImage image = cellsToImage(cells);
+ int fullHeight = image.getHeight();
+
+ if (jexerImageOption == JexerImageOption.PNG) {
+ // Encode as PNG
+ ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
+ try {
+ if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(),
+ Math.min(image.getHeight(), fullHeight)),
+ "PNG", pngOutputStream)
+ ) {
+ // We failed to render image, bail out.
+ return "";
+ }
+ } catch (IOException e) {
+ // We failed to render image, bail out.
+ return "";
+ }
+
+ sb.append("\033]444;1;0;");
+ sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
+ sb.append("\007");
+
+ } else if (jexerImageOption == JexerImageOption.JPG) {
+
+ // Encode as JPG
+ ByteArrayOutputStream jpgOutputStream = new ByteArrayOutputStream(1024);
+
+ // Convert from ARGB to RGB, otherwise the JPG encode will fail.
+ BufferedImage jpgImage = new BufferedImage(image.getWidth(),
+ image.getHeight(), BufferedImage.TYPE_INT_RGB);
+ int [] pixels = new int[image.getWidth() * image.getHeight()];
+ image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
+ 0, image.getWidth());
+ jpgImage.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels,
+ 0, image.getWidth());
+
+ try {
+ if (!ImageIO.write(jpgImage.getSubimage(0, 0,
+ jpgImage.getWidth(),
+ Math.min(jpgImage.getHeight(), fullHeight)),
+ "JPG", jpgOutputStream)
+ ) {
+ // We failed to render image, bail out.
+ return "";
+ }
+ } catch (IOException e) {
+ // We failed to render image, bail out.
+ return "";
+ }
+
+ sb.append("\033]444;2;0;");
+ sb.append(StringUtils.toBase64(jpgOutputStream.toByteArray()));
+ sb.append("\007");
+
+ } else if (jexerImageOption == JexerImageOption.RGB) {
+
+ // RGB
+ sb.append(String.format("\033]444;0;%d;%d;0;", image.getWidth(),
+ Math.min(image.getHeight(), fullHeight)));
+
+ byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3];
+ int stride = image.getWidth();
+ for (int px = 0; px < stride; px++) {
+ for (int py = 0; py < image.getHeight(); py++) {
+ int rgb = image.getRGB(px, py);
+ bytes[(py * stride * 3) + (px * 3)] = (byte) ((rgb >>> 16) & 0xFF);
+ bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>> 8) & 0xFF);
+ bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb & 0xFF);
+ }
+ }
+ sb.append(StringUtils.toBase64(bytes));
+ sb.append("\007");
+ }
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ jexerCache.put(cells, sb.toString());
+ }
+
+ return (gotoXY(x, y) + sb.toString());
+ }
+
+ /**
+ * Get the Jexer images support flag.
+ *
+ * @return true if this terminal is emitting Jexer images
+ */
+ public boolean hasJexerImages() {
+ return (jexerImageOption != JexerImageOption.DISABLED);
+ }
+
+ // ------------------------------------------------------------------------
+ // End Jexer image output support -----------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Setup system colors to match DOS color palette.
+ */
+ private void setDOSColors() {
+ MYBLACK = new java.awt.Color(0x00, 0x00, 0x00);
+ MYRED = new java.awt.Color(0xa8, 0x00, 0x00);
+ MYGREEN = new java.awt.Color(0x00, 0xa8, 0x00);
+ MYYELLOW = new java.awt.Color(0xa8, 0x54, 0x00);
+ MYBLUE = new java.awt.Color(0x00, 0x00, 0xa8);
+ MYMAGENTA = new java.awt.Color(0xa8, 0x00, 0xa8);
+ MYCYAN = new java.awt.Color(0x00, 0xa8, 0xa8);
+ MYWHITE = new java.awt.Color(0xa8, 0xa8, 0xa8);
+ MYBOLD_BLACK = new java.awt.Color(0x54, 0x54, 0x54);
+ MYBOLD_RED = new java.awt.Color(0xfc, 0x54, 0x54);
+ MYBOLD_GREEN = new java.awt.Color(0x54, 0xfc, 0x54);
+ MYBOLD_YELLOW = new java.awt.Color(0xfc, 0xfc, 0x54);
+ MYBOLD_BLUE = new java.awt.Color(0x54, 0x54, 0xfc);
+ MYBOLD_MAGENTA = new java.awt.Color(0xfc, 0x54, 0xfc);
+ MYBOLD_CYAN = new java.awt.Color(0x54, 0xfc, 0xfc);
+ MYBOLD_WHITE = new java.awt.Color(0xfc, 0xfc, 0xfc);
+ }
+
+ /**
+ * Setup ECMA48 colors to match those provided in system properties.
+ */
+ private void setCustomSystemColors() {
+ setDOSColors();
+
+ MYBLACK = getCustomColor("jexer.ECMA48.color0", MYBLACK);
+ MYRED = getCustomColor("jexer.ECMA48.color1", MYRED);
+ MYGREEN = getCustomColor("jexer.ECMA48.color2", MYGREEN);
+ MYYELLOW = getCustomColor("jexer.ECMA48.color3", MYYELLOW);
+ MYBLUE = getCustomColor("jexer.ECMA48.color4", MYBLUE);
+ MYMAGENTA = getCustomColor("jexer.ECMA48.color5", MYMAGENTA);
+ MYCYAN = getCustomColor("jexer.ECMA48.color6", MYCYAN);
+ MYWHITE = getCustomColor("jexer.ECMA48.color7", MYWHITE);
+ MYBOLD_BLACK = getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK);
+ MYBOLD_RED = getCustomColor("jexer.ECMA48.color9", MYBOLD_RED);
+ MYBOLD_GREEN = getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN);
+ MYBOLD_YELLOW = getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW);
+ MYBOLD_BLUE = getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE);
+ MYBOLD_MAGENTA = getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA);
+ MYBOLD_CYAN = getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN);
+ MYBOLD_WHITE = getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE);
+ }
+
+ /**
+ * Setup one system color to match the RGB value provided in system
+ * properties.
+ *
+ * @param key the system property key
+ * @param defaultColor the default color to return if key is not set, or
+ * incorrect
+ * @return a color from the RGB string, or defaultColor
+ */
+ private java.awt.Color getCustomColor(final String key,
+ final java.awt.Color defaultColor) {
+
+ String rgb = System.getProperty(key);
+ if (rgb == null) {
+ return defaultColor;
+ }
+ if (rgb.startsWith("#")) {
+ rgb = rgb.substring(1);
+ }
+ int rgbInt = 0;
+ try {
+ rgbInt = Integer.parseInt(rgb, 16);
+ } catch (NumberFormatException e) {
+ return defaultColor;
+ }
+ java.awt.Color color = new java.awt.Color((rgbInt & 0xFF0000) >>> 16,
+ (rgbInt & 0x00FF00) >>> 8,
+ (rgbInt & 0x0000FF));
+
+ return color;
+ }
+
+ /**
+ * Create a T.416 RGB parameter sequence for a custom system color.
+ *
+ * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
+ * @return the color portion of the string to emit to an ANSI /
+ * ECMA-style terminal
+ */
+ private String systemColorRGB(final java.awt.Color color) {
+ return String.format("%d;%d;%d", color.getRed(), color.getGreen(),
+ color.getBlue());
+ }
+