diff --git a/README.md b/README.md
index 604d7bc..a28f03b 100644
--- a/README.md
+++ b/README.md
@@ -28,9 +28,11 @@ stock xterm, with a custom mouse icon, and SGR-Pixel mode active:
![Xterm SGR-Pixel Mouse](/screenshots/xterm_pixel_mouse.gif?raw=true "Xterm SGR-Pixel Mouse")
A new sixel encoder is in the works for Xterm, which should look a lot
-better:
+better. This encoder is inspired in part by
+[chafa's](https://hpjansson.org/chafa/) high-performance principal
+component analysis based sixel encoder.
-![Xterm Snake Image](/screenshots/snake_xterm_hq.png?raw=true "Xterm Snake Image - HQ Encoder")
+![PCA color matching with 128-color palette and translucent windows](/screenshots/pca_match.png?raw=true "PCA color matching with 128-color palette and translucent windows")
Jexer can be run inside its own terminal window, with support for all
of its features including images and mouse, and more terminals:
diff --git a/screenshots/pca_match.png b/screenshots/pca_match.png
new file mode 100644
index 0000000..03391fc
Binary files /dev/null and b/screenshots/pca_match.png differ
diff --git a/src/jexer/backend/HQSixelEncoder.java b/src/jexer/backend/HQSixelEncoder.java
index 3e197de..46e7bad 100644
--- a/src/jexer/backend/HQSixelEncoder.java
+++ b/src/jexer/backend/HQSixelEncoder.java
@@ -25,6 +25,12 @@
*
* @author Autumn Lamonte ⚧ Trans Liberation Now
* @version 1
+ *
+ * Portions of this encoder were inspired / influenced by Hans Petter
+ * Jansson's chafa project: https://hpjansson.org/chafa/ . Please refer to
+ * chafa's high-performance sixel encoder for far more advanced
+ * implementations of principal component analysis color mapping, and sixel
+ * row encoding.
*/
package jexer.backend;
@@ -36,6 +42,7 @@ import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.io.FileInputStream;
import java.util.ArrayList;
+import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -48,6 +55,14 @@ import jexer.bits.MathUtils;
* HQSixelEncoder turns a BufferedImage into String of sixel image data,
* using several strategies to produce a reasonably high quality image within
* sixel's ~19.97 bit (101^3) color depth.
+ *
+ *
+ * Portions of this encoder were inspired / influenced by Hans Petter
+ * Jansson's chafa project: https://hpjansson.org/chafa/ . Please refer to
+ * chafa's high-performance sixel encoder for far more advanced
+ * implementations of principal component analysis color mapping, and sixel
+ * row encoding.
+ *
*/
public class HQSixelEncoder implements SixelEncoder {
@@ -485,6 +500,25 @@ public class HQSixelEncoder implements SixelEncoder {
}
};
+ /**
+ * Metadata regarding one sixel row.
+ */
+ private class SixelRow {
+
+ /**
+ * A set of colors that are present in this row.
+ */
+ private BitSet colors;
+
+ /**
+ * Public constructor.
+ */
+ public SixelRow() {
+ colors = new BitSet(sixelColors.size());
+ }
+
+ }
+
/**
* Number of colors in this palette.
*/
@@ -581,6 +615,11 @@ public class HQSixelEncoder implements SixelEncoder {
*/
private ArrayList buckets;
+ /**
+ * The sixel rows of this image.
+ */
+ private SixelRow [] sixelRows;
+
/**
* If true, quantization is done.
*/
@@ -620,6 +659,10 @@ public class HQSixelEncoder implements SixelEncoder {
numColors = Math.min(paletteSize, FAST_AND_DIRTY);
}
sixelColors = new ArrayList(numColors);
+ sixelRows = new SixelRow[(image.getHeight() / 6) + 1];
+ for (int i = 0; i < sixelRows.length; i++) {
+ sixelRows[i] = new SixelRow();
+ }
if (image.getTransparency() == Transparency.TRANSLUCENT) {
// PNG like images where transparency is carried in alpha.
@@ -1226,7 +1269,8 @@ public class HQSixelEncoder implements SixelEncoder {
* . The palette colors have been sorted by their principal
* component (see principal component analysis), such that a binary
* search can quickly find the region where the closest matching
- * color resides.
+ * color resides. One can then search forward and backward to find
+ * all nearby colors.
*
* @param red the red component, from 0-100
* @param green the green component, from 0-100
@@ -1457,7 +1501,9 @@ public class HQSixelEncoder implements SixelEncoder {
int height = sixelImageHeight;
int width = sixelImageWidth;
+ SixelRow sixelRow;
for (int imageY = 0; imageY < height; imageY++) {
+ sixelRow = sixelRows[imageY / 6];
for (int imageX = 0; imageX < width; imageX++) {
int oldPixel = rgbArray[imageX + (width * imageY)];
if ((oldPixel & 0xFF000000) != 0xFF000000) {
@@ -1478,6 +1524,7 @@ public class HQSixelEncoder implements SixelEncoder {
assert (colorIdx < sixelColors.size());
int newPixel = sixelColors.get(colorIdx);
rgbArray[imageX + (width * imageY)] = colorIdx;
+ sixelRow.colors.set(colorIdx);
if (quantizationType == 0) {
// For direct map, every possible color is already in
@@ -1763,41 +1810,11 @@ public class HQSixelEncoder implements SixelEncoder {
// Render the entire row of cells.
int width = bitmap.getWidth();
- // TODO: sixels[][] shouldn't be necessary, and both of these loops
- // should be able to be combined.
- int [][] sixels = new int[width][6];
for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
+ Palette.SixelRow sixelRow = palette.sixelRows[currentRow / 6];
- // See which colors are actually used in this band of sixels.
- boolean [] usedColors = new boolean[palette.sixelColors.size()];
- for (int imageX = 0; imageX < width; imageX++) {
- for (int imageY = 0;
- (imageY < 6) && (imageY + currentRow < fullHeight);
- imageY++) {
-
- int colorIdx = rgbArray[imageX +
- (width * (imageY + currentRow))];
-
- if (allowTransparent && (colorIdx == -1)) {
- sixels[imageX][imageY] = colorIdx;
- continue;
- }
- /*
- if (!palette.noDither) {
- assert (colorIdx >= 0);
- assert (colorIdx < palette.sixelColors.size());
- }
- if (!allowTransparent) {
- assert (colorIdx != -1);
- }
- */
- sixels[imageX][imageY] = colorIdx;
- usedColors[colorIdx] = true;
- }
- }
-
- for (int i = 0; i < usedColors.length; i++) {
- if (!usedColors[i]) {
+ for (int i = 0; i < paletteSize; i++) {
+ if (!sixelRow.colors.get(i)) {
continue;
}
@@ -1811,12 +1828,15 @@ public class HQSixelEncoder implements SixelEncoder {
for (int imageX = 0; imageX < width; imageX++) {
// Add up all the pixels that match this color.
+
+ // TODO: Make this scan horizontally first to build the
+ // row, so as to reduce all the random access.
int data = 0;
for (int j = 0;
(j < 6) && (currentRow + j < fullHeight);
j++) {
- if (sixels[imageX][j] == i) {
+ if (rgbArray[imageX + (width * (currentRow + j))] == i) {
switch (j) {
case 0:
data += 1;