Skip to content

Commit

Permalink
Merge pull request #3734 from tznind/sixel-encoder-tinkering
Browse files Browse the repository at this point in the history
Fixes #1265 - Adds Sixel rendering support
  • Loading branch information
tig authored Oct 28, 2024
2 parents 7baede5 + ce41afd commit 2f7d80a
Show file tree
Hide file tree
Showing 19 changed files with 2,094 additions and 57 deletions.
6 changes: 6 additions & 0 deletions Terminal.Gui/Application/Application.Driver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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> ();
}
7 changes: 7 additions & 0 deletions Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,13 @@ public override void UpdateScreen ()
}
}

// SIXELS
foreach (var s in Application.Sixel)
{
SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y);
Console.Write(s.SixelData);
}

SetCursorPosition (0, 0);

_currentCursorVisibility = savedVisibility;
Expand Down
13 changes: 13 additions & 0 deletions Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,19 @@ public enum DECSCUSR_Style
/// </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/
Expand Down
12 changes: 11 additions & 1 deletion Terminal.Gui/ConsoleDrivers/NetDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,15 @@ public override void UpdateScreen ()
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);
Expand Down Expand Up @@ -1126,9 +1135,10 @@ internal override MainLoop Init ()
_mainLoopDriver = new NetMainLoop (this);
_mainLoopDriver.ProcessInput = ProcessInput;


return new MainLoop (_mainLoopDriver);
}

private void ProcessInput (InputResult inputEvent)
{
switch (inputEvent.EventType)
Expand Down
17 changes: 16 additions & 1 deletion Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
{
Expand Down Expand Up @@ -118,7 +119,21 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord

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)
Expand Down
20 changes: 20 additions & 0 deletions Terminal.Gui/Drawing/AssumeSupportDetector.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 15 additions & 0 deletions Terminal.Gui/Drawing/ISixelSupportDetector.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 91 additions & 0 deletions Terminal.Gui/Drawing/Quant/ColorQuantizer.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 18 additions & 0 deletions Terminal.Gui/Drawing/Quant/IColorDistance.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 2f7d80a

Please sign in to comment.