mirror of
https://gitlab.com/AutumnMeowMeow/jexer
synced 2024-09-19 11:50:19 -06:00
stub for new sixel encoder
This commit is contained in:
parent
86da95e0cf
commit
0cd2e48745
2 changed files with 845 additions and 7 deletions
838
src/jexer/backend/HQSixelEncoder.java
Normal file
838
src/jexer/backend/HQSixelEncoder.java
Normal file
|
@ -0,0 +1,838 @@
|
|||
/*
|
||||
* 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.backend;
|
||||
|
||||
import java.awt.Transparency;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.FileInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class HQSixelEncoder {
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Constants --------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Alpha value (0 - 255) above which to consider the pixel opaque.
|
||||
*/
|
||||
private static final int ALPHA_OPAQUE = 102; // ~40%
|
||||
|
||||
/**
|
||||
* Palette is used to manage the conversion of images between 24-bit RGB
|
||||
* color and a palette of paletteSize colors.
|
||||
*/
|
||||
private class Palette {
|
||||
|
||||
/**
|
||||
* ColorIdx records a RGB color and its palette index.
|
||||
*/
|
||||
private class ColorIdx {
|
||||
|
||||
/**
|
||||
* The ~19.97-bit RGB color. Each component has a value between
|
||||
* 0 and 100.
|
||||
*/
|
||||
public int color;
|
||||
|
||||
/**
|
||||
* The palette index for this color.
|
||||
*/
|
||||
public int index;
|
||||
|
||||
/**
|
||||
* The population count for this color.
|
||||
*/
|
||||
public int count = 0;
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param color the ~19.97-bit sixel color
|
||||
* @param index the palette index for this color
|
||||
*/
|
||||
public ColorIdx(final int color, final int index) {
|
||||
this.color = color;
|
||||
this.index = index;
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor. Count is set to 1, index to -1.
|
||||
*
|
||||
* @param color the ~19.97-bit sixel color
|
||||
*/
|
||||
public ColorIdx(final int color) {
|
||||
this.color = color;
|
||||
this.index = -1;
|
||||
this.count = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash only on color.
|
||||
*
|
||||
* @return the hash
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable string for this entry.
|
||||
*
|
||||
* @return a human-readable string
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("color %06x index %d count %d",
|
||||
color, index, count);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of colors in this palette.
|
||||
*/
|
||||
private int paletteSize = 0;
|
||||
|
||||
/**
|
||||
* Color palette for sixel output, sorted low to high.
|
||||
*/
|
||||
private List<Integer> sixelColors = null;
|
||||
|
||||
/**
|
||||
* Map of colors used in the image by RGB.
|
||||
*/
|
||||
private HashMap<Integer, ColorIdx> colorMap = null;
|
||||
|
||||
/**
|
||||
* Type of color quantization used.
|
||||
*
|
||||
* 0 = direct map; 1 = median cut; 2 = octree.
|
||||
*/
|
||||
private int quantizationType = -1;
|
||||
|
||||
/**
|
||||
* The image from the constructor, mapped to sixel color space with
|
||||
* transparent pixels removed.
|
||||
*/
|
||||
private BufferedImage sixelImage;
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* @param size number of colors available for this palette
|
||||
* @param image a bitmap image
|
||||
*/
|
||||
public Palette(final int size, final BufferedImage image) {
|
||||
assert (size > 2);
|
||||
|
||||
paletteSize = size;
|
||||
sixelColors = new ArrayList<Integer>(size);
|
||||
|
||||
boolean transparent = false;
|
||||
if (image.getTransparency() == Transparency.TRANSLUCENT) {
|
||||
transparent = true;
|
||||
}
|
||||
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
sixelImage = new BufferedImage(image.getWidth(), image.getHeight(),
|
||||
BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
if (verbosity >= 1) {
|
||||
System.err.printf("Palette() image is %dx%d, bpp %d transparent %s\n",
|
||||
width, height, image.getColorModel().getPixelSize(),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
// Perform population count on colors.
|
||||
int [] rgbArray = image.getRGB(0, 0, width, height, null, 0, width);
|
||||
colorMap = new HashMap<Integer, ColorIdx>(width * height);
|
||||
int transparent_count = 0;
|
||||
for (int i = 0; i < rgbArray.length; i++) {
|
||||
int colorRGB = rgbArray[i];
|
||||
if (transparent) {
|
||||
int alpha = ((colorRGB >>> 24) & 0xFF);
|
||||
if (alpha < ALPHA_OPAQUE) {
|
||||
// This pixel is almost transparent, omit it.
|
||||
transparent_count++;
|
||||
rgbArray[i] = 0x00f7a8b8;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Pull the 8-bit colors, and reduce them to 0-100 as per
|
||||
// sixel.
|
||||
int sixelRGB = toSixelColor(colorRGB);
|
||||
rgbArray[i] = sixelRGB;
|
||||
ColorIdx color = colorMap.get(sixelRGB);
|
||||
if (color == null) {
|
||||
color = new ColorIdx(sixelRGB);
|
||||
colorMap.put(sixelRGB, color);
|
||||
} else {
|
||||
color.count++;
|
||||
}
|
||||
}
|
||||
// Save the image data mapped to the 101^3 sixel color space.
|
||||
// This also sets any pixels with partial transparency below
|
||||
// ALPHA_OPAQUE to fully transparent (and pink).
|
||||
sixelImage.setRGB(0, 0, width, height, rgbArray, 0, width);
|
||||
|
||||
if (verbosity >= 1) {
|
||||
System.err.printf("# colors in image: %d palette size %d\n",
|
||||
colorMap.size(), paletteSize);
|
||||
System.err.printf("# transparent pixels: %d (%3.1f%%)\n",
|
||||
transparent_count,
|
||||
(double) transparent_count * 100.0 / (width * height));
|
||||
}
|
||||
|
||||
/*
|
||||
* Here we choose between several options:
|
||||
*
|
||||
* - If the palette size is big enough for the number of colors,
|
||||
* then just do a straight 1-1 mapping.
|
||||
*
|
||||
* - If the (number of colors:palette size) ratio is below 10,
|
||||
* use median-cut.
|
||||
*
|
||||
* - Otherwise use octree.
|
||||
*/
|
||||
if (paletteSize >= colorMap.size()) {
|
||||
quantizationType = 0;
|
||||
directMap();
|
||||
} else if (colorMap.size() <= paletteSize * 10) {
|
||||
quantizationType = 1;
|
||||
medianCut();
|
||||
} else {
|
||||
quantizationType = 2;
|
||||
octree();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a 24-bit color to a 19.97-bit sixel color.
|
||||
*
|
||||
* @param rawColor the 24-bit color
|
||||
* @return the sixel color
|
||||
*/
|
||||
public int toSixelColor(final int rawColor) {
|
||||
int red = ((rawColor >>> 16) & 0xFF) * 100 / 256;
|
||||
int green = ((rawColor >>> 8) & 0xFF) * 100 / 256;
|
||||
int blue = ( rawColor & 0xFF) * 100 / 256;
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign palette entries to the image colors. This requires at
|
||||
* least as many palette colors as number of colors used in the
|
||||
* image.
|
||||
*/
|
||||
public void directMap() {
|
||||
assert (paletteSize >= colorMap.size());
|
||||
|
||||
if (verbosity >= 1) {
|
||||
System.err.println("Direct-map colors");
|
||||
}
|
||||
|
||||
// The simplest thing: just put the used colors in RGB order. We
|
||||
// don't _need_ an ordering, but it does make it nicer to look at
|
||||
// the generated output and understand what's going on.
|
||||
sixelColors = new ArrayList<Integer>(colorMap.size());
|
||||
for (Integer key: colorMap.keySet()) {
|
||||
sixelColors.add(colorMap.get(key).color);
|
||||
}
|
||||
Collections.sort(sixelColors);
|
||||
assert (sixelColors.size() == colorMap.size());
|
||||
for (int i = 0; i < sixelColors.size(); i++) {
|
||||
colorMap.get(sixelColors.get(i)).index = i;
|
||||
}
|
||||
|
||||
if (verbosity >= 1) {
|
||||
System.err.printf("colorMap size %d sixelColors size %d\n",
|
||||
colorMap.size(), sixelColors.size());
|
||||
if (verbosity >= 5) {
|
||||
System.err.printf("COLOR MAP:\n");
|
||||
for (int i = 0; i < sixelColors.size(); i++) {
|
||||
System.err.printf(" %03d %s\n", i,
|
||||
colorMap.get(sixelColors.get(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform median-cut algorithm to generate a palette that fits
|
||||
* within the palette size.
|
||||
*/
|
||||
public void medianCut() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform octree-based color quantization.
|
||||
*/
|
||||
public void octree() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamp an int value to [0, 100].
|
||||
*
|
||||
* @param x the int value
|
||||
* @return an int between 0 and 100.
|
||||
*/
|
||||
private int clampSixel(final int x) {
|
||||
if (x < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (x > 100) {
|
||||
return 100;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the nearest match for a color in the palette.
|
||||
*
|
||||
* @param color the sixel color
|
||||
* @return the color in the palette that is closest to color
|
||||
*/
|
||||
public int matchColor(final int color) {
|
||||
|
||||
assert (color >= 0);
|
||||
|
||||
assert ((quantizationType == 0)
|
||||
|| (quantizationType == 1)
|
||||
|| (quantizationType == 2));
|
||||
|
||||
if (quantizationType == 0) {
|
||||
ColorIdx colorIdx = colorMap.get(color);
|
||||
if (verbosity >= 10) {
|
||||
System.err.printf("matchColor(): %08x %d colorIdx %s\n",
|
||||
color, color, colorIdx);
|
||||
}
|
||||
return colorIdx.index;
|
||||
} else if (quantizationType == 1) {
|
||||
// TODO: median cut
|
||||
return 0;
|
||||
} else {
|
||||
// TODO: octree
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dither an image to a paletteSize palette. The dithered
|
||||
* image cells will contain indexes into the palette.
|
||||
*
|
||||
* @return the dithered image. Every pixel is an index into the
|
||||
* palette.
|
||||
*/
|
||||
public BufferedImage ditherImage() {
|
||||
|
||||
if (quantizationType == 1) {
|
||||
// TODO: support median cut
|
||||
return null;
|
||||
}
|
||||
if (quantizationType == 2) {
|
||||
// TODO: support octree
|
||||
return null;
|
||||
}
|
||||
|
||||
BufferedImage ditheredImage = new BufferedImage(sixelImage.getWidth(),
|
||||
sixelImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
int [] rgbArray = sixelImage.getRGB(0, 0, sixelImage.getWidth(),
|
||||
sixelImage.getHeight(), null, 0, sixelImage.getWidth());
|
||||
ditheredImage.setRGB(0, 0, sixelImage.getWidth(),
|
||||
sixelImage.getHeight(), rgbArray, 0, sixelImage.getWidth());
|
||||
|
||||
for (int imageY = 0; imageY < sixelImage.getHeight(); imageY++) {
|
||||
for (int imageX = 0; imageX < sixelImage.getWidth(); imageX++) {
|
||||
int oldPixel = ditheredImage.getRGB(imageX,
|
||||
imageY);
|
||||
if (oldPixel == 0) {
|
||||
// This is a transparent pixel, skip it.
|
||||
continue;
|
||||
}
|
||||
int colorIdx = matchColor(oldPixel);
|
||||
assert (colorIdx >= 0);
|
||||
assert (colorIdx < paletteSize);
|
||||
int newPixel = sixelColors.get(colorIdx);
|
||||
ditheredImage.setRGB(imageX, imageY, colorIdx);
|
||||
|
||||
int oldRed = (oldPixel >>> 16) & 0xFF;
|
||||
int oldGreen = (oldPixel >>> 8) & 0xFF;
|
||||
int oldBlue = oldPixel & 0xFF;
|
||||
|
||||
int newRed = (newPixel >>> 16) & 0xFF;
|
||||
int newGreen = (newPixel >>> 8) & 0xFF;
|
||||
int newBlue = newPixel & 0xFF;
|
||||
|
||||
int redError = (oldRed - newRed) / 16;
|
||||
int greenError = (oldGreen - newGreen) / 16;
|
||||
int blueError = (oldBlue - newBlue) / 16;
|
||||
|
||||
int red, green, blue;
|
||||
if (imageX < sixelImage.getWidth() - 1) {
|
||||
int pXpY = ditheredImage.getRGB(imageX + 1, imageY);
|
||||
red = ((pXpY >>> 16) & 0xFF) + (7 * redError);
|
||||
green = ((pXpY >>> 8) & 0xFF) + (7 * greenError);
|
||||
blue = ( pXpY & 0xFF) + (7 * blueError);
|
||||
red = clampSixel(red);
|
||||
green = clampSixel(green);
|
||||
blue = clampSixel(blue);
|
||||
pXpY = ((red & 0xFF) << 16);
|
||||
pXpY |= ((green & 0xFF) << 8) | (blue & 0xFF);
|
||||
ditheredImage.setRGB(imageX + 1, imageY, pXpY);
|
||||
if (imageY < sixelImage.getHeight() - 1) {
|
||||
int pXpYp = ditheredImage.getRGB(imageX + 1,
|
||||
imageY + 1);
|
||||
red = ((pXpYp >>> 16) & 0xFF) + redError;
|
||||
green = ((pXpYp >>> 8) & 0xFF) + greenError;
|
||||
blue = ( pXpYp & 0xFF) + blueError;
|
||||
red = clampSixel(red);
|
||||
green = clampSixel(green);
|
||||
blue = clampSixel(blue);
|
||||
pXpYp = ((red & 0xFF) << 16);
|
||||
pXpYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
|
||||
ditheredImage.setRGB(imageX + 1, imageY + 1, pXpYp);
|
||||
}
|
||||
} else if (imageY < sixelImage.getHeight() - 1) {
|
||||
int pXmYp = ditheredImage.getRGB(imageX - 1,
|
||||
imageY + 1);
|
||||
int pXYp = ditheredImage.getRGB(imageX,
|
||||
imageY + 1);
|
||||
|
||||
red = ((pXmYp >>> 16) & 0xFF) + (3 * redError);
|
||||
green = ((pXmYp >>> 8) & 0xFF) + (3 * greenError);
|
||||
blue = ( pXmYp & 0xFF) + (3 * blueError);
|
||||
red = clampSixel(red);
|
||||
green = clampSixel(green);
|
||||
blue = clampSixel(blue);
|
||||
pXmYp = ((red & 0xFF) << 16);
|
||||
pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
|
||||
ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp);
|
||||
|
||||
red = ((pXYp >>> 16) & 0xFF) + (5 * redError);
|
||||
green = ((pXYp >>> 8) & 0xFF) + (5 * greenError);
|
||||
blue = ( pXYp & 0xFF) + (5 * blueError);
|
||||
red = clampSixel(red);
|
||||
green = clampSixel(green);
|
||||
blue = clampSixel(blue);
|
||||
pXYp = ((red & 0xFF) << 16);
|
||||
pXYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
|
||||
ditheredImage.setRGB(imageX, imageY + 1, pXYp);
|
||||
}
|
||||
} // for (int imageY = 0; imageY < image.getHeight(); imageY++)
|
||||
} // for (int imageX = 0; imageX < image.getWidth(); imageX++)
|
||||
|
||||
return ditheredImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the sixel palette.
|
||||
*
|
||||
* @param sb the StringBuilder to append to
|
||||
* @param used array of booleans set to true for each color actually
|
||||
* used in this cell, or null to emit the entire palette
|
||||
* @return the string to emit to an ANSI / ECMA-style terminal
|
||||
*/
|
||||
public String emitPalette(final StringBuilder sb,
|
||||
final boolean [] used) {
|
||||
|
||||
for (int i = 0; i < paletteSize; i++) {
|
||||
if (((used != null) && (used[i] == true)) || (used == null)) {
|
||||
int sixelColor = sixelColors.get(i);
|
||||
sb.append(String.format("#%d;2;%d;%d;%d", i,
|
||||
((sixelColor >>> 16) & 0xFF),
|
||||
((sixelColor >>> 8) & 0xFF),
|
||||
( sixelColor & 0xFF)));
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Variables --------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Verbosity level for analysis mode.
|
||||
*/
|
||||
private int verbosity = 0;
|
||||
|
||||
/**
|
||||
* Number of colors in the sixel palette. Xterm 335 defines the max as
|
||||
* 1024.
|
||||
*/
|
||||
private int paletteSize = 1024;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Constructors -----------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*/
|
||||
public HQSixelEncoder() {
|
||||
reloadOptions();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// HQSixelEncoder ---------------------------------------------------------
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Reload options from System properties.
|
||||
*/
|
||||
public void reloadOptions() {
|
||||
// Palette size
|
||||
int paletteSize = 1024;
|
||||
try {
|
||||
paletteSize = Integer.parseInt(System.getProperty(
|
||||
"jexer.ECMA48.sixelPaletteSize", "1024"));
|
||||
switch (paletteSize) {
|
||||
case 2:
|
||||
case 256:
|
||||
case 512:
|
||||
case 1024:
|
||||
case 2048:
|
||||
this.paletteSize = paletteSize;
|
||||
break;
|
||||
default:
|
||||
// Ignore value
|
||||
break;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// SQUASH
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return the string to emit to an ANSI / ECMA-style terminal
|
||||
*/
|
||||
public String toSixel(final BufferedImage bitmap) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
assert (bitmap != null);
|
||||
|
||||
int fullHeight = bitmap.getHeight();
|
||||
|
||||
// Anaylze the picture and generate a palette.
|
||||
Palette palette = new Palette(paletteSize, bitmap);
|
||||
|
||||
// Dither the image
|
||||
BufferedImage image = palette.ditherImage();
|
||||
|
||||
if (image == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Collect the raster information
|
||||
int rasterHeight = 0;
|
||||
int rasterWidth = bitmap.getWidth();
|
||||
|
||||
// Emit the palette, but only for the colors actually used by
|
||||
// these cells.
|
||||
boolean [] usedColors = new boolean[paletteSize];
|
||||
for (int imageX = 0; imageX < image.getWidth(); imageX++) {
|
||||
for (int imageY = 0; imageY < image.getHeight(); imageY++) {
|
||||
if (verbosity >= 10) {
|
||||
System.err.printf("RGB(%d,%d): %08x %d\n",
|
||||
imageX, imageY, image.getRGB(imageX, imageY),
|
||||
image.getRGB(imageX, imageY));
|
||||
}
|
||||
usedColors[image.getRGB(imageX, imageY)] = true;
|
||||
}
|
||||
}
|
||||
palette.emitPalette(sb, usedColors);
|
||||
|
||||
// Render the entire row of cells.
|
||||
for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
|
||||
int [][] sixels = new int[image.getWidth()][6];
|
||||
|
||||
// See which colors are actually used in this band of sixels.
|
||||
for (int imageX = 0; imageX < image.getWidth(); imageX++) {
|
||||
for (int imageY = 0;
|
||||
(imageY < 6) && (imageY + currentRow < fullHeight);
|
||||
imageY++) {
|
||||
|
||||
int colorIdx = image.getRGB(imageX, imageY + currentRow);
|
||||
if (colorIdx == -1) {
|
||||
continue;
|
||||
}
|
||||
assert (colorIdx >= 0);
|
||||
assert (colorIdx < paletteSize);
|
||||
|
||||
sixels[imageX][imageY] = colorIdx;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < paletteSize; i++) {
|
||||
boolean isUsed = false;
|
||||
for (int imageX = 0; imageX < image.getWidth(); imageX++) {
|
||||
for (int j = 0; j < 6; j++) {
|
||||
if (sixels[imageX][j] == i) {
|
||||
isUsed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isUsed == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set to the beginning of scan line for the next set of
|
||||
// colored pixels, and select the color.
|
||||
sb.append(String.format("$#%d", i));
|
||||
|
||||
int oldData = -1;
|
||||
int oldDataCount = 0;
|
||||
for (int imageX = 0; imageX < image.getWidth(); imageX++) {
|
||||
|
||||
// Add up all the pixels that match this color.
|
||||
int data = 0;
|
||||
for (int j = 0;
|
||||
(j < 6) && (currentRow + j < fullHeight);
|
||||
j++) {
|
||||
|
||||
if (sixels[imageX][j] == i) {
|
||||
switch (j) {
|
||||
case 0:
|
||||
data += 1;
|
||||
break;
|
||||
case 1:
|
||||
data += 2;
|
||||
break;
|
||||
case 2:
|
||||
data += 4;
|
||||
break;
|
||||
case 3:
|
||||
data += 8;
|
||||
break;
|
||||
case 4:
|
||||
data += 16;
|
||||
break;
|
||||
case 5:
|
||||
data += 32;
|
||||
break;
|
||||
}
|
||||
if ((currentRow + j + 1) > rasterHeight) {
|
||||
rasterHeight = currentRow + j + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert (data >= 0);
|
||||
assert (data < 64);
|
||||
data += 63;
|
||||
|
||||
if (data == oldData) {
|
||||
oldDataCount++;
|
||||
} else {
|
||||
if (oldDataCount == 1) {
|
||||
sb.append((char) oldData);
|
||||
} else if (oldDataCount > 1) {
|
||||
sb.append(String.format("!%d", oldDataCount));
|
||||
sb.append((char) oldData);
|
||||
}
|
||||
oldDataCount = 1;
|
||||
oldData = data;
|
||||
}
|
||||
|
||||
} // for (int imageX = 0; imageX < image.getWidth(); imageX++)
|
||||
|
||||
// Emit the last sequence.
|
||||
if (oldDataCount == 1) {
|
||||
sb.append((char) oldData);
|
||||
} else if (oldDataCount > 1) {
|
||||
sb.append(String.format("!%d", oldDataCount));
|
||||
sb.append((char) oldData);
|
||||
}
|
||||
|
||||
} // for (int i = 0; i < paletteSize; i++)
|
||||
|
||||
// Advance to the next scan line.
|
||||
sb.append("-");
|
||||
|
||||
} // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
|
||||
|
||||
// Kill the very last "-", because it is unnecessary.
|
||||
sb.deleteCharAt(sb.length() - 1);
|
||||
|
||||
// Add the raster information
|
||||
sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the palette is shared for the entire terminal, emit it to a
|
||||
* StringBuilder.
|
||||
*
|
||||
* @param sb the StringBuilder to write the shared palette to
|
||||
*/
|
||||
public void emitPalette(final StringBuilder sb) {
|
||||
// NOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sixel shared palette option.
|
||||
*
|
||||
* @return true if all sixel output is using the same palette that is set
|
||||
* in one DCS sequence and used in later sequences
|
||||
*/
|
||||
public boolean hasSharedPalette() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sixel shared palette option.
|
||||
*
|
||||
* @param sharedPalette if true, then all sixel output will use the same
|
||||
* palette that is set in one DCS sequence and used in later sequences
|
||||
*/
|
||||
public void setSharedPalette(final boolean sharedPalette) {
|
||||
// NOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of colors in the sixel palette.
|
||||
*
|
||||
* @return the palette size
|
||||
*/
|
||||
public int getPaletteSize() {
|
||||
return paletteSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the number of colors in the sixel palette.
|
||||
*
|
||||
* @param paletteSize the new palette size
|
||||
*/
|
||||
public void setPaletteSize(final int paletteSize) {
|
||||
if (this.paletteSize == paletteSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (paletteSize) {
|
||||
case 2:
|
||||
case 256:
|
||||
case 512:
|
||||
case 1024:
|
||||
case 2048:
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported sixel palette " +
|
||||
" size: " + paletteSize);
|
||||
}
|
||||
|
||||
this.paletteSize = paletteSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the sixel palette. It will be regenerated on the next image
|
||||
* encode.
|
||||
*/
|
||||
public void clearPalette() {
|
||||
// NOP
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all filenames to sixel.
|
||||
*
|
||||
* @param args[] the filenames to read
|
||||
*/
|
||||
public static void main(final String [] args) {
|
||||
if ((args.length == 0)
|
||||
|| ((args.length == 1) && args[0].equals("-v"))
|
||||
|| ((args.length == 1) && args[0].equals("-vv"))
|
||||
) {
|
||||
System.err.println("USAGE: java jexer.backend.HQSixelEncoder [ -v | -vv ] { file1 [ file2 ... ] }");
|
||||
System.exit(-1);
|
||||
}
|
||||
|
||||
HQSixelEncoder encoder = new HQSixelEncoder();
|
||||
int successCount = 0;
|
||||
if (encoder.hasSharedPalette()) {
|
||||
System.out.print("\033[?1070l");
|
||||
} else {
|
||||
System.out.print("\033[?1070h");
|
||||
}
|
||||
System.out.flush();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ((i == 0) && args[i].equals("-v")) {
|
||||
encoder.verbosity = 1;
|
||||
continue;
|
||||
}
|
||||
if ((i == 0) && args[i].equals("-vv")) {
|
||||
encoder.verbosity = 10;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new FileInputStream(args[i]));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\033Pq");
|
||||
encoder.emitPalette(sb);
|
||||
sb.append(encoder.toSixel(image));
|
||||
sb.append("\033\\");
|
||||
System.out.print(sb.toString());
|
||||
System.out.flush();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error reading file:");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
System.out.print("\033[?1070h");
|
||||
System.out.flush();
|
||||
if (successCount == args.length) {
|
||||
System.exit(0);
|
||||
} else {
|
||||
System.exit(successCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -46,10 +46,10 @@ public class SixelEncoder {
|
|||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* SixelPalette is used to manage the conversion of images between 24-bit
|
||||
* RGB color and a palette of paletteSize colors.
|
||||
* Palette is used to manage the conversion of images between 24-bit RGB
|
||||
* color and a palette of paletteSize colors.
|
||||
*/
|
||||
private class SixelPalette {
|
||||
private class Palette {
|
||||
|
||||
/**
|
||||
* Color palette for sixel output, sorted low to high.
|
||||
|
@ -127,7 +127,7 @@ public class SixelEncoder {
|
|||
/**
|
||||
* Public constructor.
|
||||
*/
|
||||
public SixelPalette() {
|
||||
public Palette() {
|
||||
makePalette();
|
||||
}
|
||||
|
||||
|
@ -687,7 +687,7 @@ public class SixelEncoder {
|
|||
/**
|
||||
* The sixel palette handler.
|
||||
*/
|
||||
private SixelPalette palette = null;
|
||||
private Palette palette = null;
|
||||
|
||||
/**
|
||||
* Number of colors in the sixel palette. Xterm 335 defines the max as
|
||||
|
@ -761,7 +761,7 @@ public class SixelEncoder {
|
|||
|
||||
// Dither the image. It is ok to lose the original here.
|
||||
if (palette == null) {
|
||||
palette = new SixelPalette();
|
||||
palette = new Palette();
|
||||
if (sharedPalette == true) {
|
||||
palette.emitPalette(sb, null);
|
||||
}
|
||||
|
@ -909,7 +909,7 @@ public class SixelEncoder {
|
|||
*/
|
||||
public void emitPalette(final StringBuilder sb) {
|
||||
if (palette == null) {
|
||||
palette = new SixelPalette();
|
||||
palette = new Palette();
|
||||
if (sharedPalette == true) {
|
||||
palette.emitPalette(sb, null);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue