Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 38 additions & 28 deletions src/Sixel/Sixel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

using Sixel.Shared;
using Sixel.Terminal;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
Expand All @@ -17,21 +16,42 @@ namespace Sixel;
public class Convert
{
private static readonly StringBuilder SixelBuilder = new();

/// <summary>
/// The character to use when a sixel is empty/transparent.
/// </summary>
private const char SixelEmpty = '?';

/// <summary>
/// The character to use when moving to the next line in a sixel.
/// </summary>
private const char SixelDECGNL = '-';

/// <summary>
/// The character to use when going back to the start of the current line in a sixel to write more data over it.
/// </summary>
private const char SixelDECGCR = '$';
private const string SixelStart = "\u001BP0;1q\"1;1;";
private const string SixelEnd = "\u001b\\";

/// <summary>
/// The start of a sixel sequence.
/// </summary>
private const string SixelStart = $"{Constants.Escape}P0;1q";

/// <summary>
/// The raster settings for setting the sixel pixel ratio to 1:1 so images are square when rendered instead of the 2:1 double height default.
/// </summary>
private const string SixelRasterAttributes = "\"1;1;";

/// <summary>
/// The end of a sixel sequence.
/// </summary>
private const string SixelEnd = $"{Constants.Escape}\\";

/// <summary>
/// The transparent color for the sixel, this is red but the sixel should be transparent so this is not visible.
/// </summary>
private const string TransparentColor = "#0;2;100;0;0";
internal static readonly bool TerminalSupportsSixel;
static Convert()
{
TerminalSupportsSixel = _TerminalSupportsSixel();
}
public static bool GetTerminalSupportsSixel()
{
return TerminalSupportsSixel;
}

private static readonly ResizeOptions ResizeOptions = new()
{
Sampler = KnownResamplers.NearestNeighbor,
Expand All @@ -57,13 +77,14 @@ public static string ImgToSixel(string filename, int maxColors)
SixelBuilder.Clear();
}
}
public static string ImgToSixel(string filename, int maxColors, int width)
public static string ImgToSixel(string filename, int maxColors, int cellWidth)
{
var pixelWidth = cellWidth * Compatibility.GetCellSize().PixelWidth;
try
{
using var image = LoadImage(filename);
int scaledHeight = (int)Math.Round((double)image.Height / image.Width * width);
MutateSizeAndColors(image, width, scaledHeight, maxColors);
int scaledHeight = (int)Math.Round((double)image.Height / image.Width * pixelWidth);
MutateSizeAndColors(image, pixelWidth, scaledHeight, maxColors);
RenderImage(image);
return SixelBuilder.ToString();
}
Expand Down Expand Up @@ -207,21 +228,10 @@ private static void AppendExitSixel()
private static void StartSixel(int width, int height)
{
SixelBuilder.Append(SixelStart)
.Append(SixelRasterAttributes)
.Append(width)
.Append(';')
.Append(height)
.Append(TransparentColor);
}
private static bool _TerminalSupportsSixel()
{
char? c = null;
var response = string.Empty;
System.Console.Write("\u001b[c");
do
{
c = Console.ReadKey(true).KeyChar;
response += c;
} while (c != 'c' && Console.KeyAvailable);
return response.Contains(";4;");
}
}
19 changes: 13 additions & 6 deletions src/Sixel/SixelCmdlet.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.IO;
using System.Management.Automation;
using System.Net.Http;
using Sixel.Terminal;

namespace Sixel;

