|
| 1 | +// Copyright Kindel Systems, LLC - http://www.kindel.com |
| 2 | +// Published under the MIT License at https://github.com/tig/winprint |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Drawing; |
| 6 | +using System.IO; |
| 7 | +using System.Runtime.InteropServices; |
| 8 | +using System.Text; |
| 9 | +using System.Threading.Tasks; |
| 10 | +using libvt100; |
| 11 | +using Serilog; |
| 12 | +using WinPrint.Core.Models; |
| 13 | +using WinPrint.Core.Services; |
| 14 | +using static libvt100.Screen; |
| 15 | + |
| 16 | +namespace WinPrint.Core.ContentTypeEngines { |
| 17 | + |
| 18 | + /// <summary> |
| 19 | + /// Implements text/plain file type support. |
| 20 | + /// </summary> |
| 21 | + public class AnsiCte : ContentTypeEngineBase, IDisposable { |
| 22 | + private static readonly string _contentType = "text/ansi"; |
| 23 | + /// <summary> |
| 24 | + /// ContentType identifier (shorthand for class name). |
| 25 | + /// </summary> |
| 26 | + public override string ContentTypeEngineName => _contentType; |
| 27 | + |
| 28 | + public static AnsiCte Create() { |
| 29 | + var engine = new AnsiCte(); |
| 30 | + // Populate it with the common settings |
| 31 | + engine.CopyPropertiesFrom(ModelLocator.Current.Settings.TextContentTypeEngineSettings); |
| 32 | + return engine; |
| 33 | + } |
| 34 | + |
| 35 | + // All of the lines of the text file, after reflow/line-wrap |
| 36 | + private DynamicScreen _screen; |
| 37 | + |
| 38 | + public IAnsiDecoderClient DecoderClient { get => (IAnsiDecoderClient)_screen; } |
| 39 | + |
| 40 | + private float _lineHeight; |
| 41 | + private int _linesPerPage; |
| 42 | + |
| 43 | + private float lineNumberWidth; |
| 44 | + private int _minLineLen; |
| 45 | + private System.Drawing.Font _cachedFont; |
| 46 | + |
| 47 | + public void Dispose() { |
| 48 | + Dispose(true); |
| 49 | + GC.SuppressFinalize(this); |
| 50 | + } |
| 51 | + |
| 52 | + // Protected implementation of Dispose pattern. |
| 53 | + // Flag: Has Dispose already been called? |
| 54 | + private bool _disposed = false; |
| 55 | + |
| 56 | + private void Dispose(bool disposing) { |
| 57 | + LogService.TraceMessage($"disposing: {disposing}"); |
| 58 | + |
| 59 | + if (_disposed) { |
| 60 | + return; |
| 61 | + } |
| 62 | + |
| 63 | + if (disposing) { |
| 64 | + if (_cachedFont != null) { |
| 65 | + _cachedFont.Dispose(); |
| 66 | + } |
| 67 | + |
| 68 | + _screen = null; |
| 69 | + } |
| 70 | + _disposed = true; |
| 71 | + } |
| 72 | + |
| 73 | + // TODO: Pass doc around by ref to save copies |
| 74 | + public override async Task<bool> SetDocumentAsync(string doc) { |
| 75 | + Document = doc; |
| 76 | + return await Task.FromResult(true); |
| 77 | + } |
| 78 | + |
| 79 | + /// <summary> |
| 80 | + /// Get total count of pages. Set any local page-size related values (e.g. linesPerPage). |
| 81 | + /// </summary> |
| 82 | + /// <param name="e"></param> |
| 83 | + /// <returns></returns> |
| 84 | + public override async Task<int> RenderAsync(System.Drawing.Printing.PrinterResolution printerResolution, EventHandler<string> reflowProgress) { |
| 85 | + LogService.TraceMessage(); |
| 86 | + |
| 87 | + if (document == null) { |
| 88 | + throw new ArgumentNullException("document can't be null for Render"); |
| 89 | + } |
| 90 | + |
| 91 | + var dpiX = printerResolution.X; |
| 92 | + var dpiY = printerResolution.Y; |
| 93 | + |
| 94 | + // BUGBUG: On Windows we can use the printer's resolution to be more accurate. But on Linux we |
| 95 | + // have to use 96dpi. See https://github.com/mono/libgdiplus/issues/623, etc... |
| 96 | + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || dpiX < 0 || dpiY < 0) { |
| 97 | + dpiX = dpiY = 96; |
| 98 | + } |
| 99 | + |
| 100 | + // Create a representative Graphcis used for determining glyph metrics. |
| 101 | + using var bitmap = new Bitmap(1, 1); |
| 102 | + bitmap.SetResolution(dpiX, dpiY); |
| 103 | + var g = Graphics.FromImage(bitmap); |
| 104 | + g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" |
| 105 | + |
| 106 | + // Calculate the number of lines per page; first we need our font. Keep it around. |
| 107 | + _cachedFont = new System.Drawing.Font(ContentSettings.Font.Family, ContentSettings.Font.Size / 72F * 96, ContentSettings.Font.Style, GraphicsUnit.Pixel); // World? |
| 108 | + Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, _cachedFont.Style); |
| 109 | + |
| 110 | + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { |
| 111 | + _cachedFont.Dispose(); |
| 112 | + _cachedFont = new System.Drawing.Font(ContentSettings.Font.Family, ContentSettings.Font.Size, ContentSettings.Font.Style, GraphicsUnit.Point); |
| 113 | + Log.Debug("Font: {f}, {s} ({p}), {st}", _cachedFont.Name, _cachedFont.Size, _cachedFont.SizeInPoints, _cachedFont.Style); |
| 114 | + g.PageUnit = GraphicsUnit.Display; // Display is 1/100th" |
| 115 | + } |
| 116 | + |
| 117 | + _lineHeight = _cachedFont.GetHeight(dpiY); |
| 118 | + |
| 119 | + if (PageSize.Height < _lineHeight) { |
| 120 | + throw new InvalidOperationException("The line height is greater than page height."); |
| 121 | + } |
| 122 | + |
| 123 | + // Round down # of lines per page to ensure lines don't clip on bottom |
| 124 | + _linesPerPage = (int)Math.Floor(PageSize.Height / _lineHeight); |
| 125 | + |
| 126 | + // 3 digits + 1 wide - Will support 999 lines before line numbers start to not fit |
| 127 | + // TODO: Make line number width dynamic |
| 128 | + // Note, MeasureString is actually dependent on lineNumberWidth! |
| 129 | + lineNumberWidth = ContentSettings.LineNumbers ? MeasureString(g, _cachedFont, new string('0', 4)).Width : 0; |
| 130 | + |
| 131 | + // This is the shortest line length (in chars) that we think we'll see. |
| 132 | + // This is used as a performance optimization (probably premature) and |
| 133 | + // could be 0 with no functional change. |
| 134 | + _minLineLen = (int)((PageSize.Width - lineNumberWidth) / MeasureString(g, _cachedFont, "W").Width); |
| 135 | + |
| 136 | + // Note, MeasureLines may increment numPages due to form feeds and line wrapping |
| 137 | + IAnsiDecoder _vt100 = new AnsiDecoder(); |
| 138 | + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); |
| 139 | + _screen = new DynamicScreen(_minLineLen); |
| 140 | + _vt100.Encoding = CodePagesEncodingProvider.Instance.GetEncoding("ibm437"); |
| 141 | + _vt100.Subscribe(_screen); |
| 142 | + |
| 143 | + var bytes = _vt100.Encoding.GetBytes(document); |
| 144 | + if (bytes != null && bytes.Length > 0) { |
| 145 | + _vt100.Input(bytes); |
| 146 | + } |
| 147 | + |
| 148 | +#if TESTVT100 |
| 149 | + _screen[_screen.Lines.Count][0] = new Character('0') { Attributes = new GraphicAttributes() { ForegroundColor = Color.Red } }; |
| 150 | + |
| 151 | + for (var x = 0; x < _screen.Width; x++) { |
| 152 | + var c = _screen[x, x]; |
| 153 | + if (c == null) c = new Character('*'); |
| 154 | + _screen[x,x] = new Character(c.Char) { Attributes = new GraphicAttributes() { ForegroundColor = Color.Red } }; |
| 155 | + } |
| 156 | + |
| 157 | + for (var x = 0; x < 20; x++) { |
| 158 | + _screen[11][x] = new Character((char)((int)'0' + x)) { Attributes = new GraphicAttributes() { |
| 159 | + Bold = true, |
| 160 | + ForegroundColor = Color.Red } }; |
| 161 | + } |
| 162 | + |
| 163 | + for (var x = 0; x < _screen.Width; x++) { |
| 164 | + var c = _screen[x,20]; |
| 165 | + if (c == null) c = new Character(' '); |
| 166 | + _screen[20][x] = new Character(c.Char) { |
| 167 | + Attributes = new GraphicAttributes() { |
| 168 | + Bold = true, |
| 169 | + ForegroundColor = Color.Green |
| 170 | + } |
| 171 | + }; |
| 172 | + } |
| 173 | + |
| 174 | + _screen[8][0] = new Character('_') { Attributes = new GraphicAttributes() { ForegroundColor = Color.Red } }; |
| 175 | + _screen[23][31] = new Character('>') { Attributes = new GraphicAttributes() { ForegroundColor = Color.Red } }; |
| 176 | + _screen[57][0] = new Character('{') { Attributes = new GraphicAttributes() { ForegroundColor = Color.Red } }; |
| 177 | + |
| 178 | + _screen.CursorPosition = new Point(0, 0); |
| 179 | + |
| 180 | + var w = new StreamWriter("PygmentsCte.txt"); |
| 181 | + w.Write(_screen); |
| 182 | + w.Close(); |
| 183 | +#endif |
| 184 | + |
| 185 | + |
| 186 | + var n = (int)Math.Ceiling(_screen.Lines.Count / (double)_linesPerPage); |
| 187 | + |
| 188 | + Log.Debug("Rendered {pages} pages of {linesperpage} lines per page, for a total of {lines} lines.", n, _linesPerPage, _screen.Lines.Count); |
| 189 | + return await Task.FromResult(n); |
| 190 | + } |
| 191 | + |
| 192 | + private SizeF MeasureString(Graphics g, System.Drawing.Font font, string text) { |
| 193 | + return MeasureString(g, text, font, out var charsFitted, out var linesFilled); |
| 194 | + } |
| 195 | + |
| 196 | + /// <summary> |
| 197 | + /// Measures how much width a string will take, given current page settings |
| 198 | + /// </summary> |
| 199 | + /// <param name="g"></param> |
| 200 | + /// <param name="text"></param> |
| 201 | + /// <param name="charsFitted"></param> |
| 202 | + /// <param name="linesFilled"></param> |
| 203 | + /// <returns></returns> |
| 204 | + private SizeF MeasureString(Graphics g, string text, System.Drawing.Font font, out int charsFitted, out int linesFilled) { |
| 205 | + if (g is null) { |
| 206 | + // define context used for determining glyph metrics. |
| 207 | + using var bitmap = new Bitmap(1, 1); |
| 208 | + g = Graphics.FromImage(bitmap); |
| 209 | + //g = Graphics.FromHwnd(PrintPreview.Instance.Handle); |
| 210 | + g.PageUnit = GraphicsUnit.Display; |
| 211 | + } |
| 212 | + |
| 213 | + g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; |
| 214 | + |
| 215 | + // determine width |
| 216 | + var fontHeight = _lineHeight; |
| 217 | + // Use page settings including lineNumberWidth |
| 218 | + var proposedSize = new SizeF(PageSize.Width, _lineHeight + (_lineHeight / 2)); |
| 219 | + var size = g.MeasureString(text, font, proposedSize, ContentTypeEngineBase.StringFormat, out charsFitted, out linesFilled); |
| 220 | + |
| 221 | + // TODO: HACK to work around MeasureString not working right on Linux |
| 222 | + //if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) |
| 223 | + // linesFilled = 1; |
| 224 | + return size; |
| 225 | + } |
| 226 | + |
| 227 | + /// <summary> |
| 228 | + /// Paints a single page. |
| 229 | + /// </summary> |
| 230 | + /// <param name="g">Graphics with 0,0 being the origin of the Page</param> |
| 231 | + /// <param name="pageNum">Page number to print</param> |
| 232 | + public override void PaintPage(Graphics g, int pageNum) { |
| 233 | + LogService.TraceMessage($"{pageNum}"); |
| 234 | + if (_screen == null) { |
| 235 | + Log.Debug("_ansiDocument must not be null"); |
| 236 | + return; |
| 237 | + } |
| 238 | + |
| 239 | + g.TextRenderingHint = ContentTypeEngineBase.TextRenderingHint; |
| 240 | + |
| 241 | + // Paint each line of the file |
| 242 | + var firstLineOnPage = _linesPerPage * (pageNum - 1); |
| 243 | + int i; |
| 244 | + for (i = firstLineOnPage; i < firstLineOnPage + _linesPerPage && i < _screen.Lines.Count; i++) { |
| 245 | + var yPos = (i - (_linesPerPage * (pageNum - 1))) * _lineHeight; |
| 246 | + var x = ContentSettings.LineNumberSeparator ? (int)(lineNumberWidth - 6 - MeasureString(g, _cachedFont, $"{_screen.Lines[i].LineNumber}").Width) : 0; |
| 247 | + // Line #s |
| 248 | + if (_screen.Lines[i].LineNumber > 0) { |
| 249 | + if (ContentSettings.LineNumbers && lineNumberWidth != 0) { |
| 250 | + // TOOD: Figure out how to make the spacig around separator more dynamic |
| 251 | + // TODO: Allow a different (non-monospace) font for line numbers |
| 252 | + g.DrawString($"{_screen.Lines[i].LineNumber}", _cachedFont, Brushes.Gray, x, yPos, ContentTypeEngineBase.StringFormat); |
| 253 | + } |
| 254 | + } |
| 255 | + |
| 256 | + // Line # separator (draw even if there's no line number, but stop at end of doc) |
| 257 | + // TODO: Support setting color of line #s and separator |
| 258 | + if (ContentSettings.LineNumbers && ContentSettings.LineNumberSeparator && lineNumberWidth != 0) { |
| 259 | + g.DrawLine(Pens.Gray, lineNumberWidth - 2, yPos, lineNumberWidth - 2, yPos + _lineHeight); |
| 260 | + } |
| 261 | + |
| 262 | + // Text |
| 263 | + float xPos = lineNumberWidth; |
| 264 | + foreach (var run in _screen.Lines[i].Runs) { |
| 265 | + System.Drawing.Font font = _cachedFont; |
| 266 | + if (run.Attributes.Bold) { |
| 267 | + if (run.Attributes.Italic) { |
| 268 | + font = new System.Drawing.Font(_cachedFont.FontFamily, _cachedFont.SizeInPoints, FontStyle.Bold | FontStyle.Italic, GraphicsUnit.Point); |
| 269 | + } |
| 270 | + else { |
| 271 | + font = new System.Drawing.Font(_cachedFont.FontFamily, _cachedFont.SizeInPoints, FontStyle.Bold, GraphicsUnit.Point); |
| 272 | + } |
| 273 | + } |
| 274 | + else if (run.Attributes.Italic) { |
| 275 | + font = new System.Drawing.Font(_cachedFont.FontFamily, _cachedFont.SizeInPoints, FontStyle.Italic, GraphicsUnit.Point); |
| 276 | + } |
| 277 | + var fg = Color.Black; |
| 278 | + if (run.Attributes.ForegroundColor != Color.White) |
| 279 | + fg = run.Attributes.ForegroundColor; |
| 280 | + |
| 281 | + var text = _screen.Lines[i].Text[run.Start..(run.Start + run.Length)]; |
| 282 | + var width = MeasureString(g, font, text).Width; |
| 283 | + RectangleF rect = new RectangleF(xPos, yPos, width, _lineHeight); |
| 284 | + g.DrawString(text, font, new SolidBrush(fg), rect, StringFormat); |
| 285 | + |
| 286 | + xPos += width; |
| 287 | + } |
| 288 | + if (ContentSettings.Diagnostics) { |
| 289 | + g.DrawRectangle(Pens.Red, lineNumberWidth, yPos, PageSize.Width - lineNumberWidth, _lineHeight); |
| 290 | + } |
| 291 | + } |
| 292 | +#if CURSOR |
| 293 | + if (_screen.CursorPosition.Y >= firstLineOnPage && _screen.CursorPosition.Y < firstLineOnPage + _linesPerPage) { |
| 294 | + var text = $"{(char)219}"; |
| 295 | + var x = ContentSettings.LineNumberSeparator ? (int)(lineNumberWidth) : 0; |
| 296 | + |
| 297 | + var width = MeasureString(g, _cachedFont, text).Width; |
| 298 | + RectangleF rect = new RectangleF(x + _screen.CursorPosition.X * width, _screen.CursorPosition.Y * _lineHeight, width, _lineHeight); |
| 299 | + //g.DrawString(text, _cachedFont, new SolidBrush(Color.Blue), rect, StringFormat); |
| 300 | + g.DrawRectangle(Pens.Black, x + _screen.CursorPosition.X * width, _screen.CursorPosition.Y * _lineHeight, width, _lineHeight); |
| 301 | + } |
| 302 | +#endif |
| 303 | + Log.Debug("Painted {lineOnPage} lines.", i - 1); |
| 304 | + } |
| 305 | + } |
| 306 | +} |
0 commit comments