Skip to content

Commit

Permalink
refactor: update half block character handling
Browse files Browse the repository at this point in the history
  • Loading branch information
trackd committed Dec 2, 2024
1 parent 149b405 commit d19e9c0
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 141 deletions.
146 changes: 146 additions & 0 deletions src/Sixel/Protocols/HalfBlock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using Sixel.Terminal;
using Sixel.Terminal.Models;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;

namespace Sixel.Protocols;

public static class HalfBlock
{
private static Rgba32 _backgroundColor;
public static string ImageToAscii(Image<Rgba32> image, int cellWidth, int frame = 0, bool returnCursorToTopLeft = false)
{
_backgroundColor = GetConsoleBackgroundColor();
var cellSize = Compatibility.GetCellSize();
var maxWidth = Console.WindowWidth - 2;
var characterWidth = (int)Math.Ceiling((double)image.Width / cellSize.PixelWidth);
var characterHeight = (int)Math.Ceiling((double)image.Height / cellSize.PixelHeight);

if (cellWidth <= 0 || cellWidth > maxWidth)
{
cellWidth = Math.Min(characterWidth, maxWidth);
}
image.Mutate(ctx =>
{
// Calculate target size in pixels based on character dimensions
var pixelHeight = (int)Math.Round((double)image.Height / image.Width * cellWidth);
ctx.Resize(new ResizeOptions
{
Size = new Size(cellWidth, pixelHeight),
Sampler = KnownResamplers.Bicubic,
PremultiplyAlpha = false
});
});
var size = new Size(image.Width, image.Height);
var targetFrame = image.Frames[frame];
return ProcessFrame(targetFrame, size, returnCursorToTopLeft);
}
private static string ProcessFrame(ImageFrame<Rgba32> frame, Size size, bool returnCursorToTopLeft)
{
var _buffer = new StringBuilder();

for (int y = 0; y < size.Height; y += 2)
{
if (y + 1 >= size.Height)
{
_buffer.AppendLine();
break;
}

for (int x = 0; x < size.Width; x++)
{
var topPixel = frame[x, y];
var bottomPixel = frame[x, y + 1];

_buffer.ProcessPixelPairs(topPixel, bottomPixel);
}
_buffer.AppendLine();
}
if (returnCursorToTopLeft)
{

// Move the cursor back to the top left of the image.
_buffer.Append($"{Constants.ESC}[{size.Height}A");
}
return _buffer.ToString();
}
private static void ProcessPixelPairs(this StringBuilder _buffer, Rgba32 top, Rgba32 bottom)
{
bool topTransparent = IsTransparent(top);
bool bottomTransparent = IsTransparent(bottom);

if (topTransparent && bottomTransparent)
{
// Both pixels are transparent
_buffer.Append(' ');
}
else if (topTransparent)
{
// Only bottom pixel is opaque, use lower half block
var bottomRgb = BlendPixels(bottom);
_buffer.Append($"{Constants.ESC}{Constants.VTFG}{bottomRgb.R};{bottomRgb.G};{bottomRgb.B}m{Constants.LowerHalfBlock}{Constants.ESC}[0m");
}
else if (bottomTransparent)
{
// Only top pixel is opaque, use upper half block
var topRgb = BlendPixels(top);
_buffer.Append($"{Constants.ESC}{Constants.VTFG}{topRgb.R};{topRgb.G};{topRgb.B}m{Constants.UpperHalfBlock}{Constants.ESC}[0m");
}
else
{
// Both pixels are opaque, set foreground and background colors, use upper half block
var topRgb = BlendPixels(top);
var bottomRgb = BlendPixels(bottom);
_buffer.Append($"{Constants.ESC}{Constants.VTFG}{topRgb.R};{topRgb.G};{topRgb.B}m");
_buffer.Append($"{Constants.ESC}{Constants.VTBG}{bottomRgb.R};{bottomRgb.G};{bottomRgb.B}m{Constants.UpperHalfBlock}{Constants.ESC}[0m");
}
}

private static (byte R, byte G, byte B) BlendPixels(Rgba32 pixel)
{
// fast path for fully transparent pixels
if (pixel.A == 0)
{
return (pixel.R, pixel.G, pixel.B);
}
// var foregroundMultiplier = pixel.A / 255;
// var backgroundMultiplier = 100 - foregroundMultiplier;
// (byte)(pixel.R * foregroundMultiplier + _backgroundColor.R * backgroundMultiplier);
float amount = pixel.A / 255f;

byte r = (byte)(pixel.R * amount + _backgroundColor.R * (1 - amount));
byte g = (byte)(pixel.G * amount + _backgroundColor.G * (1 - amount));
byte b = (byte)(pixel.B * amount + _backgroundColor.B * (1 - amount));

return (r, g, b);
}
private static bool IsTransparent(Rgba32 pixel) => pixel.A < 5;

private static Rgba32 GetConsoleBackgroundColor()
{
var color = Console.BackgroundColor switch
{
ConsoleColor.Black => Color.FromRgb(0, 0, 0),
ConsoleColor.Blue => Color.FromRgb(0, 0, 170),
ConsoleColor.Cyan => Color.FromRgb(0, 170, 170),
ConsoleColor.DarkBlue => Color.FromRgb(0, 0, 85),
ConsoleColor.DarkCyan => Color.FromRgb(0, 85, 85),
ConsoleColor.DarkGray => Color.FromRgb(85, 85, 85),
ConsoleColor.DarkGreen => Color.FromRgb(0, 85, 0),
ConsoleColor.DarkMagenta => Color.FromRgb(85, 0, 85),
ConsoleColor.DarkRed => Color.FromRgb(85, 0, 0),
ConsoleColor.DarkYellow => Color.FromRgb(85, 85, 0),
ConsoleColor.Gray => Color.FromRgb(170, 170, 170),
ConsoleColor.Green => Color.FromRgb(0, 170, 0),
ConsoleColor.Magenta => Color.FromRgb(170, 0, 170),
ConsoleColor.Red => Color.FromRgb(170, 0, 0),
ConsoleColor.White => Color.FromRgb(255, 255, 255),
ConsoleColor.Yellow => Color.FromRgb(170, 170, 0),
_ => Color.FromRgb(0, 0, 0)
};
return color.ToPixel<Rgba32>();
}
}
138 changes: 0 additions & 138 deletions src/Sixel/Protocols/ascii.cs

