*/
package jexer.backend;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.util.List;
import javax.imageio.ImageIO;
-import jexer.TImage;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
import jexer.bits.Color;
+import jexer.bits.StringUtils;
import jexer.event.TCommandEvent;
import jexer.event.TInputEvent;
import jexer.event.TKeypressEvent;
MOUSE_SGR,
}
+ /**
+ * Available Jexer images support.
+ */
+ private enum JexerImageOption {
+ DISABLED,
+ JPG,
+ PNG,
+ RGB,
+ }
+
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
*/
private boolean sixel = true;
+ /**
+ * If true, use a single shared palette for sixel.
+ */
+ private boolean sixelSharedPalette = true;
+
/**
* The sixel palette handler.
*/
private ImageCache iterm2Cache = null;
/**
- * Base64 encoder used by iTerm2 images.
+ * If not DISABLED, emit image data via Jexer image protocol if the
+ * terminal supports it.
*/
- private java.util.Base64.Encoder base64 = null;
+ private JexerImageOption jexerImageOption = JexerImageOption.JPG;
+
+ /**
+ * The Jexer post-rendered string cache.
+ */
+ private ImageCache jexerCache = null;
/**
* If true, then we changed System.in and need to change it back.
*/
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
* constructor, then this InputStreamReader will be bound to System.in
"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));
+
+ // Request xterm use the sixel settings we want
+ this.output.printf("%s", xtermSetSixelSettings());
+
this.output.flush();
// Query the screen size
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));
+
+ // Request xterm use the sixel settings we want
+ this.output.printf("%s", xtermSetSixelSettings());
+
this.output.flush();
// Query the screen size
// 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();
}
// SQUASH
}
- // Default to using images for full-width characters.
+ // Shared palette
+ if (System.getProperty("jexer.ECMA48.sixelSharedPalette",
+ "true").equals("false")) {
+ sixelSharedPalette = false;
+ } else {
+ sixelSharedPalette = true;
+ }
+
+ // Default to not supporting iTerm2 images.
if (System.getProperty("jexer.ECMA48.iTerm2Images",
"false").equals("true")) {
iterm2Images = true;
iterm2Images = false;
}
+ // Default to using JPG Jexer images if terminal supports it.
+ String jexerImageStr = System.getProperty("jexer.ECMA48.jexerImages",
+ "jpg").toLowerCase();
+ if (jexerImageStr.equals("false")) {
+ jexerImageOption = JexerImageOption.DISABLED;
+ } else if (jexerImageStr.equals("jpg")) {
+ jexerImageOption = JexerImageOption.JPG;
+ } else if (jexerImageStr.equals("png")) {
+ jexerImageOption = JexerImageOption.PNG;
+ } else if (jexerImageStr.equals("rgb")) {
+ jexerImageOption = JexerImageOption.RGB;
+ }
+
// Set custom colors
setCustomSystemColors();
}
* @return the width in pixels of a character cell
*/
public int getTextWidth() {
- return (widthPixels / sessionInfo.getWindowWidth());
+ if (sessionInfo.getWindowWidth() > 0) {
+ return (widthPixels / sessionInfo.getWindowWidth());
+ }
+ return 16;
}
/**
* @return the height in pixels of a character cell
*/
public int getTextHeight() {
- return (heightPixels / sessionInfo.getWindowHeight());
+ if (sessionInfo.getWindowHeight() > 0) {
+ return (heightPixels / sessionInfo.getWindowHeight());
+ }
+ return 20;
}
/**
if (cellsToDraw.size() > 0) {
if (iterm2Images) {
sb.append(toIterm2Image(x, y, cellsToDraw));
+ } else if (jexerImageOption != JexerImageOption.DISABLED) {
+ sb.append(toJexerImage(x, y, cellsToDraw));
} else {
sb.append(toSixel(x, y, cellsToDraw));
}
params = new ArrayList<String>();
params.clear();
params.add("");
+ decPrivateModeFlag = false;
}
/**
boolean eventMouse3 = false;
boolean eventMouseWheelUp = false;
boolean eventMouseWheelDown = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
// System.err.printf("buttons: %04x\r\n", buttons);
- switch (buttons) {
+ switch (buttons & 0xE3) {
case 0:
eventMouse1 = true;
mouse1 = true;
eventType = TMouseEvent.Type.MOUSE_MOTION;
break;
}
+
+ if ((buttons & 0x04) != 0) {
+ eventShift = true;
+ }
+ if ((buttons & 0x08) != 0) {
+ eventAlt = true;
+ }
+ if ((buttons & 0x10) != 0) {
+ eventCtrl = true;
+ }
+
return new TMouseEvent(eventType, x, y, x, y,
eventMouse1, eventMouse2, eventMouse3,
- eventMouseWheelUp, eventMouseWheelDown);
+ eventMouseWheelUp, eventMouseWheelDown,
+ eventAlt, eventCtrl, eventShift);
}
/**
boolean eventMouse3 = false;
boolean eventMouseWheelUp = false;
boolean eventMouseWheelDown = false;
+ boolean eventAlt = false;
+ boolean eventCtrl = false;
+ boolean eventShift = false;
if (release) {
eventType = TMouseEvent.Type.MOUSE_UP;
}
- switch (buttons) {
+ switch (buttons & 0xE3) {
case 0:
eventMouse1 = true;
break;
// Unknown, bail out
return null;
}
+
+ if ((buttons & 0x04) != 0) {
+ eventShift = true;
+ }
+ if ((buttons & 0x08) != 0) {
+ eventAlt = true;
+ }
+ if ((buttons & 0x10) != 0) {
+ eventCtrl = true;
+ }
+
return new TMouseEvent(eventType, x, y, x, y,
eventMouse1, eventMouse2, eventMouse3,
- eventMouseWheelUp, eventMouseWheelDown);
+ eventMouseWheelUp, eventMouseWheelDown,
+ eventAlt, eventCtrl, eventShift);
}
/**
getTextWidth() + " x " + getTextHeight());
}
- this.output.printf("%s", xtermReportWindowPixelDimensions());
+ this.output.printf("%s", xtermReportPixelDimensions());
this.output.flush();
TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
// Mouse position, SGR (1006) coordinates
state = ParseState.MOUSE_SGR;
return;
+ case '?':
+ // DEC private mode flag
+ decPrivateModeFlag = true;
+ return;
default:
break;
}
events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
resetParser();
return;
+ case 'c':
+ // Device Attributes
+ if (decPrivateModeFlag == false) {
+ break;
+ }
+ boolean reportsJexerImages = false;
+ boolean reportsIterm2Images = false;
+ 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");
+ }
+ reportsJexerImages = true;
+ }
+ if (x.equals("1337")) {
+ // Terminal reports iTerm2 images support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: iTerm2 images");
+ }
+ reportsIterm2Images = true;
+ }
+ }
+ if (reportsJexerImages == false) {
+ // Terminal does not support Jexer images, disable
+ // them.
+ jexerImageOption = JexerImageOption.DISABLED;
+ }
+ if (reportsIterm2Images == false) {
+ // Terminal does not support iTerm2 images, disable
+ // them.
+ iterm2Images = false;
+ }
+ resetParser();
+ return;
case 't':
// windowOps
if ((params.size() > 2) && (params.get(0).equals("4"))) {
}
/**
- * 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) if sixelSharedPalette is set
+ *
+ * @return the string to emit to xterm
+ */
+ private String xtermSetSixelSettings() {
+ if (sixelSharedPalette == true) {
+ return "\033[?80h\033[?1070l";
+ } else {
+ return "\033[?80h\033[?1070h";
+ }
+ }
+
+ /**
+ * 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";
if (palette == null) {
palette = new SixelPalette();
+ if (sixelSharedPalette == true) {
+ palette.emitPalette(sb, null);
+ }
}
return sb.toString();
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. Do not draw the image, bail out
+ // instead.
+ 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 ImageCache(height * 10);
}
// 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);
- }
- }
- }
+ BufferedImage image = cellsToImage(cells);
+ int fullHeight = image.getHeight();
// Dither the image. It is ok to lose the original here.
if (palette == null) {
palette = new SixelPalette();
+ if (sixelSharedPalette == true) {
+ palette.emitPalette(sb, null);
+ }
}
image = palette.ditherImage(image);
int rasterHeight = 0;
int rasterWidth = image.getWidth();
- // Emit the palette, but only for the colors actually used by these
- // cells.
- boolean [] usedColors = new boolean[sixelPaletteSize];
- for (int imageX = 0; imageX < image.getWidth(); imageX++) {
- for (int imageY = 0; imageY < image.getHeight(); imageY++) {
- usedColors[image.getRGB(imageX, imageY)] = true;
+ if (sixelSharedPalette == false) {
+ // Emit the palette, but only for the colors actually used by
+ // these cells.
+ boolean [] usedColors = new boolean[sixelPaletteSize];
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+ usedColors[image.getRGB(imageX, imageY)] = true;
+ }
}
+ palette.emitPalette(sb, usedColors);
}
- palette.emitPalette(sb, usedColors);
// Render the entire row of cells.
for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
return sixel;
}
- // ------------------------------------------------------------------------
- // End sixel output support -----------------------------------------------
- // ------------------------------------------------------------------------
-
- // ------------------------------------------------------------------------
- // iTerm2 image output support --------------------------------------------
- // ------------------------------------------------------------------------
-
/**
- * Create an iTerm2 images string representing a row of several cells
- * containing bitmap data.
+ * Convert a horizontal range of cell's image data into a single
+ * contigous image, rescaled and anti-aliased to match the current text
+ * cell size.
*
- * @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
+ * @param cells the cells containing image data
+ * @return the image resized to the current text cell size
*/
- 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);
- 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");
- }
-
+ private BufferedImage cellsToImage(final List<Cell> cells) {
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
+ // Piece cells.get(x).getImage() pieces together into one larger
// image for final rendering.
int totalWidth = 0;
- int fullWidth = cells.size() * getTextWidth();
- int fullHeight = getTextHeight();
+ int fullWidth = cells.size() * imageWidth;
+ int fullHeight = imageHeight;
for (int i = 0; i < cells.size(); i++) {
totalWidth += cells.get(i).getImage().getWidth();
}
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);
+ int tileWidth = imageWidth;
+ int tileHeight = imageHeight;
+
if (false && cells.get(i).isInvertedImage()) {
// I used to put an all-white cell over the cursor, don't do
// that anymore.
image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
imageHeight, rgbArray, 0, totalWidth);
- if (totalWidth < getTextWidth()) {
+ if (totalWidth < imageWidth) {
int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
for (int imageX = image.getWidth() - totalWidth;
}
}
+ if ((image.getWidth() != cells.size() * getTextWidth())
+ || (image.getHeight() != getTextHeight())
+ ) {
+ // Rescale the image to fit the text cells it is going into.
+ BufferedImage newImage;
+ newImage = new BufferedImage(cells.size() * getTextWidth(),
+ getTextHeight(), BufferedImage.TYPE_INT_ARGB);
+
+ Graphics gr = newImage.getGraphics();
+ if (gr instanceof Graphics2D) {
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+ ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ }
+ gr.drawImage(image, 0, 0, newImage.getWidth(),
+ newImage.getHeight(), null, null);
+ gr.dispose();
+ image = newImage;
+ }
+
+ return image;
+ }
+
+ // ------------------------------------------------------------------------
+ // 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<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:
*
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;",
getTextHeight())));
*/
sb.append("inline=1:");
- sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+ sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
sb.append("\007");
if (saveInCache) {
// 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.
*/