Skip to content

Commit 2f7d80a

Browse files
authored
Merge pull request #3734 from tznind/sixel-encoder-tinkering
Fixes #1265 - Adds Sixel rendering support
2 parents 7baede5 + ce41afd commit 2f7d80a

19 files changed

+2094
-57
lines changed

Terminal.Gui/Application/Application.Driver.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,10 @@ public static partial class Application // Driver abstractions
2626
/// </remarks>
2727
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
2828
public static string ForceDriver { get; set; } = string.Empty;
29+
30+
/// <summary>
31+
/// Collection of sixel images to write out to screen when updating.
32+
/// Only add to this collection if you are sure terminal supports sixel format.
33+
/// </summary>
34+
public static List<SixelToRender> Sixel = new List<SixelToRender> ();
2935
}

Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,13 @@ public override void UpdateScreen ()
420420
}
421421
}
422422

423+
// SIXELS
424+
foreach (var s in Application.Sixel)
425+
{
426+
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
427+
Console.Write(s.SixelData);
428+
}
429+
423430
SetCursorPosition (0, 0);
424431

425432
_currentCursorVisibility = savedVisibility;

Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1356,6 +1356,19 @@ public enum DECSCUSR_Style
13561356
/// </summary>
13571357
public const string CSI_ReportDeviceAttributes_Terminator = "c";
13581358

1359+
/*
1360+
TODO: depends on https://github.com/gui-cs/Terminal.Gui/pull/3768
1361+
/// <summary>
1362+
/// CSI 16 t - Request sixel resolution (width and height in pixels)
1363+
/// </summary>
1364+
public static readonly AnsiEscapeSequenceRequest CSI_RequestSixelResolution = new () { Request = CSI + "16t", Terminator = "t" };
1365+
1366+
/// <summary>
1367+
/// CSI 14 t - Request window size in pixels (width x height)
1368+
/// </summary>
1369+
public static readonly AnsiEscapeSequenceRequest CSI_RequestWindowSizeInPixels = new () { Request = CSI + "14t", Terminator = "t" };
1370+
*/
1371+
13591372
/// <summary>
13601373
/// CSI 1 8 t | yes | yes | yes | report window size in chars
13611374
/// https://terminalguide.namepad.de/seq/csi_st-18/

Terminal.Gui/ConsoleDrivers/NetDriver.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1020,6 +1020,15 @@ public override void UpdateScreen ()
10201020
SetCursorPosition (lastCol, row);
10211021
Console.Write (output);
10221022
}
1023+
1024+
foreach (var s in Application.Sixel)
1025+
{
1026+
if (!string.IsNullOrWhiteSpace (s.SixelData))
1027+
{
1028+
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
1029+
Console.Write (s.SixelData);
1030+
}
1031+
}
10231032
}
10241033

10251034
SetCursorPosition (0, 0);
@@ -1126,9 +1135,10 @@ internal override MainLoop Init ()
11261135
_mainLoopDriver = new NetMainLoop (this);
11271136
_mainLoopDriver.ProcessInput = ProcessInput;
11281137

1138+
11291139
return new MainLoop (_mainLoopDriver);
11301140
}
1131-
1141+
11321142
private void ProcessInput (InputResult inputEvent)
11331143
{
11341144
switch (inputEvent.EventType)

Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ internal class WindowsConsole
3737
private CursorVisibility? _currentCursorVisibility;
3838
private CursorVisibility? _pendingCursorVisibility;
3939
private readonly StringBuilder _stringBuilder = new (256 * 1024);
40+
private string _lastWrite = string.Empty;
4041

4142
public WindowsConsole ()
4243
{
@@ -118,7 +119,21 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord
118119

119120
var s = _stringBuilder.ToString ();
120121

121-
result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
122+
// TODO: requires extensive testing if we go down this route
123+
// If console output has changed
124+
if (s != _lastWrite)
125+
{
126+
// supply console with the new content
127+
result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero);
128+
}
129+
130+
_lastWrite = s;
131+
132+
foreach (var sixel in Application.Sixel)
133+
{
134+
SetCursorPosition (new Coord ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y));
135+
WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero);
136+
}
122137
}
123138

