Skip to content

Commit 6422b5b

Browse files
committed
playing around with gifs
1 parent 38abd36 commit 6422b5b

File tree

5 files changed

+246
-5
lines changed

5 files changed

+246
-5
lines changed

module/Sixel.psd1

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
Author = 'trackd, ShaunLawrie'
1212
Copyright = '(c) trackd. All rights reserved.'
1313
Description = 'Convert images to Sixel format and display them in the terminal, Requires a terminal with Sixel support. (Windows Terminal v1.22.2912.0 or later), Inline Image Protocol or Kitty Graphics support.'
14-
CmdletsToExport = @('ConvertTo-Sixel')
14+
CmdletsToExport = @('ConvertTo-Sixel', 'New-SixelGif','Show-SixelGif')
1515
AliasesToExport = @('cts', 'ConvertTo-InlineImage')
1616
PrivateData = @{
1717
PSData = @{
@@ -20,9 +20,9 @@
2020
'Terminal',
2121
'Graphics',
2222
'Image',
23-
'Console Image',
24-
'Inline Image Protocol',
25-
'Kitty Graphics Protocol'
23+
'ConsoleImage',
24+
'InlineImageProtocol',
25+
'KittyGraphicsProtocol'
2626
)
2727
LicenseUri = 'https://github.com/trackd/Sixel/blob/main/LICENSE'
2828
ProjectUri = 'https://github.com/trackd/Sixel'

src/Sixel/Cmdlet/SixelGifcmdlet.cs

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using Sixel.Terminal;
2+
using Sixel.Terminal.Models;
3+
using Sixel.Protocols;
4+
using System.Management.Automation;
5+
6+
namespace Sixel.Cmdlet;
7+
8+
[Cmdlet(VerbsCommon.New, "SixelGif", DefaultParameterSetName = "Path")]
9+
[Alias("gif")]
10+
[OutputType(typeof(SixelGif))]
11+
public sealed class NewSixelGifCmdlet : PSCmdlet
12+
{
13+
[Parameter(
14+
HelpMessage = "A path to a local image to convert to sixel.",
15+
Mandatory = true,
16+
ValueFromPipelineByPropertyName = true,
17+
Position = 0,
18+
ParameterSetName = "Path"
19+
)]
20+
[ValidateNotNullOrEmpty]
21+
[Alias("FullName")]
22+
public string Path { get; set; } = null!;
23+
24+
[Parameter(
25+
HelpMessage = "A URL of the image to download and convert to sixel.",
26+
Mandatory = true,
27+
ValueFromPipeline = true,
28+
ParameterSetName = "Url"
29+
)]
30+
[ValidateNotNullOrEmpty]
31+
[Alias("Uri")]
32+
public string Url { get; set; } = null!;
33+
34+
[Parameter(
35+
HelpMessage = "The maximum number of colors to use in the image."
36+
)]
37+
[ValidateRange(1, 256)]
38+
public int MaxColors { get; set; } = 256;
39+
40+
[Parameter(
41+
HelpMessage = "Width of the image in character cells, the height will be scaled to maintain aspect ratio."
42+
)]
43+
[ValidateTerminalWidth()]
44+
public int Width { get; set; }
45+
46+
[Parameter(
47+
HelpMessage = "Force the command to attempt to output sixel data even if the terminal does not support sixel."
48+
)]
49+
public SwitchParameter Force { get; set; }
50+
51+
[Parameter(
52+
HelpMessage = "The number of times to loop the gif."
53+
)]
54+
[ValidateRange(1, 256)]
55+
public int LoopCount { get; set; } = 3;
56+
protected override void ProcessRecord()
57+
{
58+
try
59+
{
60+
Stream? imageStream = null;
61+
switch (ParameterSetName)
62+
{
63+
case "Path":
64+
{
65+
var resolvedPath = SessionState.Path.GetResolvedPSPathFromPSPath(Path)[0].Path;
66+
imageStream = new FileStream(resolvedPath, FileMode.Open, FileAccess.Read);
67+
break;
68+
}
69+
case "Url":
70+
{
71+
using var client = new HttpClient();
72+
var response = client.GetAsync(Url).Result;
73+
response.EnsureSuccessStatusCode();
74+
imageStream = response.Content.ReadAsStream();
75+
break;
76+
}
77+
}
78+
if (imageStream is null) return;
79+
using (imageStream)
80+
{
81+
WriteObject(GifToSixel.LoadGif(imageStream, MaxColors, Width, LoopCount));
82+
}
83+
}
84+
catch (Exception ex)
85+
{
86+
WriteError(new ErrorRecord(ex, "SixelError", ErrorCategory.NotSpecified, null));
87+
}
88+
}
89+
}
90+
91+
[Cmdlet(VerbsCommon.Show, "SixelGif", DefaultParameterSetName = "Path")]
92+
[Alias("play")]
93+
public sealed class ShowSixelGifCmdlet : PSCmdlet
94+
{
95+
[Parameter(
96+
HelpMessage = "SixelGif object to play.",
97+
Mandatory = true,
98+
ValueFromPipeline = true,
99+
Position = 0
100+
)]
101+
[ValidateNotNullOrEmpty]
102+
public SixelGif? Gif { get; set; }
103+
104+
[Parameter(
105+
HelpMessage = "The number of times to loop the gif."
106+
)]
107+
[ValidateRange(1, 256)]
108+
public int LoopCount { get; set; } = 0;
109+
protected override void ProcessRecord()
110+
{
111+
try
112+
{
113+
if (Gif is null) return;
114+
if (LoopCount > 0)
115+
{
116+
GifToSixel.PlaySixelGif(Gif, LoopCount);
117+
}
118+
else
119+
{
120+
GifToSixel.PlaySixelGif(Gif);
121+
}
122+
}
123+
catch (Exception ex)
124+
{
125+
WriteError(new ErrorRecord(ex, "SixelError", ErrorCategory.NotSpecified, null));
126+
}
127+
}
128+
}

src/Sixel/Protocols/Sixel.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public static string ImageToSixel(Image<Rgba32> image, int maxColors, int cellWi
4242
var targetFrame = image.Frames[frame];
4343
return FrameToSixelString(targetFrame, returnCursorToTopLeft);
4444
}
45-
private static string FrameToSixelString(ImageFrame<Rgba32> frame, bool returnCursorToTopLeft)
45+
internal static string FrameToSixelString(ImageFrame<Rgba32> frame, bool returnCursorToTopLeft)
4646
{
4747
var sixelBuilder = new StringBuilder();
4848
var palette = new Dictionary<Rgba32, int>();

src/Sixel/Protocols/gif.cs

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using Sixel.Terminal;
2+
using Sixel.Terminal.Models;
3+
using System.Text;
4+
using SixLabors.ImageSharp;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
using SixLabors.ImageSharp.Processing;
7+
using SixLabors.ImageSharp.Processing.Processors.Quantization;
8+
9+
namespace Sixel.Protocols;
10+
public static class GifToSixel {
11+
public static SixelGif LoadGif(Stream imageStream, int maxColors, int cellWidth, int LoopCount)
12+
{
13+
var image = Image.Load<Rgba32>(imageStream);
14+
var gif = ConvertGifToSixel(image, maxColors, cellWidth, LoopCount);
15+
return gif;
16+
}
17+
private static SixelGif ConvertGifToSixel(Image<Rgba32> image, int maxColors, int cellWidth, int LoopCount)
18+
{
19+
image.Mutate(ctx =>
20+
{
21+
if (cellWidth > 0)
22+
{
23+
// Some math to get the target size in pixels and reverse it to cell height that it will consume.
24+
var pixelWidth = cellWidth * Compatibility.GetCellSize().PixelWidth;
25+
var pixelHeight = (int)Math.Round((double)image.Height / image.Width * pixelWidth);
26+
// Resize the image to the target size
27+
ctx.Resize(new ResizeOptions()
28+
{
29+
Sampler = KnownResamplers.Bicubic,
30+
Size = new(pixelWidth, pixelHeight),
31+
PremultiplyAlpha = false,
32+
});
33+
}
34+
// Sixel supports 256 colors max
35+
ctx.Quantize(new OctreeQuantizer(new()
36+
{
37+
MaxColors = maxColors,
38+
}));
39+
});
40+
var metadata = image.Frames.RootFrame.Metadata.GetGifMetadata();
41+
int frameCount = image.Frames.Count;
42+
var cellHeight = Math.Ceiling((double)(image.Height / Compatibility.GetCellSize().PixelHeight));
43+
var gif = new SixelGif()
44+
{
45+
Sixel = new List<string>(),
46+
Delay = metadata?.FrameDelay * 10 ?? 1000,
47+
LoopCount = LoopCount,
48+
Height = (int)cellHeight
49+
};
50+
for (int i = 0; i < frameCount; i++)
51+
{
52+
var targetFrame = image.Frames[i];
53+
gif.Sixel.Add(Sixel.FrameToSixelString(targetFrame, false));
54+
}
55+
return gif;
56+
}
57+
public static void PlaySixelGif(SixelGif gif, int LoopCount = 0)
58+
{
59+
(int positionX, int positionY) = Console.GetCursorPosition();
60+
if (LoopCount > 0)
61+
{
62+
gif.LoopCount = LoopCount;
63+
}
64+
if (gif.Delay == 0)
65+
{
66+
gif.Delay = 1000;
67+
}
68+
Console.CursorVisible = false;
69+
for (int i = 0; i < gif.LoopCount; i++)
70+
{
71+
foreach (var sixel in gif.Sixel)
72+
{
73+
Console.SetCursorPosition(positionX, positionY);
74+
Console.Write(sixel);
75+
Thread.Sleep(gif.Delay);
76+
}
77+
}
78+
Console.CursorVisible = true;
79+
int endPositionY = positionY + gif.Height;
80+
Console.SetCursorPosition(positionX, positionY);
81+
}
82+
}

src/Sixel/Terminal/Models/SixelGif.cs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Collections.Generic;
2+
using Sixel.Protocols;
3+
namespace Sixel.Terminal.Models;
4+
/// <summary>
5+
/// Gif in sixel format.
6+
/// </summary>
7+
public class SixelGif
8+
{
9+
/// <summary>
10+
/// The sixel data for each frame of the gif.
11+
/// </summary>
12+
public List<string> Sixel { get; set; } = new List<string>();
13+
/// <summary>
14+
/// The delay in milliseconds between each frame.
15+
/// </summary>
16+
public int Delay { get; set; }
17+
/// <summary>
18+
/// The number of times the gif should loop.
19+
/// </summary>
20+
public int LoopCount { get; set; }
21+
22+
public int Height { get; set; }
23+
/// <summary>
24+
/// The audio data for the gif.
25+
/// </summary>
26+
// public string? Audio { get; set; }
27+
// public static string ToString()
28+
// {
29+
// GifToSixel.PlaySixelGif(this);
30+
// }
31+
}

0 commit comments

Comments
 (0)