Expand All @@ -10,6 +9,7 @@ namespace Sixel;
public sealed class ConvertSixelCmdlet : PSCmdlet
{
[Parameter(
HelpMessage = "A path to a local image to convert to sixel.",
Mandatory = true,
ValueFromPipelineByPropertyName = true,
Position = 0,
Expand All @@ -20,25 +20,32 @@ public sealed class ConvertSixelCmdlet : PSCmdlet
public string Path { get; set; } = null!;

[Parameter(
HelpMessage = "A URL of the image to download and convert to sixel.",
Mandatory = true,
ValueFromPipeline = true,
ParameterSetName = "Url"
)]
[Alias("Uri")]
public string Url { get; set; } = null!;

[Parameter()]
[Parameter(
HelpMessage = "The maximum number of colors to use in the image."
)]
[ValidateRange(1, 256)]
public int MaxColors { get; set; } = 256;

[Parameter()]
[Parameter(
HelpMessage = "Width of the image in character cells, the height will be scaled to maintain aspect ratio."
)]
public int Width { get; set; }

[Parameter()]
[Parameter(
HelpMessage = "Force the command to attempt to output sixel data even if the terminal does not support sixel."
)]
public SwitchParameter Force { get; set; }
protected override void BeginProcessing()
{
if (Convert.GetTerminalSupportsSixel() == false && Force == false)
if (Compatibility.TerminalSupportsSixel() == false && Force == false)
{
this.ThrowTerminatingError(new ErrorRecord(new System.Exception("Terminal does not support sixel, override with -Force for test."), "SixelError", ErrorCategory.NotImplemented, null));
}
Expand Down
93 changes: 93 additions & 0 deletions src/Sixel/Terminal/Compatibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using Sixel.Terminal.Models;

namespace Sixel.Terminal;

public class Compatibility
{
/// <summary>
/// Memory-caches the result of the terminal supporting sixel graphics.
/// </summary>
private static bool? _terminalSupportsSixel;

/// <summary>
/// Memory-caches the result of the terminal cell size.
/// </summary>
private static CellSize? _cellSize;

/// <summary>
/// Get the cell size of the terminal in pixel-sixel size.
/// The response to the command will look like [6;20;10t where the 20 is height and 10 is width.
/// I think the 6 is the terminal class, which is not used here.
/// </summary>
/// <returns>The number of pixel sixels that will fit in a single character cell.</returns>
public static CellSize GetCellSize()
{
if (_cellSize != null)
{
return _cellSize;
}

var response = GetControlSequenceResponse("[16t");

try
{
var parts = response.Split(';', 't');
_cellSize = new CellSize {
PixelWidth = int.Parse(parts[2]),
PixelHeight = int.Parse(parts[1])
};
}
catch
{
// Return the default Windows Terminal size if we can't get the size from the terminal.
_cellSize = new CellSize {
PixelWidth = 10,
PixelHeight = 20
};
// TODO: Write this to a normal powershell warning stream.
Console.Error.WriteLine("Could not get terminal cell size, using default size 10x20.");
}

return _cellSize;
}

/// <summary>
/// Check if the terminal supports sixel graphics.
/// This is done by sending the terminal a Device Attributes request.
/// If the terminal responds with a response that contains ";4;" then it supports sixel graphics.
/// https://vt100.net/docs/vt510-rm/DA1.html
/// </summary>
/// <returns>True if the terminal supports sixel graphics, false otherwise.</returns>
public static bool TerminalSupportsSixel()
{
if (_terminalSupportsSixel.HasValue)
{
return _terminalSupportsSixel.Value;
}

_terminalSupportsSixel = GetControlSequenceResponse("[c").Contains(";4;");

return _terminalSupportsSixel.Value;
}

/// <summary>
/// Send a control sequence to the terminal and read back the response from STDIN.
/// </summary>
/// <param name="controlSequence"></param>
/// <returns>The response from the terminal.</returns>
private static string GetControlSequenceResponse(string controlSequence)
{
char? c;
var response = string.Empty;

Console.Write($"{Constants.Escape}{controlSequence}");
do
{
c = Console.ReadKey(true).KeyChar;
response += c;
} while (c != 'c' && Console.KeyAvailable);

return response;
}

}
9 changes: 9 additions & 0 deletions src/Sixel/Terminal/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Sixel.Terminal;

public static class Constants
{
/// <summary>
/// The character to use when entering a terminal escape code sequence.
/// </summary>
public const string Escape = "\u001b";
}
15 changes: 15 additions & 0 deletions src/Sixel/Terminal/Models/CellSize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Sixel.Terminal.Models;

public class CellSize
{
/// <summary>
/// The width of a cell in pixels.
/// </summary>
public int PixelWidth { get; set; }

/// <summary>
/// The height of a cell in pixels.
/// This isn't used for anything yet but this would be required for something like spectre console that needs to work around the size of the rendered sixel image.
/// </summary>
public int PixelHeight { get; set; }
}