kitty image output

This commit is contained in:
Autumn Lamonte 2022-02-04 08:44:22 -06:00
parent 5939f1aa77
commit aee27fdfb9

View file

@ -267,6 +267,11 @@ public class ECMA48Terminal extends LogicalScreen
*/
private ImageCache jexerCache = null;
/**
* If true, emit image data via Kitty image protocol.
*/
private boolean kittyImages = false;
/**
* The number of threads for image rendering.
*/
@ -1104,6 +1109,17 @@ public class ECMA48Terminal extends LogicalScreen
iterm2Images = true;
}
}
str = System.getProperty("jexer.ECMA48.kittyImages");
// Default to not supporting Kitty images.
if (str != null) {
if (str.equals("false")) {
kittyImages = false;
}
if (str.equals("true")) {
kittyImages = true;
}
}
}
// Default to using JPG Jexer images if terminal supports it.
@ -1717,18 +1733,34 @@ public class ECMA48Terminal extends LogicalScreen
/*
* 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.
* draw everything else afterwards.
*/
GlyphMaker glyphMaker = GlyphMaker.getInstance(getTextHeight());
for (int y = 0; y < height; y++) {
boolean unsetRow = false;
boolean unsetKittyRow = false;
for (int x = 0; x < width; x++) {
// If physical had non-image data that is now image data, the
// entire row must be redrawn.
Cell lCell = logical[x][y];
Cell pCell = physical[x][y];
if (lCell.isImage() && !pCell.isImage()) {
// Check if we need to erase images for Kitty, since text
// does not overwrite images.
if (kittyImages
&& ((lCell.isImage() != pCell.isImage())
|| reallyCleared)
) {
// Alas, Kitty erases the entire image even if you ask
// for a single cell to be erased, and I've no idea if it
// will also alpha blend on repeats or not.
//
// It is a bit much to figure out which image was emitted
// last frame and break it up on this frame, so just
// redraw the entire row.
unsetKittyRow = true;
}
if (!kittyImages && lCell.isImage() && !pCell.isImage()) {
unsetRow = true;
}
int ch = lCell.getChar();
@ -1743,8 +1775,17 @@ public class ECMA48Terminal extends LogicalScreen
getTextWidth(), getTextHeight(), getBackend());
lCell.setImage(newImage);
unsetRow = true;
unsetKittyRow = true;
}
}
if (unsetKittyRow) {
// There are no images, or different images, on this row, so
// erase whatever might have been here before.
sb.append(String.format("\033_Ga=d,d=Y,y=%d\033\\", y + 1));
unsetImageRow(y);
}
if (unsetRow) {
unsetImageRow(y);
}
@ -1787,6 +1828,7 @@ public class ECMA48Terminal extends LogicalScreen
) {
right++;
}
ArrayList<Cell> cellsToDraw = new ArrayList<Cell>();
for (int i = 0; i < (right - x); i++) {
assert (logical[x + i][y].isImage());
@ -1818,7 +1860,8 @@ public class ECMA48Terminal extends LogicalScreen
if (cellsToDraw.size() > 0) {
if (debugToStderr && reallyDebug) {
System.err.println("images to render: iTerm2: " +
iterm2Images + " Jexer: " + jexerImageOption);
iterm2Images + " Jexer: " + jexerImageOption +
" Kitty: " + kittyImages);
}
if (iterm2Images) {
@ -1841,6 +1884,8 @@ public class ECMA48Terminal extends LogicalScreen
sb.append(toIterm2Image(x, y, cellsToDraw));
} else if (jexerImageOption != JexerImageOption.DISABLED) {
sb.append(toJexerImage(x, y, cellsToDraw));
} else if (kittyImages) {
sb.append(toKittyImage(x, y, cellsToDraw));
} else {
sb.append(toSixel(x, y, cellsToDraw));
}
@ -1859,6 +1904,8 @@ public class ECMA48Terminal extends LogicalScreen
return toIterm2Image(callX, callY, callCells);
} else if (jexerImageOption != JexerImageOption.DISABLED) {
return toJexerImage(callX, callY, callCells);
} else if (kittyImages) {
return toKittyImage(callX, callY, callCells);
} else {
return toSixel(callX, callY, callCells);
}
@ -2484,6 +2531,26 @@ public class ECMA48Terminal extends LogicalScreen
iterm2BottomRow = true;
}
}
// Kitty image support will be ASSUMED for the following terminals
// if kittyImages is not explicitly false.
if (text.contains("WezTerm")
|| text.contains("kitty")
) {
String str = System.getProperty("jexer.ECMA48.kittyImages");
if ((str != null) && (str.equals("false"))) {
if (debugToStderr) {
System.err.println(" -- terminal supports Kitty, but " +
"explicitly disabled in config");
}
kittyImages = false;
} else {
if (debugToStderr) {
System.err.println(" -- enable Kitty images");
}
kittyImages = true;
}
}
}
/**
@ -4150,6 +4217,93 @@ public class ECMA48Terminal extends LogicalScreen
// End Jexer image output support -----------------------------------------
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// Kitty image output support ---------------------------------------------
// ------------------------------------------------------------------------
/**
* Create a Kitty 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 toKittyImage(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);
// We don't cache Kitty images at this time.
BufferedImage image = cellsToImage(cells);
int fullHeight = image.getHeight();
// Always 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 "";
}
String data = StringUtils.toBase64(pngOutputStream.toByteArray());
// Kitty bug: "base64 data must have a length that is a multiple of
// 4".
data = data.replace("\r", "");
data = data.replace("\n", "");
sb.append("\033_Ga=T,f=100,C=1,");
while (data.length() > 0) {
// This chunking stuff is stupid.
String chunk = data.substring(0, Math.min(4096, data.length()));
boolean last = false;
if (chunk.length() == data.length()) {
last = true;
data = "";
} else {
data = data.substring(chunk.length());
}
if (last) {
sb.append("m=0;");
} else {
sb.append("m=1;");
}
sb.append(chunk);
sb.append("\033\\");
if (!last) {
sb.append("\033_G");
}
}
return (gotoXY(x, y) + sb.toString());
}
/**
* Get the Kitty images support flag.
*
* @return true if this terminal is emitting Jexer images
*/
public boolean hasKittyImages() {
return kittyImages;
}
// ------------------------------------------------------------------------
// End Kitty image output support -----------------------------------------
// ------------------------------------------------------------------------
/**
* Setup system colors to match DOS color palette.
*/