Bläddra i källkod

Merge pull request #3734 from tznind/sixel-encoder-tinkering

Fixes #1265 - Adds Sixel rendering support
Tig 9 månader sedan
förälder
incheckning
2f7d80a000

+ 6 - 0
Terminal.Gui/Application/Application.Driver.cs

@@ -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 - 0
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -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;

+ 13 - 0
Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs

@@ -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/

+ 11 - 1
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -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)

+ 16 - 1
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -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 - 0
Terminal.Gui/Drawing/AssumeSupportDetector.cs

@@ -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 - 0
Terminal.Gui/Drawing/ISixelSupportDetector.cs

@@ -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 - 0
Terminal.Gui/Drawing/Quant/ColorQuantizer.cs

@@ -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 - 0
Terminal.Gui/Drawing/Quant/EuclideanColorDistance.cs

@@ -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 - 0
Terminal.Gui/Drawing/Quant/IColorDistance.cs

@@ -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 - 0
Terminal.Gui/Drawing/Quant/IPaletteBuilder.cs

@@ -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 - 0
Terminal.Gui/Drawing/Quant/PopularityPaletteWithThreshold.cs

@@ -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 - 0
Terminal.Gui/Drawing/SixelEncoder.cs

@@ -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 - 0
Terminal.Gui/Drawing/SixelSupportDetector.cs

@@ -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 - 0
Terminal.Gui/Drawing/SixelSupportResult.cs

@@ -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 - 0
Terminal.Gui/Drawing/SixelToRender.cs

@@ -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; }
+}

+ 947 - 55
UICatalog/Scenarios/Images.cs

@@ -1,7 +1,10 @@
 using System;
 using System.Collections.Concurrent;
+using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Text;
+using ColorHelper;
 using SixLabors.ImageSharp;
 using SixLabors.ImageSharp.PixelFormats;
 using SixLabors.ImageSharp.Processing;
