#83 eliminate empty image cells

This commit is contained in:
Autumn Lamonte 2021-12-23 18:01:17 -06:00
parent 4e3a73bb67
commit 6c7628b01d
8 changed files with 310 additions and 77 deletions

View file

@ -2262,33 +2262,57 @@ public class ECMA48Terminal extends LogicalScreen
BufferedImage textImage;
if (logical[x + i][y].isTransparentImage()) {
// We should only see transparent cells at this layer if
// backend transparency was enabled.
assert (imagesOverText == true);
// We would normally only see transparent cells at
// this layer if backend transparency was enabled.
// But in the case of multihead, we may have been
// passed a cell with transparency even though this
// backend can't display it. So we will check, and
// if imagesOverText is disabled then we will quietly
// continue on. Otherwise render a text character
// under the image.
if (imagesOverText == true) {
// Render this cell to a flat image. The bad
// news is that we don't get to use the actual
// terminal's font, because putting image data
// over text is really iffy depending on
// terminal. So we render it here instead.
BufferedImage image = logical[x + i][y].getImage();
int textWidth = image.getWidth();
int textHeight = image.getHeight();
if (glyphMaker == null) {
glyphMaker = GlyphMaker.getInstance(textHeight);
}
newImage = new BufferedImage(textWidth,
textHeight, BufferedImage.TYPE_INT_ARGB);
textImage = glyphMaker.getImage(logical[x + i][y],
textWidth, textHeight);
// Render this cell to a flat image. The bad news is
// that we don't get to use the actual terminal's
// font, because putting image data over text is
// really iffy depending on terminal. So we render
// it here instead.
BufferedImage image = logical[x + i][y].getImage();
int textWidth = image.getWidth();
int textHeight = image.getHeight();
if (glyphMaker == null) {
glyphMaker = GlyphMaker.getInstance(textHeight);
java.awt.Graphics gr = newImage.getGraphics();
gr.setColor(jexer.backend.SwingTerminal.
attrToBackgroundColor(logical[x + i][y]));
gr.drawImage(textImage, 0, 0, null, null);
gr.drawImage(logical[x + i][y].getImage(), 0, 0,
null, null);
gr.dispose();
logical[x + i][y].setImage(newImage);
} else {
// Put the cell's background color behind the
// pixels.
BufferedImage image = logical[x + i][y].getImage();
int textWidth = image.getWidth();
int textHeight = image.getHeight();
newImage = new BufferedImage(textWidth,
textHeight, BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics gr = newImage.getGraphics();
gr.setColor(jexer.backend.SwingTerminal.
attrToBackgroundColor(logical[x + i][y]));
gr.fillRect(0, 0, newImage.getWidth(),
newImage.getHeight());
gr.drawImage(logical[x + i][y].getImage(), 0, 0,
null, null);
gr.dispose();
logical[x + i][y].setImage(newImage);
}
newImage = new BufferedImage(textWidth,
textHeight, BufferedImage.TYPE_INT_ARGB);
textImage = glyphMaker.getImage(logical[x + i][y],
textWidth, textHeight);
java.awt.Graphics gr = newImage.getGraphics();
gr.setColor(java.awt.Color.BLACK);
gr.drawImage(textImage, 0, 0, null, null);
gr.drawImage(logical[x + i][y].getImage(), 0, 0,
null, null);
gr.dispose();
logical[x + i][y].setImage(newImage);
}
assert (!logical[x + i][y].isTransparentImage());
cellsToDraw.add(logical[x + i][y]);

View file

@ -1432,13 +1432,15 @@ public class SwingTerminal extends LogicalScreen
if ((SwingComponent.tripleBuffer) && (swing.getFrame() != null)) {
gr2.dispose();
// We need a new key that will not be mutated by
// invertCell().
Cell key = new Cell(cell);
if (cell.isBlink() && !cursorBlinkVisible) {
glyphCacheBlink.put(key, image);
} else {
glyphCache.put(key, image);
if (!cell.isImage()) {
// We need a new key that will not be mutated by
// invertCell().
Cell key = new Cell(cell);
if (cell.isBlink() && !cursorBlinkVisible) {
glyphCacheBlink.put(key, image);
} else {
glyphCache.put(key, image);
}
}
if (swing.getFrame() != null) {

View file

@ -111,7 +111,8 @@ public class Cell extends CellAttributes {
* If this cell has image data, whether or not it also has transparent
* pixels. -1 = no image data; 0 = unknown if transparent pixels are
* present; 1 = transparent pixels are present; 2 = transparent pixels
* are not present.
* are not present; 3 = the entire image is transparent; 4 = transparent
* pixels are present, but not all of the image.
*/
private int hasTransparentPixels = -1;
@ -203,7 +204,7 @@ public class Cell extends CellAttributes {
* @param cell the other cell
*/
public void blitImage(final Cell cell) {
if (!cell.isImage()) {
if (!cell.isImage() || cell.isFullyTransparentImage()) {
// The other cell has no image data.
return;
}
@ -280,10 +281,67 @@ public class Cell extends CellAttributes {
// No transparent pixels.
hasTransparentPixels = 2;
}
if (hasTransparentPixels == 1) {
if ((hasTransparentPixels == 1)
|| (hasTransparentPixels == 3)
|| (hasTransparentPixels == 4)
) {
// Transparent pixels were found at some time.
return true;
}
assert (hasTransparentPixels == 2);
return false;
}
/**
* If true, this cell has image data and all of its pixels are fully
* transparent (alpha of 0).
*
* @return true if this cell has image data with only transparent pixels
*/
public boolean isFullyTransparentImage() {
if (image == null) {
return false;
}
if ((hasTransparentPixels == 0) || (hasTransparentPixels == 1)) {
// Scan for transparent pixels. Only if ALL pixels are
// transparent do we return true.
int [] rgbArray = image.getRGB(0, 0,
image.getWidth(), image.getHeight(), null, 0, image.getWidth());
if (rgbArray.length == 0) {
// No image data, fully transparent.
hasTransparentPixels = 3;
return true;
}
boolean allOpaque = true;
boolean allTransparent = true;
for (int i = 0; i < rgbArray.length; i++) {
int alpha = (rgbArray[i] >>> 24) & 0xFF;
if ((alpha != 0xFF) && (alpha != 0x00)) {
// Some transparent pixels, but not fully transparent.
hasTransparentPixels = 4;
return false;
}
// This pixel is either fully opaque or fully transparent.
if (alpha == 0xFF) {
allTransparent = false;
} else {
allOpaque = false;
}
}
if (allOpaque == true) {
// No transparent pixels.
hasTransparentPixels = 2;
} else {
assert (allTransparent == true);
hasTransparentPixels = 3;
}
}
if (hasTransparentPixels == 3) {
// Fully transparent.
return true;
}
return false;
}

View file

@ -0,0 +1,68 @@
/*
* Jexer - Java Text User Interface
*
* The MIT License (MIT)
*
* Copyright (C) 2021 Autumn Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] Trans Liberation Now
* @version 1
*/
package jexer.bits;
import java.awt.image.BufferedImage;
/**
* ImageUtils contains methods to:
*
* - Check if an image is fully transparent.
*
*/
public class ImageUtils {
/**
* Check if any pixels in an image have not-100% alpha value.
*
* @return true if every pixel is fully transparent
*/
public static boolean isFullyTransparent(final BufferedImage image) {
assert (image != null);
int [] rgbArray = image.getRGB(0, 0,
image.getWidth(), image.getHeight(), null, 0, image.getWidth());
if (rgbArray.length == 0) {
// No image data, fully transparent.
return true;
}
for (int i = 0; i < rgbArray.length; i++) {
int alpha = (rgbArray[i] >>> 24) & 0xFF;
if (alpha != 0x00) {
// A not-fully transparent pixel is found.
return false;
}
}
// Every pixel was transparent.
return true;
}
}

View file

@ -402,7 +402,8 @@ public class DemoMainWindow extends TWindow {
// For this one, render to the entire screen, not to the window.
getScreen().resetClipping();
tackboard.draw(getScreen());
tackboard.draw(getScreen(),
getApplication().getBackend().isImagesOverText());
}
/**

View file

@ -36,6 +36,7 @@ import java.util.List;
import jexer.backend.GlyphMaker;
import jexer.backend.Screen;
import jexer.bits.Cell;
import jexer.bits.ImageUtils;
/**
* Tackboard maintains a collection of TackboardItems to draw on a Screen.
@ -72,6 +73,16 @@ public class Tackboard {
*/
private ArrayList<TackboardItem> items = new ArrayList<TackboardItem>();
/**
* Last text width value.
*/
private int lastTextWidth = -1;
/**
* Last text height value.
*/
private int lastTextHeight = -1;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
@ -109,13 +120,30 @@ public class Tackboard {
* Draw everything to the screen.
*
* @param screen the screen to render to
* @param transparent if true, allow partially transparent images to be
* drawn to the screen
*/
public void draw(final Screen screen) {
public void draw(final Screen screen, final boolean transparent) {
Collections.sort(items);
int cellWidth = screen.getTextWidth();
int cellHeight = screen.getTextHeight();
boolean redraw = false;
if ((lastTextWidth == -1)
|| (lastTextWidth != cellWidth)
|| (lastTextHeight != cellHeight)
) {
// We need to force a redraw because the cell grid dimensions
// have changed.
redraw = true;
lastTextWidth = cellWidth;
lastTextHeight = cellHeight;
}
for (TackboardItem item: items) {
if (redraw) {
item.setDirty();
}
BufferedImage image = item.getImage(cellWidth, cellHeight);
if (image == null) {
continue;
@ -128,8 +156,20 @@ public class Tackboard {
int width = image.getWidth();
int height = image.getHeight();
assert (width % cellWidth == 0);
assert (height % cellHeight == 0);
if ((width % cellWidth != 0) || (height % cellHeight != 0)) {
// These should have lined up, that was the whole point of
// the redraw. Why didn't they?
/*
System.err.println("HUH? width " + width +
" cellWidth " + cellWidth +
" height " + height +
" cellHeight " + cellHeight);
*/
} else {
// This should be impossible, right?
assert (width % cellWidth == 0);
assert (height % cellHeight == 0);
}
int columns = width / cellWidth;
int rows = height / cellHeight;
@ -148,9 +188,8 @@ public class Tackboard {
int dx = x % cellWidth;
int dy = y % cellHeight;
// TODO: handle images that have negative X or Y coordinates.
int left = 0;
int top = 0;
int left = (x < 0 ? -1 : 0);
int top = (y < 0 ? -1 : 0);
for (int sy = 0; sy < rows; sy++) {
if ((sy + textY + top < 0)
@ -177,6 +216,11 @@ public class Tackboard {
newImage = image.getSubimage(sx * cellWidth,
sy * cellHeight, cellWidth, cellHeight);
if (ImageUtils.isFullyTransparent(newImage)) {
// Skip this cell.
continue;
}
// newImage has the image that needs to be overlaid on
// (sx + textX + left, sy + textY + top)
@ -186,13 +230,28 @@ public class Tackboard {
// Blit this image over that one.
BufferedImage oldImage = oldCell.getImage();
java.awt.Graphics gr = oldImage.getGraphics();
gr.setColor(java.awt.Color.BLACK);
gr.setColor(jexer.backend.SwingTerminal.
attrToBackgroundColor(oldCell));
gr.drawImage(newImage, 0, 0, null, null);
gr.dispose();
oldCell.setImage(oldImage);
} else {
// Old cell is text only, just add the image.
oldCell.setImage(newImage);
if (!transparent) {
BufferedImage backImage;
backImage = new BufferedImage(cellWidth,
cellHeight, BufferedImage.TYPE_INT_ARGB);
java.awt.Graphics gr = backImage.getGraphics();
gr.setColor(jexer.backend.SwingTerminal.
attrToBackgroundColor(oldCell));
gr.fillRect(0, 0, backImage.getWidth(),
backImage.getHeight());
gr.drawImage(newImage, 0, 0, null, null);
gr.dispose();
oldCell.setImage(backImage);
} else {
oldCell.setImage(newImage);
}
}
screen.putCharXY(sx + textX + left, sy + textY + top,
oldCell);

View file

@ -160,6 +160,13 @@ public class TackboardItem implements Comparable<TackboardItem> {
return dirty;
}
/**
* Set dirty flag.
*/
public final void setDirty() {
dirty = true;
}
/**
* Comparison check. All fields must match to return true.
*

View file

@ -56,6 +56,7 @@ import jexer.backend.GlyphMaker;
import jexer.bits.Color;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
import jexer.bits.ImageUtils;
import jexer.bits.StringUtils;
import jexer.event.TInputEvent;
import jexer.event.TKeypressEvent;
@ -7505,7 +7506,13 @@ public class ECMA48 implements Runnable {
*/
boolean maybeTransparent = false;
if ((backend != null) && backend.isImagesOverText()) {
// The check below is forced to always enable maybeTransparent. Even
// when imagesOverText is disabled, we can still process sixel images
// with missing pixels by way of checking for entirely empty text
// cell regions and removing them. The effect is to have a blocky
// black outline around the image rather than an entire black
// rectangle.
if (true || ((backend != null) && backend.isImagesOverText())) {
maybeTransparent = true;
}
Sixel sixel = new Sixel(sixelParseBuffer.toString(), sixelPalette,
@ -7721,6 +7728,7 @@ public class ECMA48 implements Runnable {
// If the backend supports transparent images, then we will not
// draw the black underneath the cells.
boolean transparent = false;
if ((backend != null) && backend.isImagesOverText()) {
transparent = true;
}
@ -7734,7 +7742,9 @@ public class ECMA48 implements Runnable {
cellRows++;
}
if (!transparent && maybeTransparent) {
// See the comment in parseSixel(). The partially-transparent cell
// will be rendered over a black background below inside the loop.
if (false && !transparent && maybeTransparent) {
// Re-render the image against a black background, so that alpha
// in the image does not lead to bleed-through artifacts.
BufferedImage newImage;
@ -7753,8 +7763,6 @@ public class ECMA48 implements Runnable {
Cell [][] cells = new Cell[cellColumns][cellRows];
for (int x = 0; x < cellColumns; x++) {
for (int y = 0; y < cellRows; y++) {
Cell cell = new Cell();
int width = textWidth;
if ((x + 1) * textWidth > image.getWidth()) {
width = image.getWidth() - (x * textWidth);
@ -7765,13 +7773,20 @@ public class ECMA48 implements Runnable {
}
// I'm genuinely not sure if making many small cells with
// array copy is better than slicing. Memory pressure is
// killing it at high animation rates. Leaving the true in
// the check below will use the smaller cells rather than
// subImages.
if (true || (width != textWidth) || (height != textHeight)) {
// Copy the smaller-than-text-cell-size image to a
// full-text-cell-size.
// array copy is better than lots of sumImages. Memory
// pressure is killing it at high animation rates. For now,
// we will ALWAYS make a copy.
Cell cell = new Cell();
BufferedImage imageSlice = image.getSubimage(x * textWidth,
y * textHeight, width, height);
if (ImageUtils.isFullyTransparent(imageSlice)) {
// There is nothing more to do, this entire image is
// empty.
// NOP
} else {
BufferedImage newImage;
newImage = new BufferedImage(textWidth, textHeight,
BufferedImage.TYPE_INT_ARGB);
@ -7781,29 +7796,26 @@ public class ECMA48 implements Runnable {
gr.fillRect(0, 0, newImage.getWidth(),
newImage.getHeight());
}
gr.drawImage(image.getSubimage(x * textWidth,
y * textHeight, width, height),
0, 0, null, null);
gr.drawImage(imageSlice, 0, 0, null, null);
gr.dispose();
cell.setImage(newImage);
} else {
cell.setImage(image.getSubimage(x * textWidth,
y * textHeight, width, height));
}
if (transparent && maybeTransparent) {
// Check now if this cell has transparent pixels. This
// will slow down the reader thread but unload the render
// thread.
//
// Truth is performance is going to be bad for a while...
cell.isTransparentImage();
} else if (transparent && !maybeTransparent) {
// We support transparency, but this image doesn't have
// any transparent pixels. Force the cell to never check
// transparency.
cell.setOpaqueImage();
}
cell.setImage(newImage);
if (maybeTransparent) {
// Check now if this cell has transparent pixels.
// This will slow down the reader thread but unload
// the render thread.
//
// Truth is performance is going to be bad for a
// while...
cell.isTransparentImage();
} else {
// We support transparency, but this image doesn't
// have any transparent pixels. Force the cell to
// never check transparency.
cell.setOpaqueImage();
}
}
cells[x][y] = cell;
}
}
@ -7865,7 +7877,9 @@ public class ECMA48 implements Runnable {
cells[x][y].isTransparentImage();
}
}
line.replace(currentState.cursorX, cells[x][y]);
if (cells[x][y].isImage()) {
line.replace(currentState.cursorX, cells[x][y]);
}
// If at the end of the visible screen, stop.
if (currentState.cursorX == rightMargin) {