terminal-image icon indicating copy to clipboard operation
terminal-image copied to clipboard

SIXEL support?

Open jerch opened this issue 5 years ago • 8 comments

For higher quality images you might want to add optional SIXEL output (supported by several terminals).

jerch avatar Jul 19 '20 21:07 jerch

Are there actually any popular terminals that support SIXEL?

sindresorhus avatar Jul 19 '20 21:07 sindresorhus

@sindresorhus I think so:

  • xterm (behind compile switch)
  • vte (currently still WIP, but covers 60% of linux desktop terminals once released)
  • iTerm2
  • mintty
  • and several more

The support got way better in the last years. The hard part might be to decide, whether to use SIXEL on a certain terminal. Maybe a switch would do in the beginning, so users could test themselves, if their terminal can handle it.

jerch avatar Jul 20 '20 08:07 jerch

The patch below adds basic SIXEL support. Flaws to be addressed, if someone wants to integrate that further:

  • way to get the real viewport size is just a bad hack based on your "cell pixels"
  • quantizer lib is not that good, quite slow and even buggy (find a better one?)
  • needs better integration (own export?), prolly with some of the settings being exposed
diff --git a/index.js b/index.js
index 896bb8e..78c468d 100644
--- a/index.js
+++ b/index.js
@@ -6,6 +6,8 @@ const Jimp = require('jimp');
 const termImg = require('term-img');
 const renderGif = require('render-gif');
 const logUpdate = require('log-update');
+const sixel = require('sixel');
+const RgbQuant = require('rgbquant');
 
 // `log-update` adds an extra newline so the generated frames need to be 2 pixels shorter.
 const ROW_OFFSET = 2;
@@ -97,11 +99,44 @@ async function render(buffer, {width: inputWidth, height: inputHeight, preserveA
 	return result;
 }
 
-exports.buffer = async (buffer, {width = '100%', height = '100%', preserveAspectRatio = true} = {}) => {
+async function renderSixel(buffer, {width: inputWidth, height: inputHeight, preserveAspectRatio}) {
+	// some conversion settings
+	const BACKGROUND_SELECT = 0;
+	const MAX_COLORS = 64;	// higher gives better quality
+
+	const image = await Jimp.read(buffer);
+	const {bitmap} = image;
+
+	// CELL_TO_PIXEL_FACTOR: hack to get close to terminal coverage (scales roughly with selected font size)
+	// ideally the real terminal viewport size would be queried from ioctl(TIOCGWINSZ) or by CSI 14 t
+	const CELL_TO_PIXEL_FACTOR = 7;
+	let {width, height} = calculateWidthHeight(bitmap.width, bitmap.height, inputWidth, inputHeight, preserveAspectRatio);
+	width *= CELL_TO_PIXEL_FACTOR;
+	height *= CELL_TO_PIXEL_FACTOR;
+
+	image.resize(width, height);
+
+	// quantisation
+	// Note: quite slow for bigger images and more colors, qualifies for worker or better quant lib
+	const q = new RgbQuant({colors: MAX_COLORS, dithKern: 'FloydSteinberg', dithSerp: true});
+	q.sample(image.bitmap.data);
+	const palette = q.palette(true);
+	const quantizedData = q.reduce(image.bitmap.data);
+
+	// sixel encoding
+	const result = sixel.sixelEncode(quantizedData, width, height, palette);
+
+	// return sixel data embedded in escape sequence
+	return sixel.introducer(BACKGROUND_SELECT) + result + sixel.FINALIZER;
+}
+
+exports.buffer = async (buffer, {width = '100%', height = '100%', preserveAspectRatio = true, sixel = false} = {}) => {
 	return termImg(buffer, {
 		width,
 		height,
-		fallback: () => render(buffer, {height, width, preserveAspectRatio})
+		fallback: () => sixel
+			? renderSixel(buffer, {height, width, preserveAspectRatio})
+			: render(buffer, {height, width, preserveAspectRatio})
 	});
 };
 
diff --git a/package.json b/package.json
index 26dda9e..f12eea5 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,8 @@
 		"jimp": "^0.14.0",
 		"log-update": "^4.0.0",
 		"render-gif": "^2.0.4",
+		"rgbquant": "^1.1.2",
+		"sixel": "^0.12.0",
 		"term-img": "^5.0.0"
 	},
 	"devDependencies": {

jerch avatar Jul 21 '20 14:07 jerch

We can use the Primary Device Attributes ANSI escape code to detect support. The only challenge is writing the escape code to stdout and then capturing the stdin straight after. See: https://stackoverflow.com/questions/63051961/querying-primary-device-attributes

Richienb avatar Jul 23 '20 10:07 Richienb

Alright, we can now automatically detect sixel support so that it is possible to autonomously switch between regular and sixel rendering:

const supportsSixel = require("supports-sixel");

(async () => {
	const isSupported = await supportsSixel();

	console.log(isSupported ? "Sixels are supported!" : "Sixels aren't supported!");
})();

Richienb avatar Jul 24 '20 09:07 Richienb

Also have an optimized quantizer in the making on my playground branch, with this dealing with the image processing semantics will be a no-brainer (well, hopefully :smile_cat:).

jerch avatar Jul 24 '20 12:07 jerch

@Richienb Created a PR with an included quantizer: https://github.com/jerch/node-sixel/pull/16

This turns out to be much faster (at least 5 times), and comes with an easy to use interface as:

const { image2sixel } = require('sixel');  // not working from npm as not published yet

console.log(image2sixel(pixelData, width, height));

It supports more optional arguments, that might be needed sometimes (like the terminal might restrict the usable colors).

jerch avatar Jul 26 '20 10:07 jerch

@Richienb Published an updated version of the lib, the snippet in my last comment should now work.

jerch avatar Nov 18 '20 16:11 jerch