terminal-image
terminal-image copied to clipboard
SIXEL support?
For higher quality images you might want to add optional SIXEL output (supported by several terminals).
Are there actually any popular terminals that support SIXEL?
@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.
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": {
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
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!");
})();
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:).
@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).
@Richienb Published an updated version of the lib, the snippet in my last comment should now work.