#95 multithread image encoding

This commit is contained in:
Autumn Lamonte 2022-01-23 17:16:56 -06:00
parent 57dec8f30b
commit 44f3010825
2 changed files with 137 additions and 53 deletions

View file

@ -48,6 +48,12 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.imageio.ImageIO;
import jexer.bits.Cell;
@ -450,7 +456,7 @@ public class ECMA48Terminal extends LogicalScreen
* @return the image string representing these cells, or null if this
* list of cells is not in the cache
*/
public String get(final ArrayList<Cell> cells) {
public synchronized String get(final ArrayList<Cell> cells) {
CacheEntry entry = cache.get(makeKey(cells));
if (entry == null) {
return null;
@ -465,7 +471,9 @@ public class ECMA48Terminal extends LogicalScreen
* @param cells the list of cells that are the cache key
* @param data the image string representing these cells
*/
public void put(final ArrayList<Cell> cells, final String data) {
public synchronized void put(final ArrayList<Cell> cells,
final String data) {
String key = makeKey(cells);
// System.err.println("put() " + key + " size " + cache.size());
@ -508,7 +516,7 @@ public class ECMA48Terminal extends LogicalScreen
*
* @return the number of entries
*/
public int size() {
public synchronized int size() {
return cache.size();
}
@ -1429,6 +1437,10 @@ public class ECMA48Terminal extends LogicalScreen
Cell lCell = logical[x][y];
Cell pCell = physical[x][y];
if (lCell.isImage()) {
continue;
}
if (!lCell.equals(pCell) || lCell.isPulse() || reallyCleared) {
if (debugToStderr && reallyDebug) {
@ -1633,6 +1645,7 @@ public class ECMA48Terminal extends LogicalScreen
} // for (int x = 0; x < width; x++)
}
/**
* Render the screen to a string that can be emitted to something that
* knows how to process ECMA-48/ANSI X3.64 escape sequences.
@ -1685,6 +1698,17 @@ public class ECMA48Terminal extends LogicalScreen
unsetImageRow(y);
}
}
/*
* Image encoding is expensive, especially when the image is not in
* cache. We multithread it. Since each image contains its own
* gotoxy(), it doesn't matter in what order they are delivered to
* the terminal.
*/
// DEBUG: two threads
ExecutorService imageExecutor = Executors.newFixedThreadPool(2);
List<Future<String>> imageResults = new ArrayList<Future<String>>();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
Cell lCell = logical[x][y];
@ -1738,19 +1762,61 @@ public class ECMA48Terminal extends LogicalScreen
System.err.println("images to render: iTerm2: " +
iterm2Images + " Jexer: " + jexerImageOption);
}
if (iterm2Images) {
sb.append(toIterm2Image(x, y, cellsToDraw));
if (iterm2Cache == null) {
iterm2Cache = new ImageCache(height * width * 10);
}
} else if (jexerImageOption != JexerImageOption.DISABLED) {
sb.append(toJexerImage(x, y, cellsToDraw));
if (jexerCache == null) {
jexerCache = new ImageCache(height * width * 10);
}
} else {
sb.append(toSixel(x, y, cellsToDraw));
if (sixelCache == null) {
sixelCache = new ImageCache(height * width * 10);
}
}
final int callX = x;
final int callY = y;
// Make a deep copy of the cells to render.
final ArrayList<Cell> callCells;
callCells = new ArrayList<Cell>(cellsToDraw);
imageResults.add(imageExecutor.submit(new Callable<String>() {
@Override
public String call() {
if (iterm2Images) {
return toIterm2Image(callX, callY, callCells);
} else if (jexerImageOption != JexerImageOption.DISABLED) {
return toJexerImage(callX, callY, callCells);
} else {
return toSixel(callX, callY, callCells);
}
}
}));
}
x = right;
}
}
// Collect all the encoded images, checking every 10ms.
imageExecutor.shutdown();
try {
while (!imageExecutor.awaitTermination(10, TimeUnit.MILLISECONDS)) {
// NOP
}
for (Future<String> image: imageResults) {
sb.append(image.get());
}
} catch (InterruptedException e) {
// SQUASH
} catch (ExecutionException e) {
// SQUASH
}
imageExecutor.shutdownNow();
// Draw the text part now.
for (int y = 0; y < height; y++) {
flushLine(y, sb, attr);
@ -3391,10 +3457,6 @@ public class ECMA48Terminal extends LogicalScreen
if (sixelFastAndDirty) {
saveInCache = false;
} else {
if (sixelCache == null) {
sixelCache = new ImageCache(height * width * 10);
}
// Save and get rows to/from the cache that do NOT have inverted
// cells.
for (Cell cell: cells) {
@ -3697,10 +3759,6 @@ public class ECMA48Terminal extends LogicalScreen
return sb.toString();
}
if (iterm2Cache == null) {
iterm2Cache = new ImageCache(height * width * 10);
}
// Save and get rows to/from the cache that do NOT have inverted
// cells.
boolean saveInCache = true;
@ -3879,10 +3937,6 @@ public class ECMA48Terminal extends LogicalScreen
return sb.toString();
}
if (jexerCache == null) {
jexerCache = new ImageCache(height * width * 10);
}
// Save and get rows to/from the cache that do NOT have inverted
// cells.
boolean saveInCache = true;

View file

@ -64,6 +64,28 @@ public class HQSixelEncoder implements SixelEncoder {
*/
private static final int FAST_AND_DIRTY = 16;
/**
* When run from the command line, we need both the image, and to know if
* the image is transparent in order to set to correct sixel introducer.
* So toSixel() returns a tuple now.
*/
private class SixelResult {
/**
* The encoded image.
*/
public String encodedImage;
/**
* If true, this image has transparent pixels.
*/
public boolean transparent = false;
/**
* The palette used by this image.
*/
public Palette palette;
}
/**
* Palette is used to manage the conversion of images between 24-bit RGB
* color and a palette of paletteSize colors.
@ -1259,15 +1281,32 @@ public class HQSixelEncoder implements SixelEncoder {
public String toSixel(final BufferedImage bitmap,
final boolean allowTransparent) {
return toSixelResult(bitmap, allowTransparent).encodedImage;
}
/**
* Create a sixel string representing a bitmap. The returned string does
* NOT include the DCS start or ST end sequences.
*
* @param bitmap the bitmap data
* @param allowTransparent if true, allow transparent pixels to be
* specified
* @return the encoded string and transparency flag
*/
private SixelResult toSixelResult(final BufferedImage bitmap,
final boolean allowTransparent) {
// Start with 16k potential total output.
StringBuilder sb = new StringBuilder(16384);
assert (bitmap != null);
int fullHeight = bitmap.getHeight();
SixelResult result = new SixelResult();
// Anaylze the picture and generate a palette.
lastPalette = new Palette(paletteSize, bitmap, allowTransparent);
Palette palette = new Palette(paletteSize, bitmap, allowTransparent);
result.palette = palette;
// DEBUG
/*
@ -1281,18 +1320,19 @@ public class HQSixelEncoder implements SixelEncoder {
*/
// Dither the image
BufferedImage image = lastPalette.ditherImage();
BufferedImage image = palette.ditherImage();
if (lastPalette.timings != null) {
lastPalette.timings.ditherImageTime = System.nanoTime();
if (palette.timings != null) {
palette.timings.ditherImageTime = System.nanoTime();
}
if (image == null) {
if (lastPalette.timings != null) {
lastPalette.timings.emitSixelTime = System.nanoTime();
lastPalette.timings.endTime = System.nanoTime();
if (palette.timings != null) {
palette.timings.emitSixelTime = System.nanoTime();
palette.timings.endTime = System.nanoTime();
}
return "";
result.encodedImage = "";
return result;
}
// DEBUG
@ -1311,7 +1351,7 @@ public class HQSixelEncoder implements SixelEncoder {
int rasterWidth = bitmap.getWidth();
// Emit the palette.
lastPalette.emitPalette(sb);
palette.emitPalette(sb);
// Render the entire row of cells.
int width = image.getWidth();
@ -1339,9 +1379,9 @@ public class HQSixelEncoder implements SixelEncoder {
sixels[imageX][imageY] = colorIdx;
continue;
}
if (!lastPalette.noDither) {
if (!palette.noDither) {
assert (colorIdx >= 0);
assert (colorIdx < lastPalette.sixelColors.size());
assert (colorIdx < palette.sixelColors.size());
}
if (!allowTransparent) {
assert (colorIdx != -1);
@ -1350,7 +1390,7 @@ public class HQSixelEncoder implements SixelEncoder {
}
}
for (int i = 0; i < lastPalette.sixelColors.size(); i++) {
for (int i = 0; i < palette.sixelColors.size(); i++) {
boolean isUsed = false;
for (int imageX = 0; imageX < width; imageX++) {
for (int j = 0; j < 6; j++) {
@ -1445,7 +1485,7 @@ public class HQSixelEncoder implements SixelEncoder {
sb.append((char) oldData);
}
} // for (int i = 0; i < lastPalette.sixelColors.size(); i++)
} // for (int i = 0; i < palette.sixelColors.size(); i++)
// Advance to the next scan line.
sb.append("-");
@ -1458,11 +1498,12 @@ public class HQSixelEncoder implements SixelEncoder {
// Add the raster information.
sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight));
if (lastPalette.timings != null) {
lastPalette.timings.emitSixelTime = System.nanoTime();
lastPalette.timings.endTime = System.nanoTime();
if (palette.timings != null) {
palette.timings.emitSixelTime = System.nanoTime();
palette.timings.endTime = System.nanoTime();
}
return sb.toString();
result.encodedImage = sb.toString();
return result;
}
/**
@ -1543,18 +1584,6 @@ public class HQSixelEncoder implements SixelEncoder {
// NOP
}
/**
* Get the sixel transparency option.
*
* @return true if some pixels will be transparent
*/
public boolean isTransparent() {
if (lastPalette != null) {
return lastPalette.transparent;
}
return false;
}
/**
* Convert all filenames to sixel.
*
@ -1598,13 +1627,15 @@ public class HQSixelEncoder implements SixelEncoder {
BufferedImage image = ImageIO.read(new FileInputStream(args[i]));
// Put together the image.
StringBuilder sb = new StringBuilder();
encoder.emitPalette(sb);
sb.append(encoder.toSixel(image, allowTransparent));
SixelResult result = encoder.toSixelResult(image,
allowTransparent);
result.palette.emitPalette(sb);
sb.append(result.encodedImage);
sb.append("\033\\");
// If there are transparent pixels, we need to note that at
// the beginning.
String header = "\033Pq";
if (encoder.isTransparent() && allowTransparent) {
if (result.transparent && allowTransparent) {
header = "\033P0;1;0q";
}
// Now put it together.
@ -1612,9 +1643,8 @@ public class HQSixelEncoder implements SixelEncoder {
System.out.print(sb.toString());
System.out.flush();
if (encoder.doTimings) {
Palette.Timings timings = encoder.lastPalette.timings;
Palette.Timings timings = result.palette.timings;
double scanTime = (double) (timings.scanImageTime - timings.startTime) / 1.0e9;
double mapTime = (double) (timings.buildColorMapTime - timings.scanImageTime) / 1.0e9;
double ditherTime = (double) (timings.ditherImageTime - timings.buildColorMapTime) / 1.0e9;