Képek megjelenítése a konzolon
Tegnap este felmerült bennem, hogy a Windows 10 konzol támogat Virtual Terminal funkciókat, ami jelentősen bővíti a konzol képességeit. Vajon képek megjelenítésére is tudnánk használni?
A virtual terminal funkciók a Windows 10 2016-os frissítésében mutatkoztak be először és leginkább a Windows Subsystem for Linux megvalósításához kellett. Ez lényegében egy bővített VT100-as terminálnak fogható fel. A tradicionális Windows konzol és e között leginkább az a különbség, hogy a Windows konzolja anno formázatlan szöveg megjelenítésére lett tervezve, valamint a szín információkat (előtér és háttér) API hívásokkal lehetett változtatni, míg a virtual terminal egyfajta leíró nyelvet használ, ami a szöveggel keveri a formázási információkat, úgynevezett escape kódok segítségével.
Ez lehetővé teszi, hogy akár minden egyes karakter előtér -és háttér színét 24 bites RGB módon meghatározzuk, vagyis ugyan pixelesen és elmosottan, de akár képeket is meg tudunk jeleníteni valami hasonló módon:
A megjelenítés lépései:
- Kép betöltése
- Kép átméretezése a konzol méretére
- A kép pixel adataiból VT kódok előállítása
- A szöveg megjelenítése
A képek betöltése és átméretezése a „legbonyolultabb” része az egész programnak, főleg ha multiplatform alkalmazást szeretnénk írni. Kézenfekvő lenne használni a System.Drawing névtérben található API-t, ami most már talán multiplatform. Ennek ellenére a példaprogram nem ezt használja, hanem a szintén multiplatform SkiaSharp könyvtárat, ami a Google Skia könyvtárának a .NET megfelelője, amit a Microsoft tart karban. A választás erre a könyvtárra azért esett, mert jóval fejlettebb a System.Drawing API-nál és a hamarosan megjelenő MAUI mögött is ez a rajzoló könyvtár van.
A teljes program:
using SkiaSharp;
using System;
using System.Text;
namespace ImgDisplay
{
internal static class Program
{
public static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Usage: ImgDisplay [imgage]");
}
if (OperatingSystem.IsWindows())
{
Console.WindowWidth = 132;
Console.WindowHeight = 40;
}
using SKBitmap img = SKBitmap.Decode(args[0]);
using SKBitmap finalSize = Resize(img,
Console.WindowWidth,
Console.WindowHeight);
VT100Encode(finalSize);
}
private static void VT100Encode(SKBitmap finalSize)
{
StringBuilder buffer = new StringBuilder(finalSize.Width);
for (int y=0; y<finalSize.Height; y++)
{
for (int x=0; x<finalSize.Width; x++)
{
var pixel = finalSize.GetPixel(x, y);
buffer.AppendFormat("\x1b[48;2;{0};{1};{2}m ",
pixel.Red,
pixel.Green,
pixel.Blue);
}
Console.WriteLine(buffer.ToString());
buffer.Clear();
}
}
private static SKBitmap Resize(SKBitmap input, int targetWidth, int targetHeight)
{
if (input.Width < targetWidth && input.Height < targetHeight)
return input;
var inputSize = new SKRect(0, 0, input.Width, input.Height);
(int renderWidth, int renderHeight) = CalcNewSize(inputSize,
targetWidth,
targetHeight);
return input.Resize(new SKImageInfo(renderWidth, renderHeight),
SKFilterQuality.High);
}
private static (int renderWidth, int renderHeight) CalcNewSize(SKRect inputSize,
int maxwidth,
int maxHeight)
{
float scale = 1.0f;
if (inputSize.Width > maxwidth || inputSize.Height > maxHeight)
{
scale = maxwidth / inputSize.Width;
}
return (renderWidth: (int)(inputSize.Width * scale),
renderHeight: maxHeight);
}
}
}
A kódban a Resize metódus végzi a képek átméretezését, ami segítségül hívja a CalcNewSize metódust, ami kiszámolja a kép új méretét. A kép új mérete a konzol ablak aktuális méretéből kerül megállapításra. Itt némi turpisság van a rendszer működésében, mivel a kép magassága nem aránytartóan kerül megállapításra, hogy a konzol teljesen kitöltésre kerüljön.
A Skia fő osztálya az SKBitmap, aminek a GetPixel metódusa hozzáférést biztosít a Kép tartalmához. Ez a picit félrevezető nevű VT100Encode metódusban kerül felhasználásra. A metódus neve azért félrevezető, mert nem csak kódol, hanem a megjelenítést is ez a kódrészlet végzi. Ezen a metóduson belül a buffer.AppendFormat felelős a varázslatért a furcsa formátumkóddal. A \x1b a VT escape kódja. Ez jelzi a terminálnak azt, hogy formátum fog következni. A [48;2; rész jelzi, hogy a karakter háttér színét módosítjuk, majd a {0};{1};{2} rész az RGB adatot határozza meg. A kód végén az m a formázás lezárásáért felel és az utolsó szóköz pedig a ténylegesen megjelenítésre kerülő karakter.
Talán még érdekesség a program fő metódusában a if (OperatingSystem.IsWindows()) utasítás, ami a multiplatform kompatibilitás biztosításáért felelős. Ez azért kell, mert csak Windows alatt, a Console osztály által támogatott az, hogy a WindowWidth és WindowHeight tulajdonságokat írjuk. Linux és egyéb Unix terminálok esetén ez is kivitelezhető, de ehhez is VT kódokat kell használnunk.
Továbbfejlesztési ötlet
A kód afféle Proof of Concept megoldás. Egy lehetséges továbbfejlesztési ötlet az lehetne, hogy a kép magasságát a konzol magasságának a duplájára méreteznénk és egy fél négyzet magas UTF kóddal megjeleníteni a pixelt úgy, hogy közben a háttérszínt is állítanánk. Ennek segítségével megduplázható lenne a vertikális felbontás 🙂
