#91 Use DECSDM to (some) sixel images on the bottom text row

This commit is contained in:
Autumn Lamonte 2022-01-18 18:09:27 -06:00
parent f24d802400
commit 8953b42c71
5 changed files with 151 additions and 30 deletions

View file

@ -498,6 +498,15 @@ public class ECMA48Terminal extends LogicalScreen
*/
}
/**
* Get th number of entries in the cache.
*
* @return the number of entries
*/
public int size() {
return cache.size();
}
}
// ------------------------------------------------------------------------
@ -1262,7 +1271,7 @@ public class ECMA48Terminal extends LogicalScreen
if (sessionInfo.getWindowWidth() > 0) {
return (widthPixels / sessionInfo.getWindowWidth());
}
return 16;
return 10;
}
/**
@ -3346,20 +3355,28 @@ public class ECMA48Terminal extends LogicalScreen
}
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(' ');
if (sixelEncoder instanceof HQSixelEncoder) {
// HQ can emit images with transparency. We can use that
// along with DECSDM to get up to 1000 pixel width images on
// the bottom row.
emitSixelOnBottomRow(x, y, cells, sb);
return sb.toString();
} else {
// 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();
}
return sb.toString();
}
if (sixelCache == null) {
sixelCache = new ImageCache(height * width / 2);
sixelCache = new ImageCache(height * width * 10);
}
// Save and get rows to/from the cache that do NOT have inverted
@ -3388,11 +3405,22 @@ public class ECMA48Terminal extends LogicalScreen
}
// If the final image would be larger than 1000 pixels wide, break it
// up into smaller images.
if (cells.size() * getTextWidth() > 1000) {
// up into smaller images. Or if we are using the HQ encoder and
// will have more than some multiple of the palette size in total
// pixels.
int maxChunkLength = 1000;
if (sixelEncoder instanceof HQSixelEncoder) {
maxChunkLength = Math.min(maxChunkLength,
sixelEncoder.getPaletteSize() * 10 / getTextHeight());
/*
System.err.printf("maxChunkLength: %d cache used size %d\n",
maxChunkLength, sixelCache.size());
*/
}
if (cells.size() * getTextWidth() > maxChunkLength) {
StringBuilder chunkSb = new StringBuilder();
int chunkStart = 0;
int chunkSize = 1000 / getTextWidth();
int chunkSize = maxChunkLength / getTextWidth();
int remaining = cells.size();
int chunkX = x;
ArrayList<Cell> chunk;
@ -3418,6 +3446,51 @@ public class ECMA48Terminal extends LogicalScreen
return (startSixel(x, y) + sixel + endSixel());
}
/**
* Create a sixel string representing a row of several cells containing
* bitmap data on the bottom. This technique may not work on all
* terminals, and is limited to 1000 pixels from the left edge.
*
* @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
* @param sb the StringBuilder to write to
*/
private void emitSixelOnBottomRow(final int x, final int y,
final ArrayList<Cell> cells, final StringBuilder sb) {
int cellWidth = getTextWidth();
int cellHeight = getTextHeight();
int pixelX = x * cellWidth;
int pixelY = y * cellHeight;
int maxPixelX = pixelX + (cells.size() * cellWidth);
int maxPixelY = pixelY + cellHeight;
if ((maxPixelX > 1000) || (maxPixelY > 1000)) {
// There is no point, xterm will not display this image.
sb.append(normal());
sb.append(gotoXY(x, y));
for (int i = 0; i < cells.size(); i++) {
sb.append(' ');
}
return;
}
// The final image will be 1000 x 1000 or less.
BufferedImage cellsImage = cellsToImage(cells);
BufferedImage fullImage = new BufferedImage(maxPixelX,
maxPixelY, BufferedImage.TYPE_INT_ARGB);
Graphics gr = fullImage.getGraphics();
gr.drawImage(cellsImage, pixelX, pixelY, null);
gr.dispose();
// HQSixelEncoder.toSixel() can accept allowTransparent.
String sixel = ((HQSixelEncoder) sixelEncoder).toSixel(fullImage, true);
sb.append("\033[?80h\033P0;1;0q");
sb.append(sixel);
// System.err.println("SIXEL: " + sixel);
sb.append("\033\\\033[?80l");
}
/**
* Get the sixel support flag.
*
@ -3606,7 +3679,7 @@ public class ECMA48Terminal extends LogicalScreen
}
if (iterm2Cache == null) {
iterm2Cache = new ImageCache(height * width / 2);
iterm2Cache = new ImageCache(height * width * 10);
}
// Save and get rows to/from the cache that do NOT have inverted
@ -3788,7 +3861,7 @@ public class ECMA48Terminal extends LogicalScreen
}
if (jexerCache == null) {
jexerCache = new ImageCache(height * width / 2);
jexerCache = new ImageCache(height * width * 10);
}
// Save and get rows to/from the cache that do NOT have inverted

View file

@ -858,7 +858,7 @@ public class MultiScreen implements Screen {
* @return the width in pixels of a character cell
*/
public int getTextWidth() {
int textWidth = 16;
int textWidth = 10;
synchronized (screens) {
for (Screen screen: screens) {
int newTextWidth = screen.getTextWidth();

View file

@ -632,17 +632,15 @@ public class Cell extends CellAttributes {
return false;
}
// Either both objects have their image inverted, or neither do.
if (imageHashCode == 0) {
// Lazy-load hash code.
imageHashCode = makeImageHashCode();
}
if (that.imageHashCode == 0) {
// Lazy-load hash code.
that.imageHashCode = that.makeImageHashCode();
}
if ((imageHashCode == that.imageHashCode)
&& (background.equals(that.background))
if ((imageHashCode != 0) && (that.imageHashCode != 0)
&& (imageHashCode != that.imageHashCode)
) {
return false;
}
if (compareCellImages(this, that) == false) {
return false;
}
if (background.equals(that.background)) {
// Fall through to the attributes check below.
// ...
} else {
@ -672,6 +670,53 @@ public class Cell extends CellAttributes {
image.getWidth()));
}
/**
* Compare two Cell's images for equality. If the images are equal, then
* the imageHashCode on both is set.
*
* @param first the first Cell
* @param second the second Cell
*/
private boolean compareCellImages(final Cell first,
final Cell second) {
assert (first.image != null);
assert (second.image != null);
int width = first.image.getWidth();
int height = first.image.getHeight();
if (width != second.image.getWidth()) {
return false;
}
if (height != second.image.getHeight()) {
return false;
}
int [] firstRgbArray = first.image.getRGB(0, 0, width, height,
null, 0, width);
int [] secondRgbArray = second.image.getRGB(0, 0, width, height,
null, 0, width);
// This should be impossible, but check anyway.
if (firstRgbArray.length != secondRgbArray.length) {
return false;
}
int hashCode = 1;
for (int i = 0; i < firstRgbArray.length; i++) {
if (firstRgbArray[i] != secondRgbArray[i]) {
return false;
}
// Integer.hashCode() was introduced in Java 1.8. It breaks the
// original Jexer 1.0 dev goal for Java 1.6 compatibility.
hashCode = 31 * hashCode + Integer.hashCode(firstRgbArray[i]);
}
first.imageHashCode = hashCode;
second.imageHashCode = hashCode;
return true;
}
/**
* Hashcode uses all fields in equals().
*

View file

@ -146,6 +146,9 @@ public class DemoApplication extends TApplication {
(backendType == BackendType.SWING ? 28 : -1), 20);
addAllWidgets();
getBackend().setTitle(i18n.getString("applicationTitle"));
// Use cute theme by default.
onMenu(new TMenuEvent(getBackend(), 10001));
}
// ------------------------------------------------------------------------
@ -204,7 +207,7 @@ public class DemoApplication extends TApplication {
}
if (menu.getId() == 10001) {
// Look sexy: switch the color theme, window borders, and button
// Look cute: switch the color theme, window borders, and button
// styles.
System.setProperty("jexer.TWindow.borderStyleForeground", "round");
System.setProperty("jexer.TWindow.borderStyleModal", "round");
@ -320,7 +323,7 @@ public class DemoApplication extends TApplication {
TMenu demoMenu = addMenu(i18n.getString("demo"));
demoMenu.addItem(10000, i18n.getString("mainWindow"));
demoMenu.addSeparator();
demoMenu.addItem(10001, i18n.getString("lookSexy"));
demoMenu.addItem(10001, i18n.getString("lookCute"));
demoMenu.addItem(10002, i18n.getString("lookBland"));
demoMenu.addSeparator();
TMenuItem item = demoMenu.addItem(2000, i18n.getString("checkable"));

View file

@ -2,7 +2,7 @@ applicationTitle=Demo Application
demo=&Demo
mainWindow=N&ew Main Window...
lookSexy=Switch to sexy look
lookCute=Switch to cute look
lookBland=Switch to bland look
checkable=&Checkable
disabled=Disabled