Read iTerm2 images in terminal widget

This commit is contained in:
Autumn Lamonte 2021-12-31 12:44:28 -06:00
parent cd90cc4f21
commit 86da95e0cf
3 changed files with 326 additions and 1 deletions

View file

@ -379,6 +379,8 @@ public class TImage extends TWidget implements EditMenuUser {
* @param always if true, always resize the cells
*/
private void sizeToImage(final boolean always) {
scaleBackColor = jexer.backend.SwingTerminal.attrToBackgroundColor(getWindow().getBackground());
int textWidth = getScreen().getTextWidth();
int textHeight = getScreen().getTextHeight();
@ -648,7 +650,7 @@ public class TImage extends TWidget implements EditMenuUser {
}
/**
* Scale an image by to be scaleFactor size.
* Scale an image to be scaleFactor size, OR stretch it.
*
* @param image the image to scale
* @param factor the scale to make the new image

View file

@ -35,9 +35,37 @@ import java.awt.image.BufferedImage;
*
* - Check if an image is fully transparent.
*
* - Scale an image and preserve aspect ratio.
*/
public class ImageUtils {
// ------------------------------------------------------------------------
// Constants --------------------------------------------------------------
// ------------------------------------------------------------------------
/**
* Selections for fitting the image to the text cells.
*/
public enum Scale {
/**
* Stretch/shrink the image in both directions to fully fill the text
* area width/height.
*/
STRETCH,
/**
* Scale the image, preserving aspect ratio, to fill the text area
* width/height (like letterbox). The background color for the
* letterboxed area is specified in the backColor argument to
* scaleImage().
*/
SCALE,
}
// ------------------------------------------------------------------------
// ImageUtils -------------------------------------------------------------
// ------------------------------------------------------------------------
/**
* Check if any pixels in an image have not-0% alpha value.
*
@ -92,4 +120,62 @@ public class ImageUtils {
return true;
}
/**
* Scale an image to be scaleFactor size and/or stretch it to fit a
* target box.
*
* @param image the image to scale
* @param width the width in pixels for the destination image
* @param height the height in pixels for the destination image
* @param scale the scaling type
* @param backColor the background color to use for Scale.SCALE
*/
public static BufferedImage scaleImage(final BufferedImage image,
final int width, final int height,
final Scale scale, final java.awt.Color backColor) {
BufferedImage newImage = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
int x = 0;
int y = 0;
int destWidth = width;
int destHeight = height;
switch (scale) {
case STRETCH:
break;
case SCALE:
double a = (double) image.getWidth() / image.getHeight();
double b = (double) width / height;
double h = (double) height / image.getHeight();
double w = (double) width / image.getWidth();
assert (a > 0);
assert (b > 0);
if (a > b) {
// Horizontal letterbox
destHeight = (int) (image.getWidth() / a * w);
destWidth = (int) (image.getWidth() * w);
y = (height - destHeight) / 2;
assert (y >= 0);
} else {
// Vertical letterbox
destHeight = (int) (image.getHeight() * h);
destWidth = (int) (image.getHeight() * a * h);
x = (width - destWidth) / 2;
assert (x >= 0);
}
break;
}
java.awt.Graphics gr = newImage.createGraphics();
if (scale == Scale.SCALE) {
gr.setColor(backColor);
gr.fillRect(0, 0, newImage.getWidth(), newImage.getHeight());
}
gr.drawImage(image, x, y, destWidth, destHeight, null);
gr.dispose();
return newImage;
}
}

View file

@ -5407,6 +5407,10 @@ public class ECMA48 implements Runnable {
parseJexerImageFile(2, p[2], p[3]);
}
}
if (p[0].equals("1337")) {
parseIterm2Image(p);
}
}
// Go to SCAN_GROUND state
@ -8017,6 +8021,239 @@ public class ECMA48 implements Runnable {
imageToCells(image, scroll, maybeTransparent);
}
/**
* Parse a iTerm2 image string into a bitmap image, and overlay that
* image onto the text cells. See reference at:
* https://iterm2.com/documentation-images.html
*
* @param args the arguments of the OSC 1337 sequence. args[0] will be
* "1337".
*/
private void parseIterm2Image(final String [] args) {
// If the file data is opaque, pass that to imageToCells().
boolean maybeTransparent = true;
// See: https://github.com/wez/wezterm/issues/1424
boolean doNotMoveCursor = false;
// We MUST see "inline=1". This terminal does NOT EVER write to the
// filesystem. Ever.
boolean sawInline = false;
boolean preserveAspectRatio = false;
// Image dimension options.
String iTerm2Width = "auto";
String iTerm2Height = "auto";
// File size. This is optional to most terminals. If it is
// specified, then we will limit to 4MB.
boolean gotSize = false;
int size = -1;
if ((args.length < 2) || !args[0].equals("1337")
|| !args[1].startsWith("File=")
) {
return;
}
// Separate the arguments into key/values, and the base64-encoded
// data payload.
// Remove the "File=" from the first argument.
args[1] = args[1].substring(5);
// System.err.println("args[1]: '" + args[1] + "'");
// Separate the last argument from the ":{base64}" part.
String lastArg = args[args.length - 1];
if (!lastArg.contains(":")) {
return;
}
String data = lastArg.substring(lastArg.indexOf(':') + 1);
if (data.length() == 0) {
return;
}
lastArg = lastArg.substring(0, lastArg.length() - data.length() - 1);
// System.err.println("lastArg: '" + lastArg + "'");
HashMap<String, String> pairs = new HashMap<String, String>();
for (int i = 1; i < args.length - 1; i++) {
String [] pair = args[i].split("=");
if (pair.length != 2) {
return;
}
pairs.put(pair[0], pair[1]);
}
String [] pair = lastArg.split("=");
if (pair.length != 2) {
return;
}
pairs.put(pair[0], pair[1]);
// Now check the arguments
for (String name: pairs.keySet()) {
String value = pairs.get(name);
// System.err.println("name='" + name + "' value='" + value + "'");
if (name.equals("size")) {
try {
size = Integer.parseInt(value);
gotSize = true;
} catch (NumberFormatException e) {
// SQUASH
}
}
if (name.equals("inline") && value.equals("1")) {
sawInline = true;
}
if (name.equals("width")) {
iTerm2Width = value;
}
if (name.equals("height")) {
iTerm2Height = value;
}
if (name.equals("preserveAspectRatio") && value.equals("1")) {
preserveAspectRatio = true;
}
if (name.equals("doNotMoveCursor") && value.equals("1")) {
doNotMoveCursor = true;
}
}
if (!sawInline) {
return;
}
if (gotSize) {
if ((size < 1) || (size > 16777216)) {
return;
}
}
// We have the options and image data, and it will be displayed. Now
// try to decode it into a bitmap. We go blindly into the night as
// far as image format is concerned.
BufferedImage image = null;
byte [] bytes = StringUtils.fromBase64(data.getBytes());
if (bytes == null) {
return;
}
try {
image = ImageIO.read(new ByteArrayInputStream(bytes));
} catch (IOException e) {
// SQUASH
return;
}
assert (image != null);
int fileImageWidth = image.getWidth();
int fileImageHeight = image.getHeight();
if ((fileImageWidth < 1)
|| (fileImageWidth > 10000)
|| (fileImageHeight < 1)
|| (fileImageHeight > 10000)
) {
return;
}
if (maybeTransparent) {
if (image.getTransparency() == java.awt.Transparency.OPAQUE) {
maybeTransparent = false;
}
}
// Scale the image according to the width/height arguments.
int displayWidth = fileImageWidth;
int displayHeight = fileImageHeight;
try {
if (iTerm2Width.equals("auto")) {
// NOP
} else if (iTerm2Width.endsWith("%")) {
// Percent of screen
iTerm2Width = iTerm2Width.substring(0, iTerm2Width.length() - 1);
int n = Integer.parseInt(iTerm2Width);
if ((n < 0) || (n > 100)) {
return;
}
displayWidth = (n * textWidth * width) / 100;
} else if (iTerm2Width.endsWith("px")) {
// Pixels
iTerm2Width = iTerm2Width.substring(0, iTerm2Width.length() - 2);
int n = Integer.parseInt(iTerm2Width);
if (n < 0) {
return;
}
displayWidth = n;
} else {
// Number of text cells
int n = Integer.parseInt(iTerm2Width);
if (n < 0) {
return;
}
displayWidth = n * textWidth;
}
// Truncate images to fit the screen.
displayWidth = Math.min(width * textWidth, displayWidth);
if (iTerm2Height.equals("auto")) {
// NOP
} else if (iTerm2Height.endsWith("%")) {
// Percent of screen
iTerm2Height = iTerm2Height.substring(0, iTerm2Height.length() - 1);
int n = Integer.parseInt(iTerm2Height);
if ((n < 0) || (n > 100)) {
return;
}
displayHeight = (n * textHeight * height) / 100;
} else if (iTerm2Height.endsWith("px")) {
// Pixels
iTerm2Height = iTerm2Height.substring(0, iTerm2Height.length() - 2);
int n = Integer.parseInt(iTerm2Height);
if (n < 0) {
return;
}
displayHeight = n;
} else {
// Number of text cells
int n = Integer.parseInt(iTerm2Height);
if (n < 0) {
return;
}
displayHeight = n * textHeight;
}
} catch (NumberFormatException e) {
// Invalid number, done.
return;
}
/*
System.err.println("File dims " + fileImageWidth + "x" +
fileImageHeight +
"Disp dims " + displayWidth + "x" + displayHeight);
*/
if (doNotMoveCursor) {
// Truncate image height to fit the screen.
displayHeight = Math.min(height * textHeight, displayHeight);
}
if (preserveAspectRatio
&& ((displayWidth != fileImageWidth)
|| (displayHeight != fileImageHeight))
) {
// Scale the image to fit the requested dimensions.
image = ImageUtils.scaleImage(image, displayWidth, displayHeight,
ImageUtils.Scale.SCALE,
SwingTerminal.attrToBackgroundColor(currentState.attr));
} else if ((displayWidth != fileImageWidth)
|| (displayHeight != fileImageHeight)
) {
// Scale the image to fit the requested dimensions.
image = ImageUtils.scaleImage(image, displayWidth, displayHeight,
ImageUtils.Scale.STRETCH,
SwingTerminal.attrToBackgroundColor(currentState.attr));
}
imageToCells(image, !doNotMoveCursor, maybeTransparent);
}
/**
* Break up an image into the cells at the current cursor.
*