using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.Text; using VPlasma.Core.Device; using VPlasma.Core.Protocol; namespace VPlasma.Wire; /// /// Plasma bitmap authoring — ported from TeslaSuite's PlasmaBitmaps /// (the console's Plasma Font Tool / Bitmap Decoder / Editor). Renders text in /// a Windows font to a 1bpp panel bitmap, converts bitmaps to and from the /// game's bitmap= hex encoding, and bridges either to an ESC P /// wire stream the display (or vPLASMA / the replica) can show. /// internal static class PlasmaBitmap { public const string DefaultFont = "Microsoft Sans Serif"; /// /// Render centered in a × /// 1bpp image, auto-shrinking the font to fit — /// the Plasma Font Tool's behavior. /// public static Bitmap RenderText(int width, int height, string fontName, string text) { var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb); using var g = Graphics.FromImage(bmp); g.SmoothingMode = SmoothingMode.None; g.InterpolationMode = InterpolationMode.NearestNeighbor; g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; g.Clear(Color.Black); var rect = new Rectangle(0, 0, width, height); using var fmt = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; for (int size = 24; ; size = size <= 2 ? size / 2 : size - 2) { using var font = new Font(fontName, Math.Max(1, size)); SizeF sz = g.MeasureString(text, font); if (size <= 1 || (sz.Width <= width && sz.Height <= height)) { g.DrawString(text, font, Brushes.White, rect, fmt); return bmp; } } } /// Threshold an image to a × on/off grid. public static bool[,] ToGrid(Bitmap src, int w, int h, double threshold) { using var scaled = new Bitmap(w, h, PixelFormat.Format24bppRgb); using (var g = Graphics.FromImage(scaled)) { g.InterpolationMode = src.Width == w && src.Height == h ? InterpolationMode.NearestNeighbor : InterpolationMode.HighQualityBicubic; g.DrawImage(src, 0, 0, w, h); } var grid = new bool[w, h]; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) grid[x, y] = scaled.GetPixel(x, y).GetBrightness() >= threshold; return grid; } /// The game's bitmap= hex encoding (MSB-first, 8 px/byte). public static string ToHexEncoding(bool[,] grid) { int w = grid.GetLength(0), h = grid.GetLength(1); if (w % 8 != 0) throw new ArgumentException("width must be a multiple of 8"); var sb = new StringBuilder(); for (int y = 0; y < h; y++) { sb.Append("bitmap="); for (int j = 0; j < w / 8; j++) { byte b = 0; for (int k = 0; k < 8; k++) if (grid[j * 8 + k, y]) b |= (byte)(0x80 >> k); sb.Append(b.ToString("X2")); } sb.Append('\n'); } sb.Append($"x={w}\n"); sb.Append($"y={h}\n"); return sb.ToString(); } /// Parse a bitmap=/x=/y= encoding back to a grid. public static bool[,] FromHexEncoding(string text) { var rows = new List(); int declaredX = 0, declaredY = 0; foreach (string raw in text.Split('\n')) { string line = raw.Trim(); if (line.StartsWith("bitmap=", StringComparison.OrdinalIgnoreCase)) { string hex = line.Substring("bitmap=".Length).Trim(); var bytes = new byte[hex.Length / 2]; for (int i = 0; i < bytes.Length; i++) bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16); rows.Add(bytes); } else if (line.StartsWith("x=", StringComparison.OrdinalIgnoreCase)) int.TryParse(line.Substring(2), out declaredX); else if (line.StartsWith("y=", StringComparison.OrdinalIgnoreCase)) int.TryParse(line.Substring(2), out declaredY); } if (rows.Count == 0) throw new ArgumentException("no 'bitmap=' rows found"); int w = declaredX > 0 ? declaredX : rows[0].Length * 8; int h = declaredY > 0 ? declaredY : rows.Count; var grid = new bool[w, h]; for (int y = 0; y < h && y < rows.Count; y++) for (int j = 0; j < rows[y].Length && j * 8 < w; j++) for (int k = 0; k < 8 && j * 8 + k < w; k++) grid[j * 8 + k, y] = (rows[y][j] & (0x80 >> k)) != 0; return grid; } /// /// Pack a grid into an ESC P wire stream (clear + cursor-off + one /// full-width row per line), the way L4PLASMA streams a frame. /// public static byte[] ToEscP(bool[,] grid) { int w = grid.GetLength(0), h = grid.GetLength(1); int wbytes = (w + 7) / 8; var b = new List { PlasmaProtocol.Esc, PlasmaProtocol.CmdCursorMode, 0x00, // hide cursor PlasmaProtocol.Esc, PlasmaProtocol.CmdClearScreen, }; for (int y = 0; y < h; y++) { b.Add(PlasmaProtocol.Esc); b.Add(PlasmaProtocol.CmdGraphicsWrite); b.Add(0); // screen b.Add((byte)y); // y b.Add(0); // x byte b.Add((byte)wbytes); // width in bytes b.Add(1); // one row for (int j = 0; j < wbytes; j++) { byte v = 0; for (int k = 0; k < 8; k++) if (j * 8 + k < w && grid[j * 8 + k, y]) v |= (byte)(0x80 >> k); b.Add(v); } } return b.ToArray(); } /// Render a grid to a plasma-orange PNG (for previews / decoding). public static void SaveGridPng(bool[,] grid, string path, int scale) { int w = grid.GetLength(0), h = grid.GetLength(1); int pitch = scale, dot = Math.Max(1, scale - 2); using var bmp = new Bitmap(w * pitch, h * pitch, PixelFormat.Format24bppRgb); using var g = Graphics.FromImage(bmp); g.Clear(Color.FromArgb(26, 14, 6)); using var unlit = new SolidBrush(Color.FromArgb(46, 24, 10)); using var lit = new SolidBrush(Color.FromArgb(255, 106, 26)); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) g.FillRectangle(grid[x, y] ? lit : unlit, x * pitch, y * pitch, dot, dot); bmp.Save(path, ImageFormat.Png); } /// Reduce a rendered vPLASMA frame (flag bytes) to an on/off grid. public static bool[,] FrameToGrid(byte[] frame) { var grid = new bool[VPlasmaDevice.Width, VPlasmaDevice.Height]; for (int y = 0; y < VPlasmaDevice.Height; y++) for (int x = 0; x < VPlasmaDevice.Width; x++) grid[x, y] = (frame[y * VPlasmaDevice.Width + x] & VPlasmaDevice.PixelLit) != 0; return grid; } }