X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fbackend%2FECMA48Terminal.java;h=e2997d2f6b17486356ddd9d902d114039258bdef;hb=12b90437b5f22c2ae6e9b9b14c3b62b60f6143e5;hp=9884835591adba9cfa57530cca3759ee8fb6404e;hpb=739ada621a9220c1764172118efe68d3e26db18a;p=nikiroo-utils.git diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index 9884835..e2997d2 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -30,6 +30,7 @@ package jexer.backend; import java.awt.image.BufferedImage; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.InputStream; @@ -44,6 +45,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import javax.imageio.ImageIO; import jexer.TImage; import jexer.bits.Cell; @@ -195,7 +197,7 @@ public class ECMA48Terminal extends LogicalScreen /** * The sixel post-rendered string cache. */ - private SixelCache sixelCache = null; + private ImageCache sixelCache = null; /** * Number of colors in the sixel palette. Xterm 335 defines the max as @@ -204,10 +206,40 @@ public class ECMA48Terminal extends LogicalScreen */ private int sixelPaletteSize = 1024; + /** + * If true, emit image data via iTerm2 image protocol. + */ + private boolean iterm2Images = false; + + /** + * The iTerm2 post-rendered string cache. + */ + private ImageCache iterm2Cache = null; + + /** + * If true, emit image data via Jexer image protocol. + */ + private boolean jexerImages = false; + + /** + * The Jexer post-rendered string cache. + */ + private ImageCache jexerCache = null; + + /** + * Base64 encoder used by iTerm2 and Jexer images. + */ + private java.util.Base64.Encoder base64 = null; + /** * If true, then we changed System.in and need to change it back. */ - private boolean setRawMode; + private boolean setRawMode = false; + + /** + * If true, '?' was seen in terminal response. + */ + private boolean decPrivateModeFlag = false; /** * The terminal's input. If an InputStream is not specified in the @@ -886,10 +918,10 @@ public class ECMA48Terminal extends LogicalScreen } /** - * SixelCache is a least-recently-used cache that hangs on to the - * post-rendered sixel string for a particular set of cells. + * ImageCache is a least-recently-used cache that hangs on to the + * post-rendered sixel or iTerm2 string for a particular set of cells. */ - private class SixelCache { + private class ImageCache { /** * Maximum size of the cache. @@ -938,7 +970,7 @@ public class ECMA48Terminal extends LogicalScreen * * @param maxSize the maximum size of the cache */ - public SixelCache(final int maxSize) { + public ImageCache(final int maxSize) { this.maxSize = maxSize; cache = new HashMap(); } @@ -1116,13 +1148,19 @@ public class ECMA48Terminal extends LogicalScreen "UTF-8")); } - // Request xterm report window dimensions in pixels - this.output.printf("%s", xtermReportWindowPixelDimensions()); + // Request Device Attributes + this.output.printf("\033[c"); + + // Request xterm report window/cell dimensions in pixels + this.output.printf("%s", xtermReportPixelDimensions()); // Enable mouse reporting and metaSendsEscape this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); this.output.flush(); + // Request xterm use the sixel settings we want + this.output.printf("%s", xtermSetSixelSettings()); + // Query the screen size sessionInfo.queryWindowSize(); setDimensions(sessionInfo.getWindowWidth(), @@ -1202,13 +1240,19 @@ public class ECMA48Terminal extends LogicalScreen this.output = writer; - // Request xterm report window dimensions in pixels - this.output.printf("%s", xtermReportWindowPixelDimensions()); + // Request Device Attributes + this.output.printf("\033[c"); + + // Request xterm report window/cell dimensions in pixels + this.output.printf("%s", xtermReportPixelDimensions()); // Enable mouse reporting and metaSendsEscape this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); this.output.flush(); + // Request xterm use the sixel settings we want + this.output.printf("%s", xtermSetSixelSettings()); + // Query the screen size sessionInfo.queryWindowSize(); setDimensions(sessionInfo.getWindowWidth(), @@ -1349,7 +1393,8 @@ public class ECMA48Terminal extends LogicalScreen // Disable mouse reporting and show cursor. Defensive null check // here in case closeTerminal() is called twice. if (output != null) { - output.printf("%s%s%s", mouse(false), cursor(true), defaultColor()); + output.printf("%s%s%s%s", mouse(false), cursor(true), + defaultColor(), xtermResetSixelSettings()); output.flush(); } @@ -1398,7 +1443,7 @@ public class ECMA48Terminal extends LogicalScreen doRgbColor = false; } - // Default to using sixel for full-width characters. + // Default to using images for full-width characters. if (System.getProperty("jexer.ECMA48.wideCharImages", "true").equals("true")) { wideCharImages = true; @@ -1434,6 +1479,14 @@ public class ECMA48Terminal extends LogicalScreen // SQUASH } + // Default to using images for full-width characters. + if (System.getProperty("jexer.ECMA48.iTerm2Images", + "false").equals("true")) { + iterm2Images = true; + } else { + iterm2Images = false; + } + // Set custom colors setCustomSystemColors(); } @@ -1951,7 +2004,7 @@ public class ECMA48Terminal extends LogicalScreen } /* - * For sixel support, draw all of the sixel output first, and then + * For images support, draw all of the image output first, and then * draw everything else afterwards. This works OK, but performance * is still a drag on larger pictures. */ @@ -1997,7 +2050,13 @@ public class ECMA48Terminal extends LogicalScreen physical[x + i][y].setTo(lCell); } if (cellsToDraw.size() > 0) { - sb.append(toSixel(x, y, cellsToDraw)); + if (iterm2Images) { + sb.append(toIterm2Image(x, y, cellsToDraw)); + } else if (jexerImages) { + sb.append(toJexerImage(x, y, cellsToDraw)); + } else { + sb.append(toSixel(x, y, cellsToDraw)); + } } x = right; @@ -2026,6 +2085,7 @@ public class ECMA48Terminal extends LogicalScreen params = new ArrayList(); params.clear(); params.add(""); + decPrivateModeFlag = false; } /** @@ -2372,7 +2432,7 @@ public class ECMA48Terminal extends LogicalScreen getTextWidth() + " x " + getTextHeight()); } - this.output.printf("%s", xtermReportWindowPixelDimensions()); + this.output.printf("%s", xtermReportPixelDimensions()); this.output.flush(); TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN, @@ -2608,6 +2668,10 @@ public class ECMA48Terminal extends LogicalScreen // Mouse position, SGR (1006) coordinates state = ParseState.MOUSE_SGR; return; + case '?': + // DEC private mode flag + decPrivateModeFlag = true; + return; default: break; } @@ -2737,6 +2801,27 @@ public class ECMA48Terminal extends LogicalScreen events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift)); resetParser(); return; + case 'c': + // Device Attributes + if (decPrivateModeFlag == false) { + break; + } + for (String x: params) { + if (x.equals("4")) { + // Terminal reports sixel support + if (debugToStderr) { + System.err.println("Device Attributes: sixel"); + } + } + if (x.equals("444")) { + // Terminal reports Jexer images support + if (debugToStderr) { + System.err.println("Device Attributes: Jexer images"); + } + jexerImages = true; + } + } + return; case 't': // windowOps if ((params.size() > 2) && (params.get(0).equals("4"))) { @@ -2810,11 +2895,39 @@ public class ECMA48Terminal extends LogicalScreen } /** - * Request (u)xterm to report the current window size dimensions. + * Request (u)xterm to use the sixel settings we need: + * + * - enable sixel scrolling + * + * - disable private color registers (so that we can use one common + * palette) + * + * @return the string to emit to xterm + */ + private String xtermSetSixelSettings() { + return "\033[?80h\033[?1070l"; + } + + /** + * Restore (u)xterm its default sixel settings: + * + * - enable sixel scrolling + * + * - enable private color registers + * + * @return the string to emit to xterm + */ + private String xtermResetSixelSettings() { + return "\033[?80h\033[?1070h"; + } + + /** + * Request (u)xterm to report the current window and cell size dimensions + * in pixels. * * @return the string to emit to xterm */ - private String xtermReportWindowPixelDimensions() { + private String xtermReportPixelDimensions() { // We will ask for both window and text cell dimensions, and // hopefully one of them will work. return "\033[14t\033[16t"; @@ -2909,6 +3022,8 @@ public class ECMA48Terminal extends LogicalScreen if (palette == null) { palette = new SixelPalette(); + // TODO: make this an option (shared palette or not) + palette.emitPalette(sb, null); } return sb.toString(); @@ -2953,8 +3068,22 @@ public class ECMA48Terminal extends LogicalScreen return sb.toString(); } + if (y == height - 1) { + // We are on the bottom row. If scrolling mode is enabled + // (default), then VT320/xterm will scroll the entire screen if + // we draw any pixels here. + + // TODO: support sixel scrolling mode disabled as an option. + sb.append(normal()); + sb.append(gotoXY(x, y)); + for (int j = 0; j < cells.size(); j++) { + sb.append(' '); + } + return sb.toString(); + } + if (sixelCache == null) { - sixelCache = new SixelCache(height * 10); + sixelCache = new ImageCache(height * 10); } // Save and get rows to/from the cache that do NOT have inverted @@ -2999,6 +3128,7 @@ public class ECMA48Terminal extends LogicalScreen imageWidth); int tileHeight = Math.min(cells.get(i).getImage().getHeight(), imageHeight); + if (false && cells.get(i).isInvertedImage()) { // I used to put an all-white cell over the cursor, don't do // that anymore. @@ -3080,9 +3210,19 @@ public class ECMA48Terminal extends LogicalScreen // Dither the image. It is ok to lose the original here. if (palette == null) { palette = new SixelPalette(); + // TODO: make this an option (shared palette or not) + palette.emitPalette(sb, null); } image = palette.ditherImage(image); + // Collect the raster information + int rasterHeight = 0; + int rasterWidth = image.getWidth(); + + /* + + // TODO: make this an option (shared palette or not) + // Emit the palette, but only for the colors actually used by these // cells. boolean [] usedColors = new boolean[sixelPaletteSize]; @@ -3092,6 +3232,7 @@ public class ECMA48Terminal extends LogicalScreen } } palette.emitPalette(sb, usedColors); + */ // Render the entire row of cells. for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) { @@ -3159,6 +3300,9 @@ public class ECMA48Terminal extends LogicalScreen data += 32; break; } + if ((currentRow + j + 1) > rasterHeight) { + rasterHeight = currentRow + j + 1; + } } } assert (data >= 0); @@ -3198,6 +3342,9 @@ public class ECMA48Terminal extends LogicalScreen // Kill the very last "-", because it is unnecessary. sb.deleteCharAt(sb.length() - 1); + // Add the raster information + sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight)); + if (saveInCache) { // This row is OK to save into the cache. sixelCache.put(cells, sb.toString()); @@ -3219,6 +3366,451 @@ public class ECMA48Terminal extends LogicalScreen // End sixel output support ----------------------------------------------- // ------------------------------------------------------------------------ + // ------------------------------------------------------------------------ + // 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 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); + base64 = java.util.Base64.getEncoder(); + } + + // 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"); + } + + int imageWidth = cells.get(0).getImage().getWidth(); + int imageHeight = cells.get(0).getImage().getHeight(); + + // cells.get(x).getImage() has a dithered bitmap containing indexes + // into the color palette. Piece these together into one larger + // image for final rendering. + int totalWidth = 0; + int fullWidth = cells.size() * getTextWidth(); + int fullHeight = getTextHeight(); + for (int i = 0; i < cells.size(); i++) { + totalWidth += cells.get(i).getImage().getWidth(); + } + + BufferedImage image = new BufferedImage(fullWidth, + fullHeight, BufferedImage.TYPE_INT_ARGB); + + int [] rgbArray; + for (int i = 0; i < cells.size() - 1; i++) { + int tileWidth = Math.min(cells.get(i).getImage().getWidth(), + imageWidth); + int tileHeight = Math.min(cells.get(i).getImage().getHeight(), + imageHeight); + if (false && cells.get(i).isInvertedImage()) { + // I used to put an all-white cell over the cursor, don't do + // that anymore. + rgbArray = new int[imageWidth * imageHeight]; + for (int j = 0; j < rgbArray.length; j++) { + rgbArray[j] = 0xFFFFFF; + } + } else { + try { + rgbArray = cells.get(i).getImage().getRGB(0, 0, + tileWidth, tileHeight, null, 0, tileWidth); + } catch (Exception e) { + throw new RuntimeException("image " + imageWidth + "x" + + imageHeight + + "tile " + tileWidth + "x" + + tileHeight + + " cells.get(i).getImage() " + + cells.get(i).getImage() + + " i " + i + + " fullWidth " + fullWidth + + " fullHeight " + fullHeight, e); + } + } + + /* + System.err.printf("calling image.setRGB(): %d %d %d %d %d\n", + i * imageWidth, 0, imageWidth, imageHeight, + 0, imageWidth); + System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n", + fullWidth, fullHeight, cells.size(), getTextWidth()); + */ + + image.setRGB(i * imageWidth, 0, tileWidth, tileHeight, + rgbArray, 0, tileWidth); + if (tileHeight < fullHeight) { + int backgroundColor = cells.get(i).getBackground().getRGB(); + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int imageY = imageHeight; imageY < fullHeight; + imageY++) { + + image.setRGB(imageX, imageY, backgroundColor); + } + } + } + } + totalWidth -= ((cells.size() - 1) * imageWidth); + if (false && cells.get(cells.size() - 1).isInvertedImage()) { + // I used to put an all-white cell over the cursor, don't do that + // anymore. + rgbArray = new int[totalWidth * imageHeight]; + for (int j = 0; j < rgbArray.length; j++) { + rgbArray[j] = 0xFFFFFF; + } + } else { + try { + rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, + totalWidth, imageHeight, null, 0, totalWidth); + } catch (Exception e) { + throw new RuntimeException("image " + imageWidth + "x" + + imageHeight + " cells.get(cells.size() - 1).getImage() " + + cells.get(cells.size() - 1).getImage(), e); + } + } + image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, + imageHeight, rgbArray, 0, totalWidth); + + if (totalWidth < getTextWidth()) { + int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB(); + + for (int imageX = image.getWidth() - totalWidth; + imageX < image.getWidth(); imageX++) { + + for (int imageY = 0; imageY < fullHeight; imageY++) { + image.setRGB(imageX, imageY, backgroundColor); + } + } + } + + /* + * 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 ""; + } + + // iTerm2 does not advance the cursor automatically, so place it + // myself. + 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(base64.encodeToString(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 cells) { + + StringBuilder sb = new StringBuilder(); + + assert (cells != null); + assert (cells.size() > 0); + assert (cells.get(0).getImage() != null); + + if (jexerImages == false) { + 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); + base64 = java.util.Base64.getEncoder(); + } + + // 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"); + } + + int imageWidth = cells.get(0).getImage().getWidth(); + int imageHeight = cells.get(0).getImage().getHeight(); + + // cells.get(x).getImage() has a dithered bitmap containing indexes + // into the color palette. Piece these together into one larger + // image for final rendering. + int totalWidth = 0; + int fullWidth = cells.size() * getTextWidth(); + int fullHeight = getTextHeight(); + for (int i = 0; i < cells.size(); i++) { + totalWidth += cells.get(i).getImage().getWidth(); + } + + BufferedImage image = new BufferedImage(fullWidth, + fullHeight, BufferedImage.TYPE_INT_ARGB); + + int [] rgbArray; + for (int i = 0; i < cells.size() - 1; i++) { + int tileWidth = Math.min(cells.get(i).getImage().getWidth(), + imageWidth); + int tileHeight = Math.min(cells.get(i).getImage().getHeight(), + imageHeight); + if (false && cells.get(i).isInvertedImage()) { + // I used to put an all-white cell over the cursor, don't do + // that anymore. + rgbArray = new int[imageWidth * imageHeight]; + for (int j = 0; j < rgbArray.length; j++) { + rgbArray[j] = 0xFFFFFF; + } + } else { + try { + rgbArray = cells.get(i).getImage().getRGB(0, 0, + tileWidth, tileHeight, null, 0, tileWidth); + } catch (Exception e) { + throw new RuntimeException("image " + imageWidth + "x" + + imageHeight + + "tile " + tileWidth + "x" + + tileHeight + + " cells.get(i).getImage() " + + cells.get(i).getImage() + + " i " + i + + " fullWidth " + fullWidth + + " fullHeight " + fullHeight, e); + } + } + + /* + System.err.printf("calling image.setRGB(): %d %d %d %d %d\n", + i * imageWidth, 0, imageWidth, imageHeight, + 0, imageWidth); + System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n", + fullWidth, fullHeight, cells.size(), getTextWidth()); + */ + + image.setRGB(i * imageWidth, 0, tileWidth, tileHeight, + rgbArray, 0, tileWidth); + if (tileHeight < fullHeight) { + int backgroundColor = cells.get(i).getBackground().getRGB(); + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int imageY = imageHeight; imageY < fullHeight; + imageY++) { + + image.setRGB(imageX, imageY, backgroundColor); + } + } + } + } + totalWidth -= ((cells.size() - 1) * imageWidth); + if (false && cells.get(cells.size() - 1).isInvertedImage()) { + // I used to put an all-white cell over the cursor, don't do that + // anymore. + rgbArray = new int[totalWidth * imageHeight]; + for (int j = 0; j < rgbArray.length; j++) { + rgbArray[j] = 0xFFFFFF; + } + } else { + try { + rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, + totalWidth, imageHeight, null, 0, totalWidth); + } catch (Exception e) { + throw new RuntimeException("image " + imageWidth + "x" + + imageHeight + " cells.get(cells.size() - 1).getImage() " + + cells.get(cells.size() - 1).getImage(), e); + } + } + image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, + imageHeight, rgbArray, 0, totalWidth); + + if (totalWidth < getTextWidth()) { + int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB(); + + for (int imageX = image.getWidth() - totalWidth; + imageX < image.getWidth(); imageX++) { + + for (int imageY = 0; imageY < fullHeight; imageY++) { + image.setRGB(imageX, imageY, backgroundColor); + } + } + } + + sb.append(String.format("\033]444;%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(base64.encodeToString(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 jexerImages; + } + + // ------------------------------------------------------------------------ + // End Jexer image output support ----------------------------------------- + // ------------------------------------------------------------------------ + /** * Setup system colors to match DOS color palette. */