Multi-platform .NET bindings to the Ultralight project.
1using System;
2using System.IO;
3using System.Text;
4using SixLabors.ImageSharp;
5using SixLabors.ImageSharp.PixelFormats;
6using SixLabors.ImageSharp.Processing;
7using SixLabors.ImageSharp.Processing.Processors.Transforms;
8
9public static class Utilities {
10
11 public static string LoadEmbeddedUtf8String(string resName) {
12 using var stream = typeof(Utilities).Assembly.GetManifestResourceStream(resName);
13 if (stream == null) throw new FileNotFoundException($"Missing embedded resource: {resName}");
14
15 return LoadString(stream);
16 }
17
18 public static string LoadString(Stream stream) {
19 using var sr = new StreamReader(stream, Encoding.UTF8, false, 16, true);
20 return sr.ReadToEnd();
21 }
22
23 public static unsafe void RenderAnsi<TColor>(void* pixels,
24 uint w, uint h,
25 uint reduceLineCount = 0, int maxLineCount = -1, int maxWidth = -1,
26 bool borderless = false
27 ) where TColor : unmanaged, IPixel<TColor> {
28 var aspect = w / (double) h;
29
30 // get the console size
31 // @formatter:off
32 var cw = 0;
33 if (maxWidth < 0)
34 try { cw = Console.WindowWidth; } catch { cw = 72; }
35 else
36 cw = maxWidth;
37
38 var ch = 0;
39 if (maxLineCount < 0)
40 try { ch = Console.WindowHeight; } catch { ch = 25; }
41 else
42 ch = maxLineCount;
43 // @formatter:on
44
45 cw -= 1;
46 ch -= (int) reduceLineCount;
47
48 if (cw <= 0 || ch <= 0)
49 return;
50
51 // come up with an aperture that fits the console window (minus drawn borders, accounting for 2v/c)
52 var borderCost = borderless ? 0 : -2;
53 var wsq = borderCost + cw / aspect;
54 var hsq = borderCost + (ch * 2) * aspect;
55 var sq = Math.Min(wsq, hsq);
56
57 var aw = (int) Math.Floor(sq * aspect);
58 var ah = (int) Math.Floor(sq / aspect);
59
60 var pPixels = (byte*) pixels;
61 var span = new ReadOnlySpan<TColor>(pPixels, checked((int) (w * h)));
62 using var img = Image.LoadPixelData(span, (int) w, (int) h);
63 img.Mutate(x => x
64 .Resize(aw, ah, LanczosResampler.Lanczos3)
65 .Crop(aw, ah));
66 using var stdOut = Console.OpenStandardOutput(256);
67 using var o = new BufferedStream(stdOut, (42 * aw * (ah / 2)) + 1);
68
69 void WriteTriplet(byte b) {
70 var ones = b % 10;
71 var tens = b / 10 % 10;
72 var hundreds = b / 100;
73 var anyHundreds = hundreds > 0;
74 if (anyHundreds)
75 o!.WriteByte((byte) ('0' + hundreds));
76 if (anyHundreds || tens > 0)
77 o!.WriteByte((byte) ('0' + tens));
78 o!.WriteByte((byte) ('0' + ones));
79 }
80
81 // ╭
82 void DrawTopLeftCorner() {
83 o.WriteByte(0xE2);
84 o.WriteByte(0x95);
85 o.WriteByte(0xAD);
86 }
87
88 // ╮
89 void DrawTopRightCorner() {
90 o.WriteByte(0xE2);
91 o.WriteByte(0x95);
92 o.WriteByte(0xAE);
93 }
94
95 // ╰
96 void DrawBottomLeftCorner() {
97 o.WriteByte(0xE2);
98 o.WriteByte(0x95);
99 o.WriteByte(0xB0);
100 }
101
102 // ╯
103 void DrawBottomRightCorner() {
104 o.WriteByte(0xE2);
105 o.WriteByte(0x95);
106 o.WriteByte(0xAF);
107 }
108
109 // ─ x width
110 void DrawHorizontalFrame(int width) {
111 for (var i = 0; i < width; ++i) {
112 o.WriteByte(0xE2);
113 o.WriteByte(0x94);
114 o.WriteByte(0x80);
115 }
116 }
117
118 // │
119 void DrawVerticalFrame() {
120 o.WriteByte(0xE2);
121 o.WriteByte(0x94);
122 o.WriteByte(0x82);
123 }
124
125 if (!borderless) {
126 DrawTopLeftCorner();
127 DrawHorizontalFrame(aw + 1);
128 DrawTopRightCorner();
129 o.WriteByte((byte) '\n');
130 }
131
132 var lastY = ah & ~ 1;
133 for (var y = 0; y < lastY; y += 2) {
134 if (!borderless)
135 DrawVerticalFrame();
136 // write 2 lines at a time
137 var u = img.GetPixelRowSpan(y);
138 var haveL = y + 1 < ah;
139 var l = haveL
140 ? img.GetPixelRowSpan(y + 1)
141 : new Span<TColor>(default, 0);
142
143 for (var x = 0; x < aw; ++x) {
144 // upper color
145 Rgba32 upx = default;
146 u[x].ToRgba32(ref upx);
147 var ua = 255.0f / upx.A;
148 o.WriteByte(0x1B);
149 o.WriteByte((byte) '[');
150 o.WriteByte((byte) '3');
151 o.WriteByte((byte) '8');
152 o.WriteByte((byte) ';');
153 o.WriteByte((byte) '2');
154 o.WriteByte((byte) ';');
155 WriteTriplet((byte) MathF.Round(upx.R * ua, MidpointRounding.AwayFromZero));
156 o.WriteByte((byte) ';');
157 WriteTriplet((byte) MathF.Round(upx.G * ua, MidpointRounding.AwayFromZero));
158 o.WriteByte((byte) ';');
159 WriteTriplet((byte) MathF.Round(upx.B * ua, MidpointRounding.AwayFromZero));
160 o.WriteByte((byte) 'm');
161
162 if (!haveL) // full block
163 o.WriteByte(0xDB);
164
165 else {
166 // lower color
167 Rgba32 lpx = default;
168 l[x].ToRgba32(ref lpx);
169 var la = 255.0f / lpx.A;
170 o.WriteByte(0x1B);
171 o.WriteByte((byte) '[');
172 o.WriteByte((byte) '4');
173 o.WriteByte((byte) '8');
174 o.WriteByte((byte) ';');
175 o.WriteByte((byte) '2');
176 o.WriteByte((byte) ';');
177 WriteTriplet((byte) Math.Round(lpx.R * la, MidpointRounding.AwayFromZero));
178 o.WriteByte((byte) ';');
179 WriteTriplet((byte) Math.Round(lpx.G * la, MidpointRounding.AwayFromZero));
180 o.WriteByte((byte) ';');
181 WriteTriplet((byte) Math.Round(lpx.B * la, MidpointRounding.AwayFromZero));
182 o.WriteByte((byte) 'm');
183
184 // half block
185 o.WriteByte(0xE2);
186 o.WriteByte(0x96);
187 o.WriteByte(0x80);
188 }
189 }
190
191 o.WriteByte(0x1B);
192 o.WriteByte((byte) '[');
193 o.WriteByte((byte) '0');
194 o.WriteByte((byte) 'm');
195 o.WriteByte((byte) ' ');
196 if (!borderless)
197 DrawVerticalFrame();
198 o.WriteByte((byte) '\n');
199 }
200
201 if (!borderless) {
202 DrawBottomLeftCorner();
203 DrawHorizontalFrame(aw + 1);
204 DrawBottomRightCorner();
205 o.WriteByte((byte) '\n');
206 }
207
208 o.Flush();
209 stdOut.Flush();
210 }
211
212}