Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SixelImage #1679

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
116 changes: 116 additions & 0 deletions src/Extensions/Spectre.Console.ImageSharp/SixelImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Spectre.Console.ImageSharp.Sixels;
using Spectre.Console.ImageSharp.Sixels.Models;
using Spectre.Console.Rendering;

namespace Spectre.Console;

/// <summary>
/// Represents a renderable image.
/// </summary>
public sealed class SixelImage : Renderable
{
/// <summary>
/// Gets the image width in pixels.
/// </summary>
public int Width => Image.Width;

/// <summary>
/// Gets the image height in pixels.
/// </summary>
public int Height => Image.Height;

/// <summary>
/// Gets or sets the render width of the canvas in terminal cells.
/// </summary>
public int? MaxWidth { get; set; }

/// <summary>
/// Gets the render width of the canvas. This is hard coded to 1 for sixel images.
/// </summary>
public int PixelWidth { get; } = 1;

internal SixLabors.ImageSharp.Image<Rgba32> Image { get; }

/// <summary>
/// Initializes a new instance of the <see cref="SixelImage"/> class.
/// </summary>
/// <param name="filename">The image filename.</param>
public SixelImage(string filename)
{
Image = SixLabors.ImageSharp.Image.Load<Rgba32>(filename);
}

/// <inheritdoc/>
protected override Measurement Measure(RenderOptions options, int maxWidth)
{
if (PixelWidth < 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
}

var width = MaxWidth ?? Width;
if (maxWidth < width * PixelWidth)
{
return new Measurement(maxWidth, maxWidth);
}

return new Measurement(width * PixelWidth, width * PixelWidth);
}

/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
{
// Got a max width smaller than the render max width?
if (MaxWidth != null && MaxWidth < maxWidth)
{
maxWidth = MaxWidth.Value;
}

// Write the sixel data as a control segment which returns the cursor to the top left cell of the sixel after render.
var sixel = SixelParser.ImageToSixel(Image, maxWidth);
var segments = new List<Segment>
{
Segment.Control(sixel.SixelString),
};

// Draw a transparent renderable to take up the space the sixel is drawn in.
// This allows Spectre.Console to render the image and not write overtop of it with space characters while padding panel borders etc.
var canvas = new Canvas(sixel.CellWidth, sixel.CellHeight)
{
MaxWidth = sixel.CellWidth,
PixelWidth = PixelWidth,
Scale = false,
};

// TODO remove this, it's for drawing a red and transparent checkerboard pattern to debug sixel positioning
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SPECTRE_CONSOLE_DEBUG")))
{
for (var y = 0; y < sixel.CellHeight; y++)
{
for (var x = 0; x < sixel.CellWidth; x++)
{
if (y % 2 == 0)
{
if (x % 2 == 0)
{
canvas.SetPixel(x, y, new Color(255, 0, 0));
}
}
else if (x % 2 != 0)
{
canvas.SetPixel(x, y, new Color(255, 0, 0));
}
}
}
}

// A combination of a zero-width control segment for sixel data and a transparent canvas.
segments.AddRange(((IRenderable)canvas).Render(options, maxWidth));

return segments;
}
}
96 changes: 96 additions & 0 deletions src/Extensions/Spectre.Console.ImageSharp/Sixels/Compatibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Spectre.Console.ImageSharp.Sixels.Models;

namespace Spectre.Console.ImageSharp.Sixels;

/// <summary>
/// Sixel terminal compatibility helpers.
/// </summary>
public static 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, sending the control code is slow.
/// </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,
};
}

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">The control sequence to send to the terminal.</param>
/// <returns>The response from the terminal.</returns>
private static string GetControlSequenceResponse(string controlSequence)
{
char? c;
var response = string.Empty;

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

return response;
}
}
65 changes: 65 additions & 0 deletions src/Extensions/Spectre.Console.ImageSharp/Sixels/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
namespace Spectre.Console.ImageSharp.Sixels;