@@ -15,15 +18,73 @@ namespace UICatalog.Scenarios;
 [ScenarioCategory ("Drawing")]
 public class Images : Scenario
 {
+    private ImageView _imageView;
+    private Point _screenLocationForSixel;
+    private string _encodedSixelData;
+    private Window _win;
+
+    /// <summary>
+    ///     Number of sixel pixels per row of characters in the console.
+    /// </summary>
+    private NumericUpDown _pxY;
+
+    /// <summary>
+    ///     Number of sixel pixels per column of characters in the console
+    /// </summary>
+    private NumericUpDown _pxX;
+
+    /// <summary>
+    ///     View shown in sixel tab if sixel is supported
+    /// </summary>
+    private View _sixelSupported;
+
+    /// <summary>
+    ///     View shown in sixel tab if sixel is not supported
+    /// </summary>
+    private View _sixelNotSupported;
+
+    private Tab _tabSixel;
+    private TabView _tabView;
+
+    /// <summary>
+    ///     The view into which the currently opened sixel image is bounded
+    /// </summary>
+    private View _sixelView;
+
+    private DoomFire _fire;
+    private SixelEncoder _fireEncoder;
+    private SixelToRender _fireSixel;
+    private int _fireFrameCounter;
+    private bool _isDisposed;
+    private RadioGroup _rgPaletteBuilder;
+    private RadioGroup _rgDistanceAlgorithm;
+    private NumericUpDown _popularityThreshold;
+    private SixelToRender _sixelImage;
+    private SixelSupportResult _sixelSupportResult;
+
     public override void Main ()
     {
+        // TODO: Change to the one that uses Ansi Requests later
+        var sixelSupportDetector = new AssumeSupportDetector ();
+        _sixelSupportResult = sixelSupportDetector.Detect ();
+
         Application.Init ();
-        var win = new Window { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName()}" };
+        _win = new () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" };
 
         bool canTrueColor = Application.Driver?.SupportsTrueColor ?? false;
 
+        var tabBasic = new Tab
+        {
+            DisplayText = "Basic"
+        };
+
+        _tabSixel = new ()
+        {
+            DisplayText = "Sixel"
+        };
+
         var lblDriverName = new Label { X = 0, Y = 0, Text = $"Driver is {Application.Driver?.GetType ().Name}" };
-        win.Add (lblDriverName);
+        _win.Add (lblDriverName);
 
         var cbSupportsTrueColor = new CheckBox
         {
@@ -33,7 +94,36 @@ public class Images : Scenario
             CanFocus = false,
             Text = "supports true color "
         };
-        win.Add (cbSupportsTrueColor);
+        _win.Add (cbSupportsTrueColor);
+
+        var cbSupportsSixel = new CheckBox
+        {
+            X = Pos.Right (lblDriverName) + 2,
+            Y = 1,
+            CheckedState = CheckState.UnChecked,
+            Text = "Supports Sixel"
+        };
+
+        var lblSupportsSixel = new Label ()
+        {
+
+            X = Pos.Right (lblDriverName) + 2,
+            Y = Pos.Bottom (cbSupportsSixel),
+            Text = "(Check if your terminal supports Sixel)"
+        };
+
+
+/*        CheckedState = _sixelSupportResult.IsSupported
+                           ? CheckState.Checked
+                           : CheckState.UnChecked;*/
+        cbSupportsSixel.CheckedStateChanging += (s, e) =>
+                                                {
+                                                    _sixelSupportResult.IsSupported = e.NewValue == CheckState.Checked;
+                                                    SetupSixelSupported (e.NewValue == CheckState.Checked);
+                                                    ApplyShowTabViewHack ();
+                                                };
+
+        _win.Add (cbSupportsSixel);
 
         var cbUseTrueColor = new CheckBox
         {
@@ -44,81 +134,501 @@ public class Images : Scenario
             Text = "Use true color"
         };
         cbUseTrueColor.CheckedStateChanging += (_, evt) => Application.Force16Colors = evt.NewValue == CheckState.UnChecked;
-        win.Add (cbUseTrueColor);
+        _win.Add (cbUseTrueColor);
 
         var btnOpenImage = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 0, Text = "Open Image" };
-        win.Add (btnOpenImage);
+        _win.Add (btnOpenImage);
 
-        var imageView = new ImageView
+        _tabView = new ()
         {
-            X = 0, Y = Pos.Bottom (lblDriverName), Width = Dim.Fill (), Height = Dim.Fill ()
+            Y = Pos.Bottom (lblSupportsSixel), Width = Dim.Fill (), Height = Dim.Fill ()
         };
-        win.Add (imageView);
 
-        btnOpenImage.Accepting += (_, _) =>
-                               {
-                                   var ofd = new OpenDialog { Title = "Open Image", AllowsMultipleSelection = false };
-                                   Application.Run (ofd);
+        _tabView.AddTab (tabBasic, true);
+        _tabView.AddTab (_tabSixel, false);
+
+        BuildBasicTab (tabBasic);
+        BuildSixelTab ();
+
+        SetupSixelSupported (cbSupportsSixel.CheckedState == CheckState.Checked);
 
-                                   if (ofd.Path is { })
-                                   {
-                                       Directory.SetCurrentDirectory (Path.GetFullPath (Path.GetDirectoryName (ofd.Path)!));
-                                   }
+        btnOpenImage.Accepting += OpenImage;
 
-                                   if (ofd.Canceled)
-                                   {
-                                       ofd.Dispose ();
+        _win.Add (lblSupportsSixel);
+        _win.Add (_tabView);
+        Application.Run (_win);
+        _win.Dispose ();
+        Application.Shutdown ();
+    }
 
-                                       return;
-                                   }
+    private void SetupSixelSupported (bool isSupported)
+    {
+        _tabSixel.View = isSupported ? _sixelSupported : _sixelNotSupported;
+        _tabView.SetNeedsDisplay ();
+    }
 
-                                   string path = ofd.FilePaths [0];
+    private void BtnStartFireOnAccept (object sender, CommandEventArgs e)
+    {
+        if (_fire != null)
+        {
+            return;
+        }
 
-                                   ofd.Dispose ();
+        if (!_sixelSupportResult.SupportsTransparency)
+        {
+            if (MessageBox.Query (
+                                  "Transparency Not Supported",
+                                  "It looks like your terminal does not support transparent sixel backgrounds. Do you want to try anyway?",
+                                  "Yes",
+                                  "No")
+                != 0)
+            {
+                return;
+            }
+        }
 
-                                   if (string.IsNullOrWhiteSpace (path))
-                                   {
-                                       return;
-                                   }
+        _fire = new (_win.Frame.Width * _pxX.Value, _win.Frame.Height * _pxY.Value);
+        _fireEncoder = new ();
+        _fireEncoder.Quantizer.MaxColors = Math.Min (_fireEncoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
+        _fireEncoder.Quantizer.PaletteBuildingAlgorithm = new ConstPalette (_fire.Palette);
 
-                                   if (!File.Exists (path))
-                                   {
-                                       return;
-                                   }
+        _fireFrameCounter = 0;
 
-                                   Image<Rgba32> img;
+        Application.AddTimeout (TimeSpan.FromMilliseconds (30), AdvanceFireTimerCallback);
+    }
 
-                                   try
-                                   {
-                                       img = Image.Load<Rgba32> (File.ReadAllBytes (path));
-                                   }
-                                   catch (Exception ex)
-                                   {
-                                       MessageBox.ErrorQuery ("Could not open file", ex.Message, "Ok");
+    private bool AdvanceFireTimerCallback ()
+    {
+        _fire.AdvanceFrame ();
+        _fireFrameCounter++;
 
-                                       return;
-                                   }
+        // Control frame rate by adjusting this
+        // Lower number means more FPS
+        if (_fireFrameCounter % 2 != 0 || _isDisposed)
+        {
+            return !_isDisposed;
+        }
 
-                                   imageView.SetImage (img);
-                                   Application.Refresh ();
-                               };
+        Color [,] bmp = _fire.GetFirePixels ();
 
-        Application.Run (win);
-        win.Dispose ();
-        Application.Shutdown ();
+        // TODO: Static way of doing this, suboptimal
+        if (_fireSixel != null)
+        {
+            Application.Sixel.Remove (_fireSixel);
+        }
+
+        _fireSixel = new ()
+        {
+            SixelData = _fireEncoder.EncodeSixel (bmp),
+            ScreenPosition = new (0, 0)
+        };
+
+        Application.Sixel.Add (_fireSixel);
+
+        _win.SetNeedsDisplay ();
+
+        return !_isDisposed;
+    }
+
+    /// <inheritdoc/>
+    protected override void Dispose (bool disposing)
+    {
+        base.Dispose (disposing);
+        _imageView.Dispose ();
+        _sixelNotSupported.Dispose ();
+        _sixelSupported.Dispose ();
+        _isDisposed = true;
+
+        Application.Sixel.Clear ();
+    }
+
+    private void OpenImage (object sender, CommandEventArgs e)
+    {
+        var ofd = new OpenDialog { Title = "Open Image", AllowsMultipleSelection = false };
+        Application.Run (ofd);
+
+        if (ofd.Path is { })
+        {
+            Directory.SetCurrentDirectory (Path.GetFullPath (Path.GetDirectoryName (ofd.Path)!));
+        }
+
+        if (ofd.Canceled)
+        {
+            ofd.Dispose ();
+
+            return;
+        }
+
+        string path = ofd.FilePaths [0];
+
+        ofd.Dispose ();
+
+        if (string.IsNullOrWhiteSpace (path))
+        {
+            return;
+        }
+
+        if (!File.Exists (path))
+        {
+            return;
+        }
+
+        Image<Rgba32> img;
+
+        try
+        {
+            img = Image.Load<Rgba32> (File.ReadAllBytes (path));
+        }
+        catch (Exception ex)
+        {
+            MessageBox.ErrorQuery ("Could not open file", ex.Message, "Ok");
+
+            return;
+        }
+
+        _imageView.SetImage (img);
+        ApplyShowTabViewHack ();
+        Application.Refresh ();
+    }
+
+    private void ApplyShowTabViewHack ()
+    {
+        // TODO HACK: This hack seems to be required to make tabview actually refresh itself
+        _tabView.SetNeedsDisplay();
+        var orig = _tabView.SelectedTab;
+        _tabView.SelectedTab = _tabView.Tabs.Except (new []{orig}).ElementAt (0);
+        _tabView.SelectedTab = orig;
+    }
+
+    private void BuildBasicTab (Tab tabBasic)
+    {
+        _imageView = new ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            CanFocus = true
+        };
+
+        tabBasic.View = _imageView;
+    }
+
+    private void BuildSixelTab ()
+    {
+        _sixelSupported = new ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            CanFocus = true
+        };
+
+        _sixelNotSupported = new ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            CanFocus = true
+        };
+
+        _sixelNotSupported.Add (
+                                new Label
+                                {
+                                    Width = Dim.Fill (),
+                                    Height = Dim.Fill (),
+                                    TextAlignment = Alignment.Center,
+                                    Text = "Your driver does not support Sixel image format",
+                                    VerticalTextAlignment = Alignment.Center
+                                });
+
+        _sixelView = new ()
+        {
+            Width = Dim.Percent (50),
+            Height = Dim.Fill (),
+            BorderStyle = LineStyle.Dotted
+        };
+
+        _sixelSupported.Add (_sixelView);
+
+        var btnSixel = new Button
+        {
+            X = Pos.Right (_sixelView),
+            Y = 0,
+            Text = "Output Sixel", Width = Dim.Auto ()
+        };
+        btnSixel.Accepting += OutputSixelButtonClick;
+        _sixelSupported.Add (btnSixel);
+
+        var btnStartFire = new Button
+        {
+            X = Pos.Right (_sixelView),
+            Y = Pos.Bottom (btnSixel),
+            Text = "Start Fire"
+        };
+        btnStartFire.Accepting += BtnStartFireOnAccept;
+        _sixelSupported.Add (btnStartFire);
+
+        var lblPxX = new Label
+        {
+            X = Pos.Right (_sixelView),
+            Y = Pos.Bottom (btnStartFire) + 1,
+            Text = "Pixels per Col:"
+        };
+
+        _pxX = new ()
+        {
+            X = Pos.Right (lblPxX),
+            Y = Pos.Bottom (btnStartFire) + 1,
+            Value = _sixelSupportResult.Resolution.Width
+        };
+
+        var lblPxY = new Label
+        {
+            X = lblPxX.X,
+            Y = Pos.Bottom (_pxX),
+            Text = "Pixels per Row:"
+        };
+
+        _pxY = new ()
+        {
+            X = Pos.Right (lblPxY),
+            Y = Pos.Bottom (_pxX),
+            Value = _sixelSupportResult.Resolution.Height
+        };
+
+        var l1 = new Label
+        {
+            Text = "Palette Building Algorithm",
+            Width = Dim.Auto (),
+            X = Pos.Right (_sixelView),
+            Y = Pos.Bottom (_pxY) + 1
+        };
+
+        _rgPaletteBuilder = new()
+        {
+            RadioLabels = new []
+            {
+                "Popularity",
+                "Median Cut"
+            },
+            X = Pos.Right (_sixelView) + 2,
+            Y = Pos.Bottom (l1),
+            SelectedItem = 1
+        };
+
+        _popularityThreshold = new ()
+        {
+            X = Pos.Right (_rgPaletteBuilder) + 1,
+            Y = Pos.Top (_rgPaletteBuilder),
+            Value = 8
+        };
+
+        var lblPopThreshold = new Label
+        {
+            Text = "(threshold)",
+            X = Pos.Right (_popularityThreshold),
+            Y = Pos.Top (_popularityThreshold)
+        };
+
+        var l2 = new Label
+        {
+            Text = "Color Distance Algorithm",
+            Width = Dim.Auto (),
+            X = Pos.Right (_sixelView),
+            Y = Pos.Bottom (_rgPaletteBuilder) + 1
+        };
+
+        _rgDistanceAlgorithm = new()
+        {
+            RadioLabels = new []
+            {
+                "Euclidian",
+                "CIE76"
+            },
+            X = Pos.Right (_sixelView) + 2,
+            Y = Pos.Bottom (l2)
+        };
+
+        _sixelSupported.Add (lblPxX);
+        _sixelSupported.Add (_pxX);
+        _sixelSupported.Add (lblPxY);
+        _sixelSupported.Add (_pxY);
+        _sixelSupported.Add (l1);
+        _sixelSupported.Add (_rgPaletteBuilder);
+
+        _sixelSupported.Add (l2);
+        _sixelSupported.Add (_rgDistanceAlgorithm);
+        _sixelSupported.Add (_popularityThreshold);
+        _sixelSupported.Add (lblPopThreshold);
+
+        _sixelView.DrawContent += SixelViewOnDrawContent;
+    }
+
+    private IPaletteBuilder GetPaletteBuilder ()
+    {
+        switch (_rgPaletteBuilder.SelectedItem)
+        {
+            case 0: return new PopularityPaletteWithThreshold (GetDistanceAlgorithm (), _popularityThreshold.Value);
+            case 1: return new MedianCutPaletteBuilder (GetDistanceAlgorithm ());
+            default: throw new ArgumentOutOfRangeException ();
+        }
+    }
+
+    private IColorDistance GetDistanceAlgorithm ()
+    {
+        switch (_rgDistanceAlgorithm.SelectedItem)
+        {
+            case 0: return new EuclideanColorDistance ();
+            case 1: return new CIE76ColorDistance ();
+            default: throw new ArgumentOutOfRangeException ();
+        }
+    }
+
+    private void OutputSixelButtonClick (object sender, CommandEventArgs e)
+    {
+        if (_imageView.FullResImage == null)
+        {
+            MessageBox.Query ("No Image Loaded", "You must first open an image.  Use the 'Open Image' button above.", "Ok");
+
+            return;
+        }
+
+        _screenLocationForSixel = _sixelView.FrameToScreen ().Location;
+
+        _encodedSixelData = GenerateSixelData (
+                                               _imageView.FullResImage,
+                                               _sixelView.Frame.Size,
+                                               _pxX.Value,
+                                               _pxY.Value);
+
+        if (_sixelImage == null)
+        {
+            _sixelImage = new ()
+            {
+                SixelData = _encodedSixelData,
+                ScreenPosition = _screenLocationForSixel
+            };
+
+            Application.Sixel.Add (_sixelImage);
+        }
+        else
+        {
+            _sixelImage.ScreenPosition = _screenLocationForSixel;
+            _sixelImage.SixelData = _encodedSixelData;
+        }
+
+        _sixelView.SetNeedsDisplay ();
+    }
+
+    private void SixelViewOnDrawContent (object sender, DrawEventArgs e)
+    {
+        if (!string.IsNullOrWhiteSpace (_encodedSixelData))
+        {
+            // Does not work (see https://github.com/gui-cs/Terminal.Gui/issues/3763)
+            // Application.Driver?.Move (_screenLocationForSixel.X, _screenLocationForSixel.Y);
+            // Application.Driver?.AddStr (_encodedSixelData);
+
+            // Works in NetDriver but results in screen flicker when moving mouse but vanish instantly
+            // Console.SetCursorPosition (_screenLocationForSixel.X, _screenLocationForSixel.Y);
+            // Console.Write (_encodedSixelData);
+        }
+    }
+
+    public string GenerateSixelData (
+        Image<Rgba32> fullResImage,
+        Size maxSize,
+        int pixelsPerCellX,
+        int pixelsPerCellY
+    )
+    {
+        var encoder = new SixelEncoder ();
+        encoder.Quantizer.MaxColors = Math.Min (encoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
+        encoder.Quantizer.PaletteBuildingAlgorithm = GetPaletteBuilder ();
+        encoder.Quantizer.DistanceAlgorithm = GetDistanceAlgorithm ();
+
+        // Calculate the target size in pixels based on console units
+        int targetWidthInPixels = maxSize.Width * pixelsPerCellX;
+        int targetHeightInPixels = maxSize.Height * pixelsPerCellY;
+
+        // Get the original image dimensions
+        int originalWidth = fullResImage.Width;
+        int originalHeight = fullResImage.Height;
+
+        // Use the helper function to get the resized dimensions while maintaining the aspect ratio
+        Size newSize = CalculateAspectRatioFit (originalWidth, originalHeight, targetWidthInPixels, targetHeightInPixels);
+
+        // Resize the image to match the console size
+        Image<Rgba32> resizedImage = fullResImage.Clone (x => x.Resize (newSize.Width, newSize.Height));
+
+        string encoded = encoder.EncodeSixel (ConvertToColorArray (resizedImage));
+
+        var pv = new PaletteView (encoder.Quantizer.Palette.ToList ());
+
+        var dlg = new Dialog
+        {
+            Title = "Palette (Esc to close)",
+            Width = Dim.Fill (2),
+            Height = Dim.Fill (1)
+        };
+
+        var btn = new Button
+        {
+            Text = "Ok"
+        };
+
+        btn.Accepting += (s, e) => Application.RequestStop ();
+        dlg.Add (pv);
+        dlg.AddButton (btn);
+        Application.Run (dlg);
+        dlg.Dispose ();
+
+        return encoded;
+    }
+
+    private Size CalculateAspectRatioFit (int originalWidth, int originalHeight, int targetWidth, int targetHeight)
+    {
+        // Calculate the scaling factor for width and height
+        double widthScale = (double)targetWidth / originalWidth;
+        double heightScale = (double)targetHeight / originalHeight;
+
+        // Use the smaller scaling factor to maintain the aspect ratio
+        double scale = Math.Min (widthScale, heightScale);
+
+        // Calculate the new width and height while keeping the aspect ratio
+        var newWidth = (int)(originalWidth * scale);
+        var newHeight = (int)(originalHeight * scale);
+
+        // Return the new size as a Size object
+        return new (newWidth, newHeight);
+    }
+
+    public static Color [,] ConvertToColorArray (Image<Rgba32> image)
+    {
+        int width = image.Width;
+        int height = image.Height;
+        Color [,] colors = new Color [width, height];
+
+        // Loop through each pixel and convert Rgba32 to Terminal.Gui color
+        for (var x = 0; x < width; x++)
+        {
+            for (var y = 0; y < height; y++)
+            {
+                Rgba32 pixel = image [x, y];
+                colors [x, y] = new (pixel.R, pixel.G, pixel.B); // Convert Rgba32 to Terminal.Gui color
+            }
+        }
+
+        return colors;
     }
 
     private class ImageView : View
     {
         private readonly ConcurrentDictionary<Rgba32, Attribute> _cache = new ();
-        private Image<Rgba32> _fullResImage;
+        public Image<Rgba32> FullResImage;
         private Image<Rgba32> _matchSize;
 
         public override void OnDrawContent (Rectangle bounds)
         {
             base.OnDrawContent (bounds);
 
-            if (_fullResImage == null)
+            if (FullResImage == null)
             {
                 return;
             }
@@ -127,7 +637,7 @@ public class Images : Scenario
             if (_matchSize == null || bounds.Width != _matchSize.Width || bounds.Height != _matchSize.Height)
             {
                 // generate one
-                _matchSize = _fullResImage.Clone (x => x.Resize (bounds.Width, bounds.Height));
+                _matchSize = FullResImage.Clone (x => x.Resize (bounds.Width, bounds.Height));
             }
 
             for (var y = 0; y < bounds.Height; y++)
@@ -138,10 +648,10 @@ public class Images : Scenario
 
                     Attribute attr = _cache.GetOrAdd (
                                                       rgb,
-                                                      rgb => new Attribute (
-                                                                            new Color (),
-                                                                            new Color (rgb.R, rgb.G, rgb.B)
-                                                                           )
+                                                      rgb => new (
+                                                                  new Color (),
+                                                                  new Color (rgb.R, rgb.G, rgb.B)
+                                                                 )
                                                      );
 
                     Driver.SetAttribute (attr);
@@ -152,8 +662,390 @@ public class Images : Scenario
 
         internal void SetImage (Image<Rgba32> image)
         {
-            _fullResImage = image;
+            FullResImage = image;
             SetNeedsDisplay ();
         }
     }
+
+    public class PaletteView : View
+    {
+        private readonly List<Color> _palette;
+
+        public PaletteView (List<Color> palette)
+        {
+            _palette = palette ?? new List<Color> ();
+            Width = Dim.Fill ();
+            Height = Dim.Fill ();
+        }
+
+        // Automatically calculates rows and columns based on the available bounds
+        private (int columns, int rows) CalculateGridSize (Rectangle bounds)
+        {
+            // Characters are twice as wide as they are tall, so use 2:1 width-to-height ratio
+            int availableWidth = bounds.Width / 2; // Each color block is 2 character wide
+            int availableHeight = bounds.Height;
+
+            int numColors = _palette.Count;
+
+            // Calculate the number of columns and rows we can fit within the bounds
+            int columns = Math.Min (availableWidth, numColors);
+            int rows = (numColors + columns - 1) / columns; // Ceiling division for rows
+
+            // Ensure we do not exceed the available height
+            if (rows > availableHeight)
+            {
+                rows = availableHeight;
+                columns = (numColors + rows - 1) / rows; // Recalculate columns if needed
+            }
+
+            return (columns, rows);
+        }
+
+        public override void OnDrawContent (Rectangle bounds)
+        {
+            base.OnDrawContent (bounds);
+
+            if (_palette == null || _palette.Count == 0)
+            {
+                return;
+            }
+
+            // Calculate the grid size based on the bounds
+            (int columns, int rows) = CalculateGridSize (bounds);
+
+            // Draw the colors in the palette
+            for (var i = 0; i < _palette.Count && i < columns * rows; i++)
+            {
+                int row = i / columns;
+                int col = i % columns;
+
+                // Calculate position in the grid
+                int x = col * 2; // Each color block takes up 2 horizontal spaces
+                int y = row;
+
+                // Set the color attribute for the block
+                Driver.SetAttribute (new (_palette [i], _palette [i]));
+
+                // Draw the block (2 characters wide per block)
+                for (var dx = 0; dx < 2; dx++) // Fill the width of the block
+                {
+                    AddRune (x + dx, y, (Rune)' ');
+                }
+            }
+        }
+    }
+}
+
+internal class ConstPalette : IPaletteBuilder
+{
+    private readonly List<Color> _palette;
+
+    public ConstPalette (Color [] palette) { _palette = palette.ToList (); }
+
+    /// <inheritdoc/>
+    public List<Color> BuildPalette (List<Color> colors, int maxColors) { return _palette; }
+}
+
+public abstract class LabColorDistance : IColorDistance
+{
+    // Reference white point for D65 illuminant (can be moved to constants)
+    private const double RefX = 95.047;
+    private const double RefY = 100.000;
+    private const double RefZ = 108.883;
+
+    // Conversion from RGB to Lab
+    protected LabColor RgbToLab (Color c)
+    {
+        XYZ xyz = ColorConverter.RgbToXyz (new (c.R, c.G, c.B));
+
+        // Normalize XYZ values by reference white point
+        double x = xyz.X / RefX;
+        double y = xyz.Y / RefY;
+        double z = xyz.Z / RefZ;
+
+        // Apply the nonlinear transformation for Lab
+        x = x > 0.008856 ? Math.Pow (x, 1.0 / 3.0) : 7.787 * x + 16.0 / 116.0;
+        y = y > 0.008856 ? Math.Pow (y, 1.0 / 3.0) : 7.787 * y + 16.0 / 116.0;
+        z = z > 0.008856 ? Math.Pow (z, 1.0 / 3.0) : 7.787 * z + 16.0 / 116.0;
+
+        // Calculate Lab values
+        double l = 116.0 * y - 16.0;
+        double a = 500.0 * (x - y);
+        double b = 200.0 * (y - z);
+
+        return new (l, a, b);
+    }
+
+    // LabColor class encapsulating L, A, and B values
+    protected class LabColor
+    {
+        public double L { get; }
+        public double A { get; }
+        public double B { get; }
+
+        public LabColor (double l, double a, double b)
+        {
+            L = l;
+            A = a;
+            B = b;
+        }
+    }
+
+    /// <inheritdoc/>
+    public abstract double CalculateDistance (Color c1, Color c2);
+}
+
+/// <summary>
+///     This is the simplest method to measure color difference in the CIE Lab color space. The Euclidean distance in Lab
+///     space is more aligned with human perception than RGB space, as Lab attempts to model how humans perceive color
+///     differences.
+/// </summary>
+public class CIE76ColorDistance : LabColorDistance
+{
+    public override double CalculateDistance (Color c1, Color c2)
+    {
+        LabColor lab1 = RgbToLab (c1);
+        LabColor lab2 = RgbToLab (c2);
+
+        // Euclidean distance in Lab color space
+        return Math.Sqrt (Math.Pow (lab1.L - lab2.L, 2) + Math.Pow (lab1.A - lab2.A, 2) + Math.Pow (lab1.B - lab2.B, 2));
+    }
+}
+
+public class MedianCutPaletteBuilder : IPaletteBuilder
+{
+    private readonly IColorDistance _colorDistance;
+
+    public MedianCutPaletteBuilder (IColorDistance colorDistance) { _colorDistance = colorDistance; }
+
+    public List<Color> BuildPalette (List<Color> colors, int maxColors)
+    {
+        if (colors == null || colors.Count == 0 || maxColors <= 0)
+        {
+            return new ();
+        }
+
+        return MedianCut (colors, maxColors);
+    }
+
+    private List<Color> MedianCut (List<Color> colors, int maxColors)
+    {
+        List<List<Color>> cubes = new () { colors };
+
+        // Recursively split color regions
+        while (cubes.Count < maxColors)
+        {
+            var added = false;
+            cubes.Sort ((a, b) => Volume (a).CompareTo (Volume (b)));
+
+            List<Color> largestCube = cubes.Last ();
+            cubes.RemoveAt (cubes.Count - 1);
+
+            // Check if the largest cube contains only one unique color
+            if (IsSingleColorCube (largestCube))
+            {
+                // Add back and stop splitting this cube
+                cubes.Add (largestCube);
+
+                break;
+            }
+
+            (List<Color> cube1, List<Color> cube2) = SplitCube (largestCube);
+
+            if (cube1.Any ())
+            {
+                cubes.Add (cube1);
+                added = true;
+            }
+
+            if (cube2.Any ())
+            {
+                cubes.Add (cube2);
+                added = true;
+            }
+
+            // Break the loop if no new cubes were added
+            if (!added)
+            {
+                break;
+            }
+        }
+
+        // Calculate average color for each cube
+        return cubes.Select (AverageColor).Distinct ().ToList ();
+    }
+
+    // Checks if all colors in the cube are the same
+    private bool IsSingleColorCube (List<Color> cube)
+    {
+        Color firstColor = cube.First ();
+
+        return cube.All (c => c.R == firstColor.R && c.G == firstColor.G && c.B == firstColor.B);
+    }
+
+    // Splits the cube based on the largest color component range
+    private (List<Color>, List<Color>) SplitCube (List<Color> cube)
+    {
+        (int component, int range) = FindLargestRange (cube);
+
+        // Sort by the largest color range component (either R, G, or B)
+        cube.Sort (
+                   (c1, c2) => component switch
+                               {
+                                   0 => c1.R.CompareTo (c2.R),
+                                   1 => c1.G.CompareTo (c2.G),
+                                   2 => c1.B.CompareTo (c2.B),
+                                   _ => 0
+                               });
+
+        int medianIndex = cube.Count / 2;
+        List<Color> cube1 = cube.Take (medianIndex).ToList ();
+        List<Color> cube2 = cube.Skip (medianIndex).ToList ();
+
+        return (cube1, cube2);
+    }
+
+    private (int, int) FindLargestRange (List<Color> cube)
+    {
+        byte minR = cube.Min (c => c.R);
+        byte maxR = cube.Max (c => c.R);
+        byte minG = cube.Min (c => c.G);
+        byte maxG = cube.Max (c => c.G);
+        byte minB = cube.Min (c => c.B);
+        byte maxB = cube.Max (c => c.B);
+
+        int rangeR = maxR - minR;
+        int rangeG = maxG - minG;
+        int rangeB = maxB - minB;
+
+        if (rangeR >= rangeG && rangeR >= rangeB)
+        {
+            return (0, rangeR);
+        }
+
+        if (rangeG >= rangeR && rangeG >= rangeB)
+        {
+            return (1, rangeG);
+        }
+
+        return (2, rangeB);
+    }
+
+    private Color AverageColor (List<Color> cube)
+    {
+        var avgR = (byte)cube.Average (c => c.R);
+        var avgG = (byte)cube.Average (c => c.G);
+        var avgB = (byte)cube.Average (c => c.B);
+
+        return new (avgR, avgG, avgB);
+    }
+
+    private int Volume (List<Color> cube)
+    {
+        if (cube == null || cube.Count == 0)
+        {
+            // Return a volume of 0 if the cube is empty or null
+            return 0;
+        }
+
+        byte minR = cube.Min (c => c.R);
+        byte maxR = cube.Max (c => c.R);
+        byte minG = cube.Min (c => c.G);
+        byte maxG = cube.Max (c => c.G);
+        byte minB = cube.Min (c => c.B);
+        byte maxB = cube.Max (c => c.B);
+
+        return (maxR - minR) * (maxG - minG) * (maxB - minB);
+    }
+}
+
+public class DoomFire
+{
+    private readonly int _width;
+    private readonly int _height;
+    private readonly Color [,] _firePixels;
+    private static Color [] _palette;
+    public Color [] Palette => _palette;
+    private readonly Random _random = new ();
+
+    public DoomFire (int width, int height)
+    {
+        _width = width;
+        _height = height;
+        _firePixels = new Color [width, height];
+        InitializePalette ();
+        InitializeFire ();
+    }
+
+    private void InitializePalette ()
+    {
+        // Initialize a basic fire palette. You can modify these colors as needed.
+        _palette = new Color [37]; // Using 37 colors as per the original Doom fire palette scale.
+
+        // First color is transparent black
+        _palette [0] = new (0, 0, 0, 0); // Transparent black (ARGB)
+
+        // The rest of the palette is fire colors
+        for (var i = 1; i < 37; i++)
+        {
+            var r = (byte)Math.Min (255, i * 7);
+            var g = (byte)Math.Min (255, i * 5);
+            var b = (byte)Math.Min (255, i * 2);
+            _palette [i] = new (r, g, b); // Full opacity
+        }
+    }
+
+    public void InitializeFire ()
+    {
+        // Set the bottom row to full intensity (simulate the base of the fire).
+        for (var x = 0; x < _width; x++)
+        {
+            _firePixels [x, _height - 1] = _palette [36]; // Max intensity fire.
+        }
+
+        // Set the rest of the pixels to black (transparent).
+        for (var y = 0; y < _height - 1; y++)
+        {
+            for (var x = 0; x < _width; x++)
+            {
+                _firePixels [x, y] = _palette [0]; // Transparent black
+            }
+        }
+    }
+
+    public void AdvanceFrame ()
+    {
+        // Process every pixel except the bottom row
+        for (var x = 0; x < _width; x++)
+        {
+            for (var y = 1; y < _height; y++) // Skip the last row (which is always max intensity)
+            {
+                int srcX = x;
+                int srcY = y;
+                int dstY = y - 1;
+
+                // Spread fire upwards with randomness
+                int decay = _random.Next (0, 2);
+                int dstX = srcX + _random.Next (-1, 2);
+
+                if (dstX < 0 || dstX >= _width) // Prevent out of bounds
+                {
+                    dstX = srcX;
+                }
+
+                // Get the fire color from below and reduce its intensity
+                Color srcColor = _firePixels [srcX, srcY];
+                int intensity = Array.IndexOf (_palette, srcColor) - decay;
+
+                if (intensity < 0)
+                {
+                    intensity = 0;
+                }
+
+                _firePixels [dstX, dstY] = _palette [intensity];
+            }
+        }
+    }
+
+    public Color [,] GetFirePixels () { return _firePixels; }
 }

+ 118 - 0
UnitTests/Drawing/PopularityPaletteWithThresholdTests.cs

@@ -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 - 0
UnitTests/Drawing/SixelEncoderTests.cs

@@ -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);
+    }
+}