124139
if (!result)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Terminal.Gui;
2+
3+
/// <summary>
4+
/// Implementation of <see cref="ISixelSupportDetector"/> that assumes best
5+
/// case scenario (full support including transparency with 10x20 resolution).
6+
/// </summary>
7+
public class AssumeSupportDetector : ISixelSupportDetector
8+
{
9+
/// <inheritdoc/>
10+
public SixelSupportResult Detect ()
11+
{
12+
return new()
13+
{
14+
IsSupported = true,
15+
MaxPaletteColors = 256,
16+
Resolution = new (10, 20),
17+
SupportsTransparency = true
18+
};
19+
}
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Terminal.Gui;
2+
3+
/// <summary>
4+
/// Interface for detecting sixel support. Either through
5+
/// ansi requests to terminal or config file etc.
6+
/// </summary>
7+
public interface ISixelSupportDetector
8+
{
9+
/// <summary>
10+
/// Gets the supported sixel state e.g. by sending Ansi escape sequences
11+
/// or from a config file etc.
12+
/// </summary>
13+
/// <returns>Description of sixel support.</returns>
14+
public SixelSupportResult Detect ();
15+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System.Collections.Concurrent;
2+
3+
namespace Terminal.Gui;
4+
5+
/// <summary>
6+
/// Translates colors in an image into a Palette of up to <see cref="MaxColors"/> colors (typically 256).
7+
/// </summary>
8+
public class ColorQuantizer
9+
{
10+
/// <summary>
11+
/// Gets the current colors in the palette based on the last call to
12+
/// <see cref="BuildPalette"/>.
13+
/// </summary>
14+
public IReadOnlyCollection<Color> Palette { get; private set; } = new List<Color> ();
15+
16+
/// <summary>
17+
/// Gets or sets the maximum number of colors to put into the <see cref="Palette"/>.
18+
/// Defaults to 256 (the maximum for sixel images).
19+
/// </summary>
20+
public int MaxColors { get; set; } = 256;
21+
22+
/// <summary>
23+
/// Gets or sets the algorithm used to map novel colors into existing
24+
/// palette colors (closest match). Defaults to <see cref="EuclideanColorDistance"/>
25+
/// </summary>
26+
public IColorDistance DistanceAlgorithm { get; set; } = new EuclideanColorDistance ();
27+
28+
/// <summary>
29+
/// Gets or sets the algorithm used to build the <see cref="Palette"/>.
30+
/// </summary>
31+
public IPaletteBuilder PaletteBuildingAlgorithm { get; set; } = new PopularityPaletteWithThreshold (new EuclideanColorDistance (), 8);
32+
33+
private readonly ConcurrentDictionary<Color, int> _nearestColorCache = new ();
34+
35+
/// <summary>
36+
/// Builds a <see cref="Palette"/> of colors that most represent the colors used in <paramref name="pixels"/> image.
37+
/// This is based on the currently configured <see cref="PaletteBuildingAlgorithm"/>.
38+
/// </summary>
39+
/// <param name="pixels"></param>
40+
public void BuildPalette (Color [,] pixels)
41+
{
42+
List<Color> allColors = new ();
43+
int width = pixels.GetLength (0);
44+
int height = pixels.GetLength (1);
45+
46+
for (var x = 0; x < width; x++)
47+
{
48+
for (var y = 0; y < height; y++)
49+
{
50+
allColors.Add (pixels [x, y]);
51+
}
52+
}
53+
54+
_nearestColorCache.Clear ();
55+
Palette = PaletteBuildingAlgorithm.BuildPalette (allColors, MaxColors);
56+
}
57+
58+
/// <summary>
59+
/// Returns the closest color in <see cref="Palette"/> that matches <paramref name="toTranslate"/>
60+
/// based on the color comparison algorithm defined by <see cref="DistanceAlgorithm"/>
61+
/// </summary>
62+
/// <param name="toTranslate"></param>
63+
/// <returns></returns>
64+
public int GetNearestColor (Color toTranslate)
65+
{
66+
if (_nearestColorCache.TryGetValue (toTranslate, out int cachedAnswer))
67+
{
68+
return cachedAnswer;
69+
}
70+
71+
// Simple nearest color matching based on DistanceAlgorithm
72+
var minDistance = double.MaxValue;
73+
var nearestIndex = 0;
74+
75+
for (var index = 0; index < Palette.Count; index++)
76+
{
77+
Color color = Palette.ElementAt (index);
78+
double distance = DistanceAlgorithm.CalculateDistance (color, toTranslate);
79+
80+
if (distance < minDistance)
81+
{
82+
minDistance = distance;
83+
nearestIndex = index;
84+
}
85+
}
86+
87+
_nearestColorCache.TryAdd (toTranslate, nearestIndex);
88+
89+
return nearestIndex;
90+
}
91+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
namespace Terminal.Gui;
2+
3+
/// <summary>
4+
/// <para>
5+
/// Calculates the distance between two colors using Euclidean distance in 3D RGB space.
6+
/// This measures the straight-line distance between the two points representing the colors.
7+
/// </para>
8+
/// <para>
9+
/// Euclidean distance in RGB space is calculated as:
10+
/// </para>
11+
/// <code>
12+
/// √((R2 - R1)² + (G2 - G1)² + (B2 - B1)²)
13+
/// </code>
14+
/// <remarks>Values vary from 0 to ~441.67 linearly</remarks>
15+
/// <remarks>
16+
/// This distance metric is commonly used for comparing colors in RGB space, though
17+
/// it doesn't account for perceptual differences in color.
18+
/// </remarks>
19+
/// </summary>
20+
public class EuclideanColorDistance : IColorDistance
21+
{
22+
/// <inheritdoc/>
23+
public double CalculateDistance (Color c1, Color c2)
24+
{
25+
int rDiff = c1.R - c2.R;
26+
int gDiff = c1.G - c2.G;
27+
int bDiff = c1.B - c2.B;
28+
29+
return Math.Sqrt (rDiff * rDiff + gDiff * gDiff + bDiff * bDiff);
30+
}
31+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Terminal.Gui;
2+
3+
/// <summary>
4+
/// Interface for algorithms that compute the relative distance between pairs of colors.
5+
/// This is used for color matching to a limited palette, such as in Sixel rendering.
6+
/// </summary>
7+
public interface IColorDistance
8+
{
9+
/// <summary>
10+
/// Computes a similarity metric between two <see cref="Color"/> instances.
11+
/// A larger value indicates more dissimilar colors, while a smaller value indicates more similar colors.
12+
/// The metric is internally consistent for the given algorithm.
13+
/// </summary>
14+
/// <param name="c1">The first color.</param>
15+
/// <param name="c2">The second color.</param>
16+
/// <returns>A numeric value representing the distance between the two colors.</returns>
17+
double CalculateDistance (Color c1, Color c2);
18+
}

0 commit comments

Comments
 (0)