Prechádzať zdrojové kódy

Merge pull request #170 from tznind/add-sixel-detector-interface

Add sixel detector interface
Thomas Nind 9 mesiacov pred
rodič
commit
1c3b0d7605

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

@@ -1356,6 +1356,16 @@ public static class EscSeqUtils
     /// </summary>
     /// </summary>
     public const string CSI_ReportDeviceAttributes_Terminator = "c";
     public const string CSI_ReportDeviceAttributes_Terminator = "c";
 
 
+    /// <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>
     /// <summary>
     ///     CSI 1 8 t  | yes | yes |  yes  | report window size in chars
     ///     CSI 1 8 t  | yes | yes |  yes  | report window size in chars
     ///     https://terminalguide.namepad.de/seq/csi_st-18/
     ///     https://terminalguide.namepad.de/seq/csi_st-18/

+ 133 - 0
Terminal.Gui/Drawing/SixelSupportDetector.cs

@@ -0,0 +1,133 @@
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Uses Ansi escape sequences to detect whether sixel is supported
+///     by the terminal.
+/// </summary>
+public class SixelSupportDetector
+{
+    /// <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 <see cref="SixelSupportDetector"/>
+/// 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; }
+}

+ 24 - 2
UICatalog/Scenarios/Images.cs

@@ -61,9 +61,15 @@ public class Images : Scenario
     private RadioGroup _rgDistanceAlgorithm;
     private RadioGroup _rgDistanceAlgorithm;
     private NumericUpDown _popularityThreshold;
     private NumericUpDown _popularityThreshold;
     private SixelToRender _sixelImage;
     private SixelToRender _sixelImage;
+    private SixelSupportResult _sixelSupportResult;
 
 
     public override void Main ()
     public override void Main ()
     {
     {
+        var sixelSupportDetector = new SixelSupportDetector ();
+        _sixelSupportResult = sixelSupportDetector.Detect ();
+
+        ConsoleDriver.SupportsSixel = _sixelSupportResult.IsSupported;
+
         Application.Init ();
         Application.Init ();
         _win = new() { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" };
         _win = new() { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" };
 
 
@@ -158,8 +164,23 @@ public class Images : Scenario
             return;
             return;
         }
         }
 
 
+        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;
+            }
+        }
+
+
         _fire = new DoomFire (_win.Frame.Width * _pxX.Value, _win.Frame.Height * _pxY.Value);
         _fire = new DoomFire (_win.Frame.Width * _pxX.Value, _win.Frame.Height * _pxY.Value);
         _fireEncoder = new SixelEncoder ();
         _fireEncoder = new SixelEncoder ();
+        _fireEncoder.Quantizer.MaxColors = Math.Min (_fireEncoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
         _fireEncoder.Quantizer.PaletteBuildingAlgorithm = new ConstPalette (_fire.Palette);
         _fireEncoder.Quantizer.PaletteBuildingAlgorithm = new ConstPalette (_fire.Palette);
 
 
         _fireFrameCounter = 0;
         _fireFrameCounter = 0;
@@ -337,7 +358,7 @@ public class Images : Scenario
         {
         {
             X = Pos.Right (lblPxX),
             X = Pos.Right (lblPxX),
             Y = Pos.Bottom (btnStartFire) + 1,
             Y = Pos.Bottom (btnStartFire) + 1,
-            Value = 10
+            Value = _sixelSupportResult.Resolution.Width
         };
         };
 
 
         var lblPxY = new Label
         var lblPxY = new Label
@@ -351,7 +372,7 @@ public class Images : Scenario
         {
         {
             X = Pos.Right (lblPxY),
             X = Pos.Right (lblPxY),
             Y = Pos.Bottom (_pxX),
             Y = Pos.Bottom (_pxX),
-            Value = 20
+            Value = _sixelSupportResult.Resolution.Height
         };
         };
 
 
         var l1 = new Label ()
         var l1 = new Label ()
@@ -500,6 +521,7 @@ public class Images : Scenario
     )
     )
     {
     {
         var encoder = new SixelEncoder ();
         var encoder = new SixelEncoder ();
+        encoder.Quantizer.MaxColors = Math.Min (encoder.Quantizer.MaxColors, _sixelSupportResult.MaxPaletteColors);
         encoder.Quantizer.PaletteBuildingAlgorithm = GetPaletteBuilder ();
         encoder.Quantizer.PaletteBuildingAlgorithm = GetPaletteBuilder ();
         encoder.Quantizer.DistanceAlgorithm = GetDistanceAlgorithm ();
         encoder.Quantizer.DistanceAlgorithm = GetDistanceAlgorithm ();