using System.Text; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; namespace Inspectron.Epson; /// /// HTML-based printer implementation that builds HTML in memory, /// simulating Epson thermal printer output for testing and preview purposes. /// No file I/O is performed; call to retrieve the result. /// public class HtmlPrintBuilder : IEpsonPrinter { private readonly ILogger? _logger; private readonly byte _id; private readonly int _paperWidth; private bool _isConnected; private readonly StringBuilder _content = new(); // Current styling state private EpsonCommands.PrinterFont _currentFont = EpsonCommands.PrinterFont.A; private int _fontWidthMultiplier = 1; private int _fontHeightMultiplier = 1; private bool _isEmphasized; private bool _isRedColor; private bool _isQuadrupleMode; private bool _isBiggerFontWidthTM220; private bool _isBiggerFontHeightTM220; private int _lineSpacing = 14; private int _absolutePosition; // Loaded image buffer private Image? _loadedImage; private int _loadedImageMaxWidth; private static readonly Dictionary FontSizes = new() { { EpsonCommands.PrinterFont.A, (6, 12) }, { EpsonCommands.PrinterFont.B, (5, 9) }, { EpsonCommands.PrinterFont.C, (5, 12) }, { EpsonCommands.PrinterFont.D, (5, 12) }, { EpsonCommands.PrinterFont.E, (6, 12) } }; /// public bool IsConnected => _isConnected; /// /// Initialize a new instance of HtmlPrintBuilder /// /// Paper width in pixels (default 384 for 80mm thermal printer) /// Optional logger for diagnostic output /// Printer ID byte public HtmlPrintBuilder(int paperWidth = 384, ILogger? logger = null, byte id = 0x01) { _paperWidth = paperWidth; _logger = logger; _id = id; } /// /// Returns the generated HTML string. /// public string GetHtml() { return GenerateHtml(); } /// public Task ConnectAsync(string ip, int port = 9100, int timeoutSeconds = 10) { _logger?.LogInformation("HtmlPrintBuilder: Connecting (paper width: {Width}px)", _paperWidth); _content.Clear(); ResetStylingState(); _isConnected = true; _logger?.LogInformation("HtmlPrintBuilder: Connected successfully"); return Task.CompletedTask; } /// public void Disconnect() { _logger?.LogDebug("HtmlPrintBuilder: Disconnecting"); _loadedImage?.Dispose(); _loadedImage = null; _isConnected = false; } /// public Task GetPrinterStatusAsync() { _logger?.LogDebug("HtmlPrintBuilder: Returning mock printer status (always ready)"); return Task.FromResult(new PrinterStatus { RawByte = 0x12, DrawerOpen = false, IsOnline = true, IsCoverClosed = true, PaperFeedButtonPressed = false }); } /// public Task GetOfflineStatusAsync() { _logger?.LogDebug("HtmlPrintBuilder: Returning mock offline status (no issues)"); return Task.FromResult(new OfflineStatus { RawByte = 0x12, CoverOpen = false, PaperFeedButton = false, PaperEnd = false, ErrorOccurred = false }); } /// public Task GetErrorStatusAsync() { _logger?.LogDebug("HtmlPrintBuilder: Returning mock error status (no errors)"); return Task.FromResult(new ErrorStatus { RawByte = 0x12, RecoverableError = false, AutoCutterError = false, UnrecoverableError = false, AutoRecoveryError = false }); } /// public Task GetPaperSensorStatusAsync() { _logger?.LogDebug("HtmlPrintBuilder: Returning mock paper sensor status (paper present)"); return Task.FromResult(new PaperSensorStatus { RawByte = 0x12, PaperNearEnd = false, PaperPresent = true }); } /// public Task GetTM220StatusAsync() { _logger?.LogDebug("HtmlPrintBuilder: Returning mock TM220 status"); return Task.FromResult((byte)0x12); } /// public Task GetPrinterIdAsync() { return Task.FromResult(_id); } /// public Task GetOverallStatusAsync() { _logger?.LogInformation("HtmlPrintBuilder: Returning mock overall status (ready)"); return Task.FromResult(new OverallStatus { PrinterModel = "HtmlPrintBuilder", PrinterStatus = new PrinterStatus { RawByte = 0x12, IsOnline = true, IsCoverClosed = true }, OfflineStatus = new OfflineStatus { RawByte = 0x12 }, ErrorStatus = new ErrorStatus { RawByte = 0x12 }, PaperStatus = new PaperSensorStatus { RawByte = 0x12, PaperPresent = true }, StatusText = "READY - HTML Print Builder", StatusIcon = "🟢", IsReady = true, Recommendations = new List() }); } /// public Task InitAsync() { _logger?.LogInformation("HtmlPrintBuilder: Initializing (resetting styling state)"); ResetStylingState(); return Task.CompletedTask; } /// public Task SetQuadrupleMode(bool enable) { _logger?.LogInformation("HtmlPrintBuilder: {Action} quadruple mode", enable ? "Enabling" : "Disabling"); _isQuadrupleMode = enable; return Task.CompletedTask; } /// public Task SetRedColor(bool enable) { _logger?.LogInformation("HtmlPrintBuilder: {Action} red color mode", enable ? "Enabling" : "Disabling"); _isRedColor = enable; return Task.CompletedTask; } /// public Task SetEmphasized(bool enable) { _logger?.LogInformation("HtmlPrintBuilder: {Action} emphasized mode", enable ? "Enabling" : "Disabling"); _isEmphasized = enable; return Task.CompletedTask; } /// public Task SelectFont(EpsonCommands.PrinterFont font) { _logger?.LogInformation("HtmlPrintBuilder: Selecting font {Font}", font); _currentFont = font; return Task.CompletedTask; } /// public Task SetBiggerFontTM220(bool biggerWidth, bool biggerHeight = false, bool secondaryFont = true) { _logger?.LogInformation("HtmlPrintBuilder: Setting bigger font mode: Width={BiggerWidth}, Height={BiggerHeight}, SecondaryFont={SecondaryFont}", biggerWidth, biggerHeight, secondaryFont); _isBiggerFontWidthTM220 = biggerWidth; _isBiggerFontHeightTM220 = biggerHeight; return Task.CompletedTask; } /// public Task PrintTextAsync(string text) { EnsureConnected(); _logger?.LogInformation("HtmlPrintBuilder: Printing text ({Length} characters)", text.Length); var style = BuildCurrentStyle(); var escapedText = EscapeHtml(text); escapedText = escapedText.TrimEnd('\n').Replace("\n", "
"); var positionStyle = _absolutePosition > 0 ? $"margin-left: {_absolutePosition}px;" : ""; _content.AppendLine($"{escapedText}"); _absolutePosition = 0; return Task.CompletedTask; } /// public Task SetFontSizeAsync(int width, int height) { if (width < 1 || width > 8 || height < 1 || height > 8) throw new ArgumentException("Size must be between 1 and 8"); _logger?.LogInformation("HtmlPrintBuilder: Setting font size to {Width}x{Height}", width, height); _fontWidthMultiplier = width; _fontHeightMultiplier = height; return Task.CompletedTask; } /// public async Task PrintTextAndCutAsync(string text, int feedLines = 5) { _logger?.LogInformation("HtmlPrintBuilder: Printing text with cut ({Length} characters, {FeedLines} feed lines)", text.Length, feedLines); await InitAsync(); await PrintTextAsync(text); await PrintTextAsync("\n"); await FeedLinesAsync(feedLines); await CutAsync(); } /// public Task PrintBuffer() { _logger?.LogDebug("HtmlPrintBuilder: PrintBuffer (no-op)"); return Task.CompletedTask; } /// public Task FeedLinesAsync(int lines) { EnsureConnected(); _logger?.LogInformation("HtmlPrintBuilder: Feeding {Lines} lines", lines); for (int i = 0; i < lines; i++) { _content.AppendLine($"
"); } return Task.CompletedTask; } /// public Task CutAsync(bool fullCut = true) { EnsureConnected(); _logger?.LogInformation("HtmlPrintBuilder: Cutting paper"); _content.AppendLine("
✂️
"); return Task.CompletedTask; } /// public async Task LoadImageAsync(string imagePath, int maxWidth = 384) { _logger?.LogInformation("HtmlPrintBuilder: Loading image from file: {Path}", imagePath); if (!File.Exists(imagePath)) { throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath); } _loadedImage?.Dispose(); _loadedImage = await Image.LoadAsync(imagePath); _loadedImageMaxWidth = maxWidth; } /// public async Task LoadImageAsync(Stream imageStream, int maxWidth = 384) { _logger?.LogInformation("HtmlPrintBuilder: Loading image from stream"); _loadedImage?.Dispose(); _loadedImage = await Image.LoadAsync(imageStream); _loadedImageMaxWidth = maxWidth; } /// public Task LoadImageAsync(Image image, int maxWidth = 384) { _logger?.LogInformation("HtmlPrintBuilder: Loading image from ImageSharp object"); _loadedImage?.Dispose(); _loadedImage = image.Clone(); _loadedImageMaxWidth = maxWidth; return Task.CompletedTask; } /// public async Task PrintLoadedImage() { EnsureConnected(); if (_loadedImage == null) { _logger?.LogWarning("HtmlPrintBuilder: No image loaded to print"); return; } _logger?.LogInformation("HtmlPrintBuilder: Printing loaded image"); await RenderImageToHtml(_loadedImage, _loadedImageMaxWidth); _loadedImage.Dispose(); _loadedImage = null; } /// public async Task PrintImageBitModeAsync(string imagePath, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.SingleDensity8Dot, int maxWidth = 10) { _logger?.LogInformation("HtmlPrintBuilder: Printing image (bit mode) from file: {Path}", imagePath); if (!File.Exists(imagePath)) { throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath); } using var image = await Image.LoadAsync(imagePath); await PrintImageBitModeAsync(image, mode, maxWidth); } /// public async Task PrintImageBitModeAsync(Stream imageStream, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.DoubleDensity24Dot, int maxWidth = 384) { _logger?.LogInformation("HtmlPrintBuilder: Printing image (bit mode) from stream"); using var image = await Image.LoadAsync(imageStream); await PrintImageBitModeAsync(image, mode, maxWidth); } /// public Task PrintImageBitModeAsync(Image image, EpsonCommands.BitImageMode mode, int maxWidth) { EnsureConnected(); _logger?.LogInformation("HtmlPrintBuilder: Printing image (bit mode) {Width}x{Height}", image.Width, image.Height); return RenderImageToHtml(image, maxWidth); } /// public Task SendRawCommandAsync(byte[] command) { _logger?.LogDebug("HtmlPrintBuilder: SendRawCommandAsync ignored ({Length} bytes)", command.Length); return Task.CompletedTask; } /// public Task SetAbsolutePrintPosition(int x) { if (x < 0 || x > 65535) throw new ArgumentOutOfRangeException(nameof(x), "Position must be between 0 and 65535"); _logger?.LogInformation("HtmlPrintBuilder: Setting absolute print position to {X}", x); _absolutePosition = x; return Task.CompletedTask; } /// public Task SetDefaultLineSpacing() { _logger?.LogInformation("HtmlPrintBuilder: Setting default line spacing"); _lineSpacing = 14; return Task.CompletedTask; } /// public Task SetCustomLineSpacing(int spacing = 24) { if (spacing < 0 || spacing > 255) throw new ArgumentOutOfRangeException(nameof(spacing), "Spacing must be between 0 and 255"); _logger?.LogInformation("HtmlPrintBuilder: Setting custom line spacing to {Spacing}", spacing); _lineSpacing = (int)(spacing * 0.6); return Task.CompletedTask; } /// public ValueTask DisposeAsync() { Disconnect(); return ValueTask.CompletedTask; } #region Private Methods private void ResetStylingState() { _currentFont = EpsonCommands.PrinterFont.A; _fontWidthMultiplier = 1; _fontHeightMultiplier = 1; _isEmphasized = false; _isRedColor = false; _isQuadrupleMode = false; _isBiggerFontWidthTM220 = false; _isBiggerFontHeightTM220 = false; _lineSpacing = 14; _absolutePosition = 0; } private string BuildCurrentStyle() { var styles = new List(); var (baseWidth, baseHeight) = FontSizes[_currentFont]; float effectiveWidthMultiplier = _fontWidthMultiplier; float effectiveHeightMultiplier = _fontHeightMultiplier; if (_isQuadrupleMode) { effectiveWidthMultiplier *= 2; effectiveHeightMultiplier *= 2; } if (_isBiggerFontWidthTM220) { effectiveWidthMultiplier *= 2f; } if (_isBiggerFontHeightTM220) { effectiveHeightMultiplier *= 1.2f; } int fontSize = (int)(baseHeight * effectiveHeightMultiplier); styles.Add($"font-size: {fontSize}px"); if (effectiveWidthMultiplier > 1) { int letterSpacing = (int)((effectiveWidthMultiplier) * baseWidth / 2); styles.Add($"letter-spacing: {letterSpacing}px"); } if (_isEmphasized) { styles.Add("font-weight: bold"); } if (_isRedColor) { styles.Add("color: #cc0000"); } styles.Add($"line-height: {_lineSpacing}px"); return string.Join("; ", styles) + ";"; } private async Task RenderImageToHtml(Image sourceImage, int maxWidth) { using var processedImage = sourceImage.Clone(); if (processedImage.Width > maxWidth) { EpsonImageConverter.ResizeImage(processedImage, maxWidth); } using var grayscaleImage = EpsonImageConverter.ConvertToGrayscale(processedImage); EpsonImageConverter.ApplyFloydSteinbergDithering(grayscaleImage); using var ditheredImage = new Image(grayscaleImage.Width, grayscaleImage.Height); grayscaleImage.ProcessPixelRows(ditheredImage, (sourceAccessor, targetAccessor) => { for (int y = 0; y < sourceAccessor.Height; y++) { var sourceRow = sourceAccessor.GetRowSpan(y); var targetRow = targetAccessor.GetRowSpan(y); for (int x = 0; x < sourceAccessor.Width; x++) { byte value = sourceRow[x].PackedValue; targetRow[x] = new Rgba32(value, value, value, 255); } } }); // Embed image as base64 data URI instead of saving to file using var ms = new MemoryStream(); await ditheredImage.SaveAsync(ms, new PngEncoder()); var base64 = Convert.ToBase64String(ms.ToArray()); var dataUri = $"data:image/png;base64,{base64}"; var positionStyle = _absolutePosition > 0 ? $"margin-left: {_absolutePosition}px;" : ""; _content.AppendLine($"
\"Printed
"); _absolutePosition = 0; _logger?.LogInformation("HtmlPrintBuilder: Image embedded as base64 ({Bytes} bytes)", ms.Length); } private string GenerateHtml() { return $@" Epson Printer Output
{_content}
"; } private static string EscapeHtml(string text) { return text .Replace("&", "&") .Replace("<", "<") .Replace(">", ">") .Replace("\"", """) .Replace("'", "'"); } private void EnsureConnected() { if (!_isConnected) { throw new EpsonConnectionException("Not connected. Call ConnectAsync first."); } } #endregion }