/// <summary>
/// Sixel terminal compatibility helpers.
/// </summary>
public static class Constants
{
/// <summary>
/// The character to use when entering a terminal escape code sequence.
/// </summary>
public const string ESC = "\u001b";

/// <summary>
/// The character to indicate the start of a sixel color palette entry or to switch to a new color.
/// https://vt100.net/docs/vt3xx-gp/chapter14.html#S14.3.3.
/// </summary>
public const char SIXELCOLOR = '#';

/// <summary>
/// The character to use when a sixel is empty/transparent.
/// ? (hex 3F) represents the binary value 000000.
/// https://vt100.net/docs/vt3xx-gp/chapter14.html#S14.2.1.
/// </summary>
public const char SIXELEMPTY = '?';

/// <summary>
/// The character to use when entering a repeated sequence of a color in a sixel.
/// https://vt100.net/docs/vt3xx-gp/chapter14.html#S14.3.1.
/// </summary>
public const char SIXELREPEAT = '!';

/// <summary>
/// The character to use when moving to the next line in a sixel.
/// https://vt100.net/docs/vt3xx-gp/chapter14.html#S14.3.5.
/// </summary>
public 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.
/// https://vt100.net/docs/vt3xx-gp/chapter14.html#S14.3.4.
/// </summary>
public const char SIXELDECGCR = '$';

/// <summary>
/// The start of a sixel sequence.
/// https://vt100.net/docs/vt3xx-gp/chapter14.html#S14.2.1.
/// </summary>
public const string SIXELSTART = $"{ESC}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.
/// https://vt100.net/docs/vt3xx-gp/chapter14.html#S14.3.2.
/// </summary>
public const string SIXELRASTERATTRIBUTES = "\"1;1;";

/// <summary>
/// The end of a sixel sequence.
/// </summary>
public const string SIXELEND = $"{ESC}\\";

/// <summary>
/// The transparent color for the sixel, this is black but the sixel should be transparent so this is not visible.
/// </summary>
public const string SIXELTRANSPARENTCOLOR = "#0;2;0;0;0";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Spectre.Console.ImageSharp.Sixels.Models;

/// <summary>
/// Represents the size of a cell in pixels for sixel rendering.
/// </summary>
public class CellSize
{
/// <summary>
/// Gets the width of a cell in pixels.
/// </summary>
public int PixelWidth { get; init; }

/// <summary>
/// Gets the height of a cell in pixels.
/// </summary>
public int PixelHeight { get; init; }
}
41 changes: 41 additions & 0 deletions src/Extensions/Spectre.Console.ImageSharp/Sixels/Models/Sixel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Spectre.Console.ImageSharp.Sixels.Models;

/// <summary>
/// Represents the size of a cell in pixels for sixel rendering.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="Sixel"/> class.
/// </remarks>
/// <param name="pixelWidth">The width of a sixel image in pixels.</param>
/// <param name="pixelHeight">The height of a sixel image in pixels.</param>
/// <param name="cellHeight">The height of a sixel image in terminal cells.</param>
/// <param name="cellWidth">The width of a sixel image in terminal cells.</param>
/// <param name="sixelString">The Sixel string.</param>
public class Sixel(int pixelWidth, int pixelHeight, int cellHeight, int cellWidth, string sixelString)
{
/// <summary>
/// Gets the width of a sixel image in pixels.
/// </summary>
public int PixelWidth { get; init; } = pixelWidth;

/// <summary>
/// Gets the height of a sixel image in pixels.
/// </summary>
public int PixelHeight { get; init; } = pixelHeight;

/// <summary>
/// Gets the height of a sixel image in terminal cells.
/// </summary>
public int CellHeight { get; init; } = cellHeight;

/// <summary>
/// Gets the width of a sixel image in terminal cells.
/// </summary>
public int CellWidth { get; init; } = cellWidth;

/// <summary>
/// Gets the Sixel string.
/// </summary>
/// <returns>The Sixel string.</returns>
public string SixelString { get; init; } = sixelString;
}
Loading
Loading