This file was deleted.

27 changes: 25 additions & 2 deletions src/Sixel/Terminal/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,46 @@ internal static class Constants
/// Kitty raster
/// </summary>
internal const string KittyPos = "a=T,f=100,";

/// <summary>
/// Kitty more chunks
/// </summary>
internal const string KittyMore = "m=1";

/// <summary>
/// Kitty last chunk
/// </summary>
internal const string KittyFinish = "m=0";

/// <summary>
/// Kitty chunksize
/// </summary>
internal const int KittychunkSize = 4096;

/// <summary>
/// half block character
/// lower half block character
/// ▄
/// this allows you to color the top and bottom of a cell.
/// foreground colors the lower block and background colors the space above the block in the same cell.
/// </summary>
///
internal const char LowerHalfBlock = '\u2584';

/// <summary>
/// upper half block character
/// ▀
/// this allows you to color the top and bottom of a cell.
/// foreground colors the upper block and background colors the space below the block in the same cell.
/// </summary>
internal const char UpperHalfBlock = '\u2580';

/// <summary>
/// background color escape sequence
/// </summary>
internal const string VTBG = "[48;2;";

/// <summary>
/// foreground color escape sequence
/// </summary>
internal const string VTFG = "[38;2;";

}
2 changes: 1 addition & 1 deletion src/Sixel/Terminal/Load.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static class Load
if (imageProtocol == ImageProtocol.None)
{
using var _image = Image.Load<Rgba32>(imageStream);
return Protocols.HalfBlockCell.ImageToAscii(_image, width);
return Protocols.HalfBlock.ImageToAscii(_image, width);
}
return null;
}
Expand Down

0 comments on commit d19e9c0

Please sign in to comment.