mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
Merge pull request #3734 from tznind/sixel-encoder-tinkering
Fixes #1265 - Adds Sixel rendering support
This commit is contained in:
@@ -26,4 +26,10 @@ public static partial class Application // Driver abstractions
|
||||
/// </remarks>
|
||||
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
|
||||
public static string ForceDriver { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Collection of sixel images to write out to screen when updating.
|
||||
/// Only add to this collection if you are sure terminal supports sixel format.
|
||||
/// </summary>
|
||||
public static List<SixelToRender> Sixel = new List<SixelToRender> ();
|
||||
}
|
||||
|
||||
@@ -420,6 +420,13 @@ internal class CursesDriver : ConsoleDriver
|
||||
}
|
||||
}
|
||||
|
||||
// SIXELS
|
||||
foreach (var s in Application.Sixel)
|
||||
{
|
||||
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
|
||||
Console.Write(s.SixelData);
|
||||
}
|
||||
|
||||
SetCursorPosition (0, 0);
|
||||
|
||||
_currentCursorVisibility = savedVisibility;
|
||||
|
||||
@@ -1356,6 +1356,19 @@ public static class EscSeqUtils
|
||||
/// </summary>
|
||||
public const string CSI_ReportDeviceAttributes_Terminator = "c";
|
||||
|
||||
/*
|
||||
TODO: depends on https://github.com/gui-cs/Terminal.Gui/pull/3768
|
||||
/// <summary>
|
||||
/// CSI 16 t - Request sixel resolution (width and height in pixels)
|
||||
/// </summary>
|
||||
public static readonly AnsiEscapeSequenceRequest CSI_RequestSixelResolution = new () { Request = CSI + "16t", Terminator = "t" };
|
||||
|
||||
/// <summary>
|
||||
/// CSI 14 t - Request window size in pixels (width x height)
|
||||
/// </summary>
|
||||
public static readonly AnsiEscapeSequenceRequest CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" };
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// CSI 1 8 t | yes | yes | yes | report window size in chars
|
||||
/// https://terminalguide.namepad.de/seq/csi_st-18/
|
||||
|
||||
@@ -1020,6 +1020,15 @@ internal class NetDriver : ConsoleDriver
|
||||
SetCursorPosition (lastCol, row);
|
||||
Console.Write (output);
|
||||
}
|
||||
|
||||
foreach (var s in Application.Sixel)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace (s.SixelData))
|
||||
{
|
||||
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
|
||||
Console.Write (s.SixelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetCursorPosition (0, 0);
|
||||
@@ -1126,9 +1135,10 @@ internal class NetDriver : ConsoleDriver
|
||||
_mainLoopDriver = new NetMainLoop (this);
|
||||
_mainLoopDriver.ProcessInput = ProcessInput;
|
||||
|
||||
|
||||
return new MainLoop (_mainLoopDriver);
|
||||
}
|
||||
|
||||
|
||||
private void ProcessInput (InputResult inputEvent)
|
||||
{
|
||||
switch (inputEvent.EventType)
|
||||
|
||||
@@ -37,6 +37,7 @@ internal class WindowsConsole
|
||||
private CursorVisibility? _currentCursorVisibility;
|
||||
private CursorVisibility? _pendingCursorVisibility;
|
||||
private readonly StringBuilder _stringBuilder = new (256 * 1024);
|
||||
private string _lastWrite = string.Empty;
|
||||
|
||||
public WindowsConsole ()
|
||||
{
|
||||
@@ -118,7 +119,21 @@ internal class WindowsConsole
|
||||
|
||||
var s = _stringBuilder.ToString ();
|
||||
|
||||
result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
|
||||
// TODO: requires extensive testing if we go down this route
|
||||
// If console output has changed
|
||||
if (s != _lastWrite)
|
||||
{
|
||||
// supply console with the new content
|
||||
result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
|
||||
}
|
||||
|
||||
_lastWrite = s;
|
||||
|
||||
foreach (var sixel in Application.Sixel)
|
||||
{
|
||||
SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
|
||||
WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result)
|
||||
|
||||
20
Terminal.Gui/Drawing/AssumeSupportDetector.cs
Normal file
20
Terminal.Gui/Drawing/AssumeSupportDetector.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISixelSupportDetector"/> that assumes best
|
||||
/// case scenario (full support including transparency with 10x20 resolution).
|
||||
/// </summary>
|
||||
public class AssumeSupportDetector : ISixelSupportDetector
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public SixelSupportResult Detect ()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
IsSupported = true,
|
||||
MaxPaletteColors = 256,
|
||||
Resolution = new (10, 20),
|
||||
SupportsTransparency = true
|
||||
};
|
||||
}
|
||||
}
|
||||
15
Terminal.Gui/Drawing/ISixelSupportDetector.cs
Normal file
15
Terminal.Gui/Drawing/ISixelSupportDetector.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for detecting sixel support. Either through
|
||||
/// ansi requests to terminal or config file etc.
|
||||
/// </summary>
|
||||
public interface ISixelSupportDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the supported sixel state e.g. by sending Ansi escape sequences
|
||||
/// or from a config file etc.
|
||||
/// </summary>
|
||||
/// <returns>Description of sixel support.</returns>
|
||||
public SixelSupportResult Detect ();
|
||||
}
|
||||
91
Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
Normal file
91
Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Translates colors in an image into a Palette of up to <see cref="MaxColors"/> colors (typically 256).
|
||||
/// </summary>
|
||||
public class ColorQuantizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current colors in the palette based on the last call to
|
||||
/// <see cref="BuildPalette"/>.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Color> Palette { get; private set; } = new List<Color> ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of colors to put into the <see cref="Palette"/>.
|
||||
/// Defaults to 256 (the maximum for sixel images).
|
||||
/// </summary>
|
||||
public int MaxColors { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the algorithm used to map novel colors into existing
|
||||
/// palette colors (closest match). Defaults to <see cref="EuclideanColorDistance"/>
|
||||
/// </summary>
|
||||
public IColorDistance DistanceAlgorithm { get; set; } = new EuclideanColorDistance ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the algorithm used to build the <see cref="Palette"/>.
|
||||
/// </summary>
|
||||
public IPaletteBuilder PaletteBuildingAlgorithm { get; set; } = new PopularityPaletteWithThreshold (new EuclideanColorDistance (), 8);
|
||||
|
||||
private readonly ConcurrentDictionary<Color, int> _nearestColorCache = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="Palette"/> of colors that most represent the colors used in <paramref name="pixels"/> image.
|
||||
/// This is based on the currently configured <see cref="PaletteBuildingAlgorithm"/>.
|
||||
/// </summary>
|
||||
/// <param name="pixels"></param>
|
||||
public void BuildPalette (Color [,] pixels)
|
||||
{
|
||||
List<Color> allColors = new ();
|
||||
int width = pixels.GetLength (0);
|
||||
int height = pixels.GetLength (1);
|
||||
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
allColors.Add (pixels [x, y]);
|
||||
}
|
||||
}
|
||||
|
||||
_nearestColorCache.Clear ();
|
||||
Palette = PaletteBuildingAlgorithm.BuildPalette (allColors, MaxColors);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the closest color in <see cref="Palette"/> that matches <paramref name="toTranslate"/>
|
||||
/// based on the color comparison algorithm defined by <see cref="DistanceAlgorithm"/>
|
||||
/// </summary>
|
||||
/// <param name="toTranslate"></param>
|
||||
/// <returns></returns>
|
||||
public int GetNearestColor (Color toTranslate)
|
||||
{
|
||||
if (_nearestColorCache.TryGetValue (toTranslate, out int cachedAnswer))
|
||||
{
|
||||
return cachedAnswer;
|
||||
}
|
||||
|
||||
// Simple nearest color matching based on DistanceAlgorithm
|
||||
var minDistance = double.MaxValue;
|
||||
var nearestIndex = 0;
|
||||
|
||||
for (var index = 0; index < Palette.Count; index++)
|
||||
{
|
||||
Color color = Palette.ElementAt (index);
|
||||
double distance = DistanceAlgorithm.CalculateDistance (color, toTranslate);
|
||||
|
||||
if (distance < minDistance)
|
||||
{
|
||||
minDistance = distance;
|
||||
nearestIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
_nearestColorCache.TryAdd (toTranslate, nearestIndex);
|
||||
|
||||
return nearestIndex;
|
||||
}
|
||||
}
|
||||
31
Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs
Normal file
31
Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Calculates the distance between two colors using Euclidean distance in 3D RGB space.
|
||||
/// This measures the straight-line distance between the two points representing the colors.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Euclidean distance in RGB space is calculated as:
|
||||
/// </para>
|
||||
/// <code>
|
||||
/// √((R2 - R1)² + (G2 - G1)² + (B2 - B1)²)
|
||||
/// </code>
|
||||
/// <remarks>Values vary from 0 to ~441.67 linearly</remarks>
|
||||
/// <remarks>
|
||||
/// This distance metric is commonly used for comparing colors in RGB space, though
|
||||
/// it doesn't account for perceptual differences in color.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
public class EuclideanColorDistance : IColorDistance
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public double CalculateDistance (Color c1, Color c2)
|
||||
{
|
||||
int rDiff = c1.R - c2.R;
|
||||
int gDiff = c1.G - c2.G;
|
||||
int bDiff = c1.B - c2.B;
|
||||
|
||||
return Math.Sqrt (rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
|
||||
}
|
||||
}
|
||||
18
Terminal.Gui/Drawing/Quant/IColorDistance.cs
Normal file
18
Terminal.Gui/Drawing/Quant/IColorDistance.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for algorithms that compute the relative distance between pairs of colors.
|
||||
/// This is used for color matching to a limited palette, such as in Sixel rendering.
|
||||
/// </summary>
|
||||
public interface IColorDistance
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes a similarity metric between two <see cref="Color"/> instances.
|
||||
/// A larger value indicates more dissimilar colors, while a smaller value indicates more similar colors.
|
||||
/// The metric is internally consistent for the given algorithm.
|
||||
/// </summary>
|
||||
/// <param name="c1">The first color.</param>
|
||||
/// <param name="c2">The second color.</param>
|
||||
/// <returns>A numeric value representing the distance between the two colors.</returns>
|
||||
double CalculateDistance (Color c1, Color c2);
|
||||
}
|
||||
19
Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
Normal file
19
Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Builds a palette of a given size for a given set of input colors.
|
||||
/// </summary>
|
||||
public interface IPaletteBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Reduce the number of <paramref name="colors"/> to <paramref name="maxColors"/> (or less)
|
||||
/// using an appropriate selection algorithm.
|
||||
/// </summary>
|
||||
/// <param name="colors">
|
||||
/// Color of every pixel in the image. Contains duplication in order
|
||||
/// to support algorithms that weigh how common a color is.
|
||||
/// </param>
|
||||
/// <param name="maxColors">The maximum number of colours that should be represented.</param>
|
||||
/// <returns></returns>
|
||||
List<Color> BuildPalette (List<Color> colors, int maxColors);
|
||||
}
|
||||
112
Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs
Normal file
112
Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using Terminal.Gui;
|
||||
using Color = Terminal.Gui.Color;
|
||||
|
||||
/// <summary>
|
||||
/// Simple fast palette building algorithm which uses the frequency that a color is seen
|
||||
/// to determine whether it will appear in the final palette. Includes a threshold where
|
||||
/// by colors will be considered 'the same'. This reduces the chance of under represented
|
||||
/// colors being missed completely.
|
||||
/// </summary>
|
||||
public class PopularityPaletteWithThreshold : IPaletteBuilder
|
||||
{
|
||||
private readonly IColorDistance _colorDistance;
|
||||
private readonly double _mergeThreshold;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance with the given color grouping parameters.
|
||||
/// </summary>
|
||||
/// <param name="colorDistance">Determines which different colors can be considered the same.</param>
|
||||
/// <param name="mergeThreshold">Threshold for merging two colors together.</param>
|
||||
public PopularityPaletteWithThreshold (IColorDistance colorDistance, double mergeThreshold)
|
||||
{
|
||||
_colorDistance = colorDistance;
|
||||
_mergeThreshold = mergeThreshold; // Set the threshold for merging similar colors
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<Color> BuildPalette (List<Color> colors, int maxColors)
|
||||
{
|
||||
if (colors == null || colors.Count == 0 || maxColors <= 0)
|
||||
{
|
||||
return new ();
|
||||
}
|
||||
|
||||
// Step 1: Build the histogram of colors (count occurrences)
|
||||
Dictionary<Color, int> colorHistogram = new ();
|
||||
|
||||
foreach (Color color in colors)
|
||||
{
|
||||
if (colorHistogram.ContainsKey (color))
|
||||
{
|
||||
colorHistogram [color]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
colorHistogram [color] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If we already have fewer or equal colors than the limit, no need to merge
|
||||
if (colorHistogram.Count <= maxColors)
|
||||
{
|
||||
return colorHistogram.Keys.ToList ();
|
||||
}
|
||||
|
||||
// Step 2: Merge similar colors using the color distance threshold
|
||||
Dictionary<Color, int> mergedHistogram = MergeSimilarColors (colorHistogram, maxColors);
|
||||
|
||||
// Step 3: Sort the histogram by frequency (most frequent colors first)
|
||||
List<Color> sortedColors = mergedHistogram.OrderByDescending (c => c.Value)
|
||||
.Take (maxColors) // Keep only the top `maxColors` colors
|
||||
.Select (c => c.Key)
|
||||
.ToList ();
|
||||
|
||||
return sortedColors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merge colors in the histogram if they are within the threshold distance
|
||||
/// </summary>
|
||||
/// <param name="colorHistogram"></param>
|
||||
/// <param name="maxColors"></param>
|
||||
/// <returns></returns>
|
||||
private Dictionary<Color, int> MergeSimilarColors (Dictionary<Color, int> colorHistogram, int maxColors)
|
||||
{
|
||||
Dictionary<Color, int> mergedHistogram = new ();
|
||||
|
||||
foreach (KeyValuePair<Color, int> entry in colorHistogram)
|
||||
{
|
||||
Color currentColor = entry.Key;
|
||||
var merged = false;
|
||||
|
||||
// Try to merge the current color with an existing entry in the merged histogram
|
||||
foreach (Color mergedEntry in mergedHistogram.Keys.ToList ())
|
||||
{
|
||||
double distance = _colorDistance.CalculateDistance (currentColor, mergedEntry);
|
||||
|
||||
// If the colors are similar enough (within the threshold), merge them
|
||||
if (distance <= _mergeThreshold)
|
||||
{
|
||||
mergedHistogram [mergedEntry] += entry.Value; // Add the color frequency to the existing one
|
||||
merged = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no similar color is found, add the current color as a new entry
|
||||
if (!merged)
|
||||
{
|
||||
mergedHistogram [currentColor] = entry.Value;
|
||||
}
|
||||
|
||||
// Early exit if we've reduced the colors to the maxColors limit
|
||||
if (mergedHistogram.Count >= maxColors)
|
||||
{
|
||||
return mergedHistogram;
|
||||
}
|
||||
}
|
||||
|
||||
return mergedHistogram;
|
||||
}
|
||||
}
|
||||
252
Terminal.Gui/Drawing/SixelEncoder.cs
Normal file
252
Terminal.Gui/Drawing/SixelEncoder.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
// This code is based on existing implementations of sixel algorithm in MIT licensed open source libraries
|
||||
// node-sixel (Typescript) - https://github.com/jerch/node-sixel/tree/master/src
|
||||
// Copyright (c) 2019, Joerg Breitbart @license MIT
|
||||
// libsixel (C/C++) - https://github.com/saitoha/libsixel
|
||||
// Copyright (c) 2014-2016 Hayaki Saito @license MIT
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a images into the sixel console image output format.
|
||||
/// </summary>
|
||||
public class SixelEncoder
|
||||
{
|
||||
/*
|
||||
|
||||
A sixel is a column of 6 pixels - with a width of 1 pixel
|
||||
|
||||
Column controlled by one sixel character:
|
||||
[ ] - Bit 0 (top-most pixel)
|
||||
[ ] - Bit 1
|
||||
[ ] - Bit 2
|
||||
[ ] - Bit 3
|
||||
[ ] - Bit 4
|
||||
[ ] - Bit 5 (bottom-most pixel)
|
||||
|
||||
Special Characters
|
||||
The '-' acts like '\n'. It moves the drawing cursor
|
||||
to beginning of next line
|
||||
|
||||
The '$' acts like the <Home> key. It moves drawing
|
||||
cursor back to beginning of the current line
|
||||
e.g. to draw more color layers.
|
||||
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the quantizer responsible for building a representative
|
||||
/// limited color palette for images and for mapping novel colors in
|
||||
/// images to their closest palette color
|
||||
/// </summary>
|
||||
public ColorQuantizer Quantizer { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Encode the given bitmap into sixel encoding
|
||||
/// </summary>
|
||||
/// <param name="pixels"></param>
|
||||
/// <returns></returns>
|
||||
public string EncodeSixel (Color [,] pixels)
|
||||
{
|
||||
const string start = "\u001bP"; // Start sixel sequence
|
||||
|
||||
string defaultRatios = AnyHasAlphaOfZero (pixels) ? "0;1;0" : "0;0;0"; // Defaults for aspect ratio and grid size
|
||||
const string completeStartSequence = "q"; // Signals beginning of sixel image data
|
||||
const string noScaling = "\"1;1;"; // no scaling factors (1x1);
|
||||
|
||||
string fillArea = GetFillArea (pixels);
|
||||
|
||||
string pallette = GetColorPalette (pixels);
|
||||
|
||||
string pixelData = WriteSixel (pixels);
|
||||
|
||||
const string terminator = "\u001b\\"; // End sixel sequence
|
||||
|
||||
return start + defaultRatios + completeStartSequence + noScaling + fillArea + pallette + pixelData + terminator;
|
||||
}
|
||||
|
||||
private string WriteSixel (Color [,] pixels)
|
||||
{
|
||||
var sb = new StringBuilder ();
|
||||
int height = pixels.GetLength (1);
|
||||
int width = pixels.GetLength (0);
|
||||
|
||||
// Iterate over each 'row' of the image. Because each sixel write operation
|
||||
// outputs a screen area 6 pixels high (and 1+ across) we must process the image
|
||||
// 6 'y' units at once (1 band)
|
||||
for (var y = 0; y < height; y += 6)
|
||||
{
|
||||
sb.Append (ProcessBand (pixels, y, Math.Min (6, height - y), width));
|
||||
|
||||
// Line separator between bands
|
||||
if (y + 6 < height) // Only add separator if not the last band
|
||||
{
|
||||
// This completes the drawing of the current line of sixel and
|
||||
// returns the 'cursor' to beginning next line, newly drawn sixel
|
||||
// after this will draw in the next 6 pixel high band (i.e. below).
|
||||
sb.Append ("-");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString ();
|
||||
}
|
||||
|
||||
private string ProcessBand (Color [,] pixels, int startY, int bandHeight, int width)
|
||||
{
|
||||
var last = new sbyte [Quantizer.Palette.Count + 1];
|
||||
var code = new byte [Quantizer.Palette.Count + 1];
|
||||
var accu = new ushort [Quantizer.Palette.Count + 1];
|
||||
var slots = new short [Quantizer.Palette.Count + 1];
|
||||
|
||||
Array.Fill (last, (sbyte)-1);
|
||||
Array.Fill (accu, (ushort)1);
|
||||
Array.Fill (slots, (short)-1);
|
||||
|
||||
List<int> usedColorIdx = new List<int> ();
|
||||
List<List<string>> targets = new List<List<string>> ();
|
||||
|
||||
// Process columns within the band
|
||||
for (var x = 0; x < width; ++x)
|
||||
{
|
||||
Array.Clear (code, 0, usedColorIdx.Count);
|
||||
|
||||
// Process each row in the 6-pixel high band
|
||||
for (var row = 0; row < bandHeight; ++row)
|
||||
{
|
||||
Color color = pixels [x, startY + row];
|
||||
|
||||
int colorIndex = Quantizer.GetNearestColor (color);
|
||||
|
||||
if (color.A == 0) // Skip fully transparent pixels
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (slots [colorIndex] == -1)
|
||||
{
|
||||
targets.Add (new ());
|
||||
|
||||
if (x > 0)
|
||||
{
|
||||
last [usedColorIdx.Count] = 0;
|
||||
accu [usedColorIdx.Count] = (ushort)x;
|
||||
}
|
||||
|
||||
slots [colorIndex] = (short)usedColorIdx.Count;
|
||||
usedColorIdx.Add (colorIndex);
|
||||
}
|
||||
|
||||
code [slots [colorIndex]] |= (byte)(1 << row); // Accumulate SIXEL data
|
||||
}
|
||||
|
||||
// Handle transitions between columns
|
||||
for (var j = 0; j < usedColorIdx.Count; ++j)
|
||||
{
|
||||
if (code [j] == last [j])
|
||||
{
|
||||
accu [j]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (last [j] != -1)
|
||||
{
|
||||
targets [j].Add (CodeToSixel (last [j], accu [j]));
|
||||
}
|
||||
|
||||
last [j] = (sbyte)code [j];
|
||||
accu [j] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining data for this band
|
||||
for (var j = 0; j < usedColorIdx.Count; ++j)
|
||||
{
|
||||
if (last [j] != 0)
|
||||
{
|
||||
targets [j].Add (CodeToSixel (last [j], accu [j]));
|
||||
}
|
||||
}
|
||||
|
||||
// Build the final output for this band
|
||||
var result = new StringBuilder ();
|
||||
|
||||
for (var j = 0; j < usedColorIdx.Count; ++j)
|
||||
{
|
||||
result.Append ($"#{usedColorIdx [j]}{string.Join ("", targets [j])}$");
|
||||
}
|
||||
|
||||
return result.ToString ();
|
||||
}
|
||||
|
||||
private static string CodeToSixel (int code, int repeat)
|
||||
{
|
||||
var c = (char)(code + 63);
|
||||
|
||||
if (repeat > 3)
|
||||
{
|
||||
return "!" + repeat + c;
|
||||
}
|
||||
|
||||
if (repeat == 3)
|
||||
{
|
||||
return c.ToString () + c + c;
|
||||
}
|
||||
|
||||
if (repeat == 2)
|
||||
{
|
||||
return c.ToString () + c;
|
||||
}
|
||||
|
||||
return c.ToString ();
|
||||
}
|
||||
|
||||
private string GetColorPalette (Color [,] pixels)
|
||||
{
|
||||
Quantizer.BuildPalette (pixels);
|
||||
|
||||
var paletteSb = new StringBuilder ();
|
||||
|
||||
for (var i = 0; i < Quantizer.Palette.Count; i++)
|
||||
{
|
||||
Color color = Quantizer.Palette.ElementAt (i);
|
||||
|
||||
paletteSb.AppendFormat (
|
||||
"#{0};2;{1};{2};{3}",
|
||||
i,
|
||||
color.R * 100 / 255,
|
||||
color.G * 100 / 255,
|
||||
color.B * 100 / 255);
|
||||
}
|
||||
|
||||
return paletteSb.ToString ();
|
||||
}
|
||||
|
||||
private string GetFillArea (Color [,] pixels)
|
||||
{
|
||||
int widthInChars = pixels.GetLength (0);
|
||||
int heightInChars = pixels.GetLength (1);
|
||||
|
||||
return $"{widthInChars};{heightInChars}";
|
||||
}
|
||||
|
||||
private bool AnyHasAlphaOfZero (Color [,] pixels)
|
||||
{
|
||||
int width = pixels.GetLength (0);
|
||||
int height = pixels.GetLength (1);
|
||||
|
||||
// Loop through each pixel in the 2D array
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
// Check if the alpha component (A) is 0
|
||||
if (pixels [x, y].A == 0)
|
||||
{
|
||||
return true; // Found a pixel with A of 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false; // No pixel with A of 0 was found
|
||||
}
|
||||
}
|
||||
133
Terminal.Gui/Drawing/SixelSupportDetector.cs
Normal file
133
Terminal.Gui/Drawing/SixelSupportDetector.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
/* TODO : Depends on https://github.com/gui-cs/Terminal.Gui/pull/3768
|
||||
/// <summary>
|
||||
/// Uses Ansi escape sequences to detect whether sixel is supported
|
||||
/// by the terminal.
|
||||
/// </summary>
|
||||
public class SixelSupportDetector : ISixelSupportDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends Ansi escape sequences to the console to determine whether
|
||||
/// sixel is supported (and <see cref="SixelSupportResult.Resolution"/>
|
||||
/// etc).
|
||||
/// </summary>
|
||||
/// <returns>Description of sixel support, may include assumptions where
|
||||
/// expected response codes are not returned by console.</returns>
|
||||
public SixelSupportResult Detect ()
|
||||
{
|
||||
var result = new SixelSupportResult ();
|
||||
|
||||
result.IsSupported = IsSixelSupportedByDar ();
|
||||
|
||||
if (result.IsSupported)
|
||||
{
|
||||
if (TryGetResolutionDirectly (out var res))
|
||||
{
|
||||
result.Resolution = res;
|
||||
}
|
||||
else if(TryComputeResolution(out res))
|
||||
{
|
||||
result.Resolution = res;
|
||||
}
|
||||
|
||||
result.SupportsTransparency = IsWindowsTerminal () || IsXtermWithTransparency ();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private bool TryGetResolutionDirectly (out Size resolution)
|
||||
{
|
||||
// Expect something like:
|
||||
//<esc>[6;20;10t
|
||||
|
||||
if (AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_RequestSixelResolution, out var response))
|
||||
{
|
||||
// Terminal supports directly responding with resolution
|
||||
var match = Regex.Match (response.Response, @"\[\d+;(\d+);(\d+)t$");
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
if (int.TryParse (match.Groups [1].Value, out var ry) &&
|
||||
int.TryParse (match.Groups [2].Value, out var rx))
|
||||
{
|
||||
resolution = new Size (rx, ry);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolution = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private bool TryComputeResolution (out Size resolution)
|
||||
{
|
||||
// Fallback to window size in pixels and characters
|
||||
if (AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_RequestWindowSizeInPixels, out var pixelSizeResponse)
|
||||
&& AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_ReportTerminalSizeInChars, out var charSizeResponse))
|
||||
{
|
||||
// Example [4;600;1200t
|
||||
var pixelMatch = Regex.Match (pixelSizeResponse.Response, @"\[\d+;(\d+);(\d+)t$");
|
||||
|
||||
// Example [8;30;120t
|
||||
var charMatch = Regex.Match (charSizeResponse.Response, @"\[\d+;(\d+);(\d+)t$");
|
||||
|
||||
if (pixelMatch.Success && charMatch.Success)
|
||||
{
|
||||
// Extract pixel dimensions
|
||||
if (int.TryParse (pixelMatch.Groups [1].Value, out var pixelHeight)
|
||||
&& int.TryParse (pixelMatch.Groups [2].Value, out var pixelWidth)
|
||||
&&
|
||||
|
||||
// Extract character dimensions
|
||||
int.TryParse (charMatch.Groups [1].Value, out var charHeight)
|
||||
&& int.TryParse (charMatch.Groups [2].Value, out var charWidth)
|
||||
&& charWidth != 0
|
||||
&& charHeight != 0) // Avoid divide by zero
|
||||
{
|
||||
// Calculate the character cell size in pixels
|
||||
var cellWidth = (int)Math.Round ((double)pixelWidth / charWidth);
|
||||
var cellHeight = (int)Math.Round ((double)pixelHeight / charHeight);
|
||||
|
||||
// Set the resolution based on the character cell size
|
||||
resolution = new Size (cellWidth, cellHeight);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolution = default;
|
||||
return false;
|
||||
}
|
||||
private bool IsSixelSupportedByDar ()
|
||||
{
|
||||
return AnsiEscapeSequenceRequest.TryExecuteAnsiRequest (EscSeqUtils.CSI_SendDeviceAttributes, out AnsiEscapeSequenceResponse darResponse)
|
||||
? darResponse.Response.Split (';').Contains ("4")
|
||||
: false;
|
||||
}
|
||||
|
||||
private bool IsWindowsTerminal ()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable ("WT_SESSION"));;
|
||||
}
|
||||
private bool IsXtermWithTransparency ()
|
||||
{
|
||||
// Check if running in real xterm (XTERM_VERSION is more reliable than TERM)
|
||||
var xtermVersionStr = Environment.GetEnvironmentVariable ("XTERM_VERSION");
|
||||
|
||||
// If XTERM_VERSION exists, we are in a real xterm
|
||||
if (!string.IsNullOrWhiteSpace (xtermVersionStr) && int.TryParse (xtermVersionStr, out var xtermVersion) && xtermVersion >= 370)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}*/
|
||||
33
Terminal.Gui/Drawing/SixelSupportResult.cs
Normal file
33
Terminal.Gui/Drawing/SixelSupportResult.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the discovered state of sixel support and ancillary information
|
||||
/// e.g. <see cref="Resolution"/>. You can use any <see cref="ISixelSupportDetector"/>
|
||||
/// to discover this information.
|
||||
/// </summary>
|
||||
public class SixelSupportResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the terminal supports sixel graphic format.
|
||||
/// Defaults to false.
|
||||
/// </summary>
|
||||
public bool IsSupported { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of pixels of sixel that corresponds to each Col (<see cref="Size.Width"/>)
|
||||
/// and each Row (<see cref="Size.Height"/>. Defaults to 10x20.
|
||||
/// </summary>
|
||||
public Size Resolution { get; set; } = new (10, 20);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum number of colors that can be included in a sixel image. Defaults
|
||||
/// to 256.
|
||||
/// </summary>
|
||||
public int MaxPaletteColors { get; set; } = 256;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the terminal supports transparent background sixels.
|
||||
/// Defaults to false
|
||||
/// </summary>
|
||||
public bool SupportsTransparency { get; set; }
|
||||
}
|
||||
19
Terminal.Gui/Drawing/SixelToRender.cs
Normal file
19
Terminal.Gui/Drawing/SixelToRender.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a request to render a given <see cref="SixelData"/> at a given <see cref="ScreenPosition"/>.
|
||||
/// Requires that the terminal and <see cref="ConsoleDriver"/> both support sixel.
|
||||
/// </summary>
|
||||
public class SixelToRender
|
||||
{
|
||||
/// <summary>
|
||||
/// gets or sets the encoded sixel data. Use <see cref="SixelEncoder"/> to convert bitmaps
|
||||
/// into encoded sixel data.
|
||||
/// </summary>
|
||||
public string SixelData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// gets or sets where to move the cursor to before outputting the <see cref="SixelData"/>.
|
||||
/// </summary>
|
||||
public Point ScreenPosition { get; set; }
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
118
UnitTests/Drawing/PopularityPaletteWithThresholdTests.cs
Normal file
118
UnitTests/Drawing/PopularityPaletteWithThresholdTests.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
namespace Terminal.Gui.DrawingTests;
|
||||
|
||||
public class PopularityPaletteWithThresholdTests
|
||||
{
|
||||
private readonly IColorDistance _colorDistance;
|
||||
|
||||
public PopularityPaletteWithThresholdTests () { _colorDistance = new EuclideanColorDistance (); }
|
||||
|
||||
[Fact]
|
||||
public void BuildPalette_EmptyColorList_ReturnsEmptyPalette ()
|
||||
{
|
||||
// Arrange
|
||||
var paletteBuilder = new PopularityPaletteWithThreshold (_colorDistance, 50);
|
||||
List<Color> colors = new ();
|
||||
|
||||
// Act
|
||||
List<Color> result = paletteBuilder.BuildPalette (colors, 256);
|
||||
|
||||
// Assert
|
||||
Assert.Empty (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPalette_MaxColorsZero_ReturnsEmptyPalette ()
|
||||
{
|
||||
// Arrange
|
||||
var paletteBuilder = new PopularityPaletteWithThreshold (_colorDistance, 50);
|
||||
List<Color> colors = new () { new (255, 0), new (0, 255) };
|
||||
|
||||
// Act
|
||||
List<Color> result = paletteBuilder.BuildPalette (colors, 0);
|
||||
|
||||
// Assert
|
||||
Assert.Empty (result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPalette_SingleColorList_ReturnsSingleColor ()
|
||||
{
|
||||
// Arrange
|
||||
var paletteBuilder = new PopularityPaletteWithThreshold (_colorDistance, 50);
|
||||
List<Color> colors = new () { new (255, 0), new (255, 0) };
|
||||
|
||||
// Act
|
||||
List<Color> result = paletteBuilder.BuildPalette (colors, 256);
|
||||
|
||||
// Assert
|
||||
Assert.Single (result);
|
||||
Assert.Equal (new (255, 0), result [0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPalette_ThresholdMergesSimilarColors_WhenColorCountExceedsMax ()
|
||||
{
|
||||
// Arrange
|
||||
var paletteBuilder = new PopularityPaletteWithThreshold (_colorDistance, 50); // Set merge threshold to 50
|
||||
|
||||
List<Color> colors = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (250, 0), // Very close to Red
|
||||
new (0, 255), // Green
|
||||
new (0, 250) // Very close to Green
|
||||
};
|
||||
|
||||
// Act
|
||||
List<Color> result = paletteBuilder.BuildPalette (colors, 2); // Limit palette to 2 colors
|
||||
|
||||
// Assert
|
||||
Assert.Equal (2, result.Count); // Red and Green should be merged with their close colors
|
||||
Assert.Contains (new (255, 0), result); // Red (or close to Red) should be present
|
||||
Assert.Contains (new (0, 255), result); // Green (or close to Green) should be present
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPalette_NoMergingIfColorCountIsWithinMax ()
|
||||
{
|
||||
// Arrange
|
||||
var paletteBuilder = new PopularityPaletteWithThreshold (_colorDistance, 50);
|
||||
|
||||
List<Color> colors = new ()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (0, 255) // Green
|
||||
};
|
||||
|
||||
// Act
|
||||
List<Color> result = paletteBuilder.BuildPalette (colors, 256); // Set maxColors higher than the number of unique colors
|
||||
|
||||
// Assert
|
||||
Assert.Equal (2, result.Count); // No merging should occur since we are under the limit
|
||||
Assert.Contains (new (255, 0), result);
|
||||
Assert.Contains (new (0, 255), result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildPalette_MergesUntilMaxColorsReached ()
|
||||
{
|
||||
// Arrange
|
||||
var paletteBuilder = new PopularityPaletteWithThreshold (_colorDistance, 50);
|
||||
|
||||
List<Color> colors = new()
|
||||
{
|
||||
new (255, 0), // Red
|
||||
new (254, 0), // Close to Red
|
||||
new (0, 255), // Green
|
||||
new (0, 254) // Close to Green
|
||||
};
|
||||
|
||||
// Act
|
||||
List<Color> result = paletteBuilder.BuildPalette (colors, 2); // Set maxColors to 2
|
||||
|
||||
// Assert
|
||||
Assert.Equal (2, result.Count); // Only two colors should be in the final palette
|
||||
Assert.Contains (new (255, 0), result);
|
||||
Assert.Contains (new (0, 255), result);
|
||||
}
|
||||
}
|
||||
233
UnitTests/Drawing/SixelEncoderTests.cs
Normal file
233
UnitTests/Drawing/SixelEncoderTests.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using Color = Terminal.Gui.Color;
|
||||
|
||||
namespace UnitTests.Drawing;
|
||||
|
||||
public class SixelEncoderTests
|
||||
{
|
||||
[Fact]
|
||||
public void EncodeSixel_RedSquare12x12_ReturnsExpectedSixel ()
|
||||
{
|
||||
string expected = "\u001bP" // Start sixel sequence
|
||||
+ "0;0;0" // Defaults for aspect ratio and grid size
|
||||
+ "q" // Signals beginning of sixel image data
|
||||
+ "\"1;1;12;12" // no scaling factors (1x1) and filling 12x12 pixel area
|
||||
/*
|
||||
* Definition of the color palette
|
||||
* #<index>;<type>;<R>;<G>;<B>" - 2 means RGB. The values range 0 to 100
|
||||
*/
|
||||
+ "#0;2;100;0;0" // Red color definition
|
||||
/*
|
||||
* Start of the Pixel data
|
||||
* We draw 6 rows at once, so end up with 2 'lines'
|
||||
* Both are basically the same and terminate with dollar hyphen (except last row)
|
||||
* Format is:
|
||||
* #0 (selects to use color palette index 0 i.e. red)
|
||||
* !12 (repeat next byte 12 times i.e. the whole length of the row)
|
||||
* ~ (the byte 111111 i.e. fill completely)
|
||||
* $ (return to start of line)
|
||||
* - (move down to next line)
|
||||
*/
|
||||
+ "#0!12~$-"
|
||||
+ "#0!12~$" // Next 6 rows of red pixels
|
||||
+ "\u001b\\"; // End sixel sequence
|
||||
|
||||
// Arrange: Create a 12x12 bitmap filled with red
|
||||
Color [,] pixels = new Color [12, 12];
|
||||
|
||||
for (var x = 0; x < 12; x++)
|
||||
{
|
||||
for (var y = 0; y < 12; y++)
|
||||
{
|
||||
pixels [x, y] = new (255, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Act: Encode the image
|
||||
var encoder = new SixelEncoder (); // Assuming SixelEncoder is the class that contains the EncodeSixel method
|
||||
string result = encoder.EncodeSixel (pixels);
|
||||
|
||||
// Since image is only red we should only have 1 color definition
|
||||
Color c1 = Assert.Single (encoder.Quantizer.Palette);
|
||||
|
||||
Assert.Equal (new (255, 0, 0), c1);
|
||||
Assert.Equal (expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeSixel_12x12GridPattern3x3_ReturnsExpectedSixel ()
|
||||
{
|
||||
/*
|
||||
* Each block is a 3x3 square, alternating black and white.
|
||||
* The pattern alternates between rows, creating a checkerboard.
|
||||
* We have 4 blocks per row, and this repeats over 12x12 pixels.
|
||||
*
|
||||
* ███...███...
|
||||
* ███...███...
|
||||
* ███...███...
|
||||
* ...███...███
|
||||
* ...███...███
|
||||
* ...███...███
|
||||
* ███...███...
|
||||
* ███...███...
|
||||
* ███...███...
|
||||
* ...███...███
|
||||
* ...███...███
|
||||
* ...███...███
|
||||
*
|
||||
* Because we are dealing with sixels (drawing 6 rows at once), we will
|
||||
* see 2 bands being drawn. We will also see how we have to 'go back over'
|
||||
* the current line after drawing the black (so we can draw the white).
|
||||
*/
|
||||
|
||||
string expected = "\u001bP" // Start sixel sequence
|
||||
+ "0;0;0" // Defaults for aspect ratio and grid size
|
||||
+ "q" // Signals beginning of sixel image data
|
||||
+ "\"1;1;12;12" // no scaling factors (1x1) and filling 12x12 pixel area
|
||||
/*
|
||||
* Definition of the color palette
|
||||
*/
|
||||
+ "#0;2;0;0;0" // Black color definition (index 0: RGB 0,0,0)
|
||||
+ "#1;2;100;100;100" // White color definition (index 1: RGB 100,100,100)
|
||||
/*
|
||||
* Start of the Pixel data
|
||||
*
|
||||
* Lets consider only the first 6 pixel (vertically). We have to fill the top 3 black and bottom 3 white.
|
||||
* So we need to select black and fill 000111. To convert this into a character we must +63 and convert to ASCII.
|
||||
* Later on we will also need to select white and fill the inverse, i.e. 111000.
|
||||
*
|
||||
* 111000 (binary) → w (ASCII 119).
|
||||
* 000111 (binary) → F (ASCII 70).
|
||||
*
|
||||
* Therefore the lines become
|
||||
*
|
||||
* #0 (Select black)
|
||||
* FFF (fill first 3 pixels horizontally - and top half of band black)
|
||||
* www (fill next 3 pixels horizontally - bottom half of band black)
|
||||
* FFFwww (as above to finish the line)
|
||||
*
|
||||
* Next we must go back and fill the white (on the same band)
|
||||
* #1 (Select white)
|
||||
*/
|
||||
+ "#0FFFwwwFFFwww$" // First pass of top band (Filling black)
|
||||
+ "#1wwwFFFwwwFFF$-" // Second pass of top band (Filling white)
|
||||
// Sequence repeats exactly the same because top band is actually identical pixels to bottom band
|
||||
+ "#0FFFwwwFFFwww$" // First pass of bottom band (Filling black)
|
||||
+ "#1wwwFFFwwwFFF$" // Second pass of bottom band (Filling white)
|
||||
+ "\u001b\\"; // End sixel sequence
|
||||
|
||||
// Arrange: Create a 12x12 bitmap with a 3x3 checkerboard pattern
|
||||
Color [,] pixels = new Color [12, 12];
|
||||
|
||||
for (var y = 0; y < 12; y++)
|
||||
{
|
||||
for (var x = 0; x < 12; x++)
|
||||
{
|
||||
// Create a 3x3 checkerboard by alternating the color based on pixel coordinates
|
||||
if ((x / 3 + y / 3) % 2 == 0)
|
||||
{
|
||||
pixels [x, y] = new (0, 0, 0); // Black
|
||||
}
|
||||
else
|
||||
{
|
||||
pixels [x, y] = new (255, 255, 255); // White
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Act: Encode the image
|
||||
var encoder = new SixelEncoder (); // Assuming SixelEncoder is the class that contains the EncodeSixel method
|
||||
string result = encoder.EncodeSixel (pixels);
|
||||
|
||||
// We should have only black and white in the palette
|
||||
Assert.Equal (2, encoder.Quantizer.Palette.Count);
|
||||
Color black = encoder.Quantizer.Palette.ElementAt (0);
|
||||
Color white = encoder.Quantizer.Palette.ElementAt (1);
|
||||
|
||||
Assert.Equal (new (0, 0, 0), black);
|
||||
Assert.Equal (new (255, 255, 255), white);
|
||||
|
||||
// Compare the generated SIXEL string with the expected one
|
||||
Assert.Equal (expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeSixel_Transparent12x12_ReturnsExpectedSixel ()
|
||||
{
|
||||
string expected = "\u001bP" // Start sixel sequence
|
||||
+ "0;1;0" // Defaults for aspect ratio and grid size
|
||||
+ "q" // Signals beginning of sixel image data
|
||||
+ "\"1;1;12;12" // no scaling factors (1x1) and filling 12x12 pixel area
|
||||
+ "#0;2;0;0;0" // Black transparent (TODO: Shouldn't really be output this if it is transparent)
|
||||
// Since all pixels are transparent we don't output any colors at all, so its just newline
|
||||
+ "-" // Nothing on first or second lines
|
||||
+ "\u001b\\"; // End sixel sequence
|
||||
|
||||
// Arrange: Create a 12x12 bitmap filled with fully transparent pixels
|
||||
Color [,] pixels = new Color [12, 12];
|
||||
|
||||
for (var x = 0; x < 12; x++)
|
||||
{
|
||||
for (var y = 0; y < 12; y++)
|
||||
{
|
||||
pixels [x, y] = new (0, 0, 0, 0); // Fully transparent
|
||||
}
|
||||
}
|
||||
|
||||
// Act: Encode the image
|
||||
var encoder = new SixelEncoder ();
|
||||
string result = encoder.EncodeSixel (pixels);
|
||||
|
||||
// Assert: Expect the result to be fully transparent encoded output
|
||||
Assert.Equal (expected, result);
|
||||
}
|
||||
[Fact]
|
||||
public void EncodeSixel_VerticalMix_TransparentAndColor_ReturnsExpectedSixel ()
|
||||
{
|
||||
string expected = "\u001bP" // Start sixel sequence
|
||||
+ "0;1;0" // Defaults for aspect ratio and grid size (1 indicates support for transparent pixels)
|
||||
+ "q" // Signals beginning of sixel image data
|
||||
+ "\"1;1;12;12" // No scaling factors (1x1) and filling 12x12 pixel area
|
||||
/*
|
||||
* Define the color palette:
|
||||
* We'll use one color (Red) for the colored pixels.
|
||||
*/
|
||||
+ "#0;2;100;0;0" // Red color definition (index 0: RGB 100,0,0)
|
||||
+ "#1;2;0;0;0" // Black transparent (TODO: Shouldn't really be output this if it is transparent)
|
||||
/*
|
||||
* Start of the Pixel data
|
||||
* We have alternating transparent (0) and colored (red) pixels in a vertical band.
|
||||
* The pattern for each sixel byte is 101010, which in binary (+63) converts to ASCII character 'T'.
|
||||
* Since we have 12 pixels horizontally, we'll see this pattern repeat across the row so we see
|
||||
* the 'sequence repeat' 12 times i.e. !12 (do the next letter 'T' 12 times).
|
||||
*/
|
||||
+ "#0!12T$-" // First band of alternating red and transparent pixels
|
||||
+ "#0!12T$" // Second band, same alternating red and transparent pixels
|
||||
+ "\u001b\\"; // End sixel sequence
|
||||
|
||||
// Arrange: Create a 12x12 bitmap with alternating transparent and red pixels in a vertical band
|
||||
Color [,] pixels = new Color [12, 12];
|
||||
|
||||
for (var x = 0; x < 12; x++)
|
||||
{
|
||||
for (var y = 0; y < 12; y++)
|
||||
{
|
||||
// For simplicity, we'll make every other row transparent
|
||||
if (y % 2 == 0)
|
||||
{
|
||||
pixels [x, y] = new (255, 0, 0); // Red pixel
|
||||
}
|
||||
else
|
||||
{
|
||||
pixels [x, y] = new (0, 0, 0, 0); // Transparent pixel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Act: Encode the image
|
||||
var encoder = new SixelEncoder ();
|
||||
string result = encoder.EncodeSixel (pixels);
|
||||
|
||||
// Assert: Expect the result to match the expected sixel output
|
||||
Assert.Equal (expected, result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user