using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System.Text; using Inspectron.Epson.PrintServer.PrintServices; namespace Inspectron.Epson; /// /// HTML-based printer implementation that outputs to an HTML file, /// simulating Epson thermal printer output for testing and preview purposes. /// public class HtmlPrinter : IEpsonPrinter { private readonly ILogger? _logger; private readonly byte _id; private readonly string _outputPath; private readonly int _paperWidth; private readonly string _imageDirectory; private bool _isConnected; private readonly StringBuilder _content = new(); private int _imageCounter; // 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; // Default line spacing in pixels (scaled for screen) private int _absolutePosition; // Current X position offset // Loaded image buffer private Image? _loadedImage; private int _loadedImageMaxWidth; /// /// Font size mappings for different Epson fonts (scaled for screen display) /// Original dots: Font A: 12x24, Font B: 9x17, Font C: 9x24, Font D: 10x24, Font E: 12x24 /// Scaled to ~0.5x for reasonable screen display /// 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 HtmlPrinter /// /// Path to the output HTML file /// Paper width in pixels (default 384 for 80mm thermal printer) /// Optional logger for diagnostic output public HtmlPrinter(string outputPath, int paperWidth = 384, ILogger? logger = null, byte id=0x01) { _outputPath = outputPath; _paperWidth = paperWidth; _logger = logger; _id = id; // Create image directory next to HTML file var directory = Path.GetDirectoryName(outputPath) ?? "."; var fileName = Path.GetFileNameWithoutExtension(outputPath); _imageDirectory = Path.Combine(directory, $"{fileName}_images"); } /// public Task ConnectAsync(string ip, int port = 9100, int timeoutSeconds = 10) { _logger?.LogInformation("HtmlPrinter: Connecting (output: {Path}, paper width: {Width}px)", _outputPath, _paperWidth); // Create image directory if it doesn't exist if (!Directory.Exists(_imageDirectory)) { Directory.CreateDirectory(_imageDirectory); } // Clear previous content _content.Clear(); _imageCounter = 0; ResetStylingState(); _isConnected = true; WriteHtmlFile(); _logger?.LogInformation("HtmlPrinter: Connected successfully"); return Task.CompletedTask; } /// public void Disconnect() { _logger?.LogDebug("HtmlPrinter: Disconnecting"); if (_isConnected) { WriteHtmlFile(); } _loadedImage?.Dispose(); _loadedImage = null; _isConnected = false; } /// public Task GetPrinterStatusAsync() { _logger?.LogDebug("HtmlPrinter: Returning mock printer status (always ready)"); return Task.FromResult(new PrinterStatus { RawByte = 0x12, // Online, cover closed DrawerOpen = false, IsOnline = true, IsCoverClosed = true, PaperFeedButtonPressed = false }); } /// public Task GetOfflineStatusAsync() { _logger?.LogDebug("HtmlPrinter: 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("HtmlPrinter: 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("HtmlPrinter: Returning mock paper sensor status (paper present)"); return Task.FromResult(new PaperSensorStatus { RawByte = 0x12, PaperNearEnd = false, PaperPresent = true }); } /// public Task GetTM220StatusAsync() { _logger?.LogDebug("HtmlPrinter: Returning mock TM220 status"); return Task.FromResult((byte)0x12); } /// public Task GetPrinterIdAsync() { return Task.FromResult(_id); } /// public Task GetOverallStatusAsync() { _logger?.LogInformation("HtmlPrinter: Returning mock overall status (ready)"); return Task.FromResult(new OverallStatus { PrinterModel = "HtmlPrinter", 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 Printer Simulator", StatusIcon = "🟢", IsReady = true, Recommendations = new List() }); } /// public Task InitAsync() { _logger?.LogInformation("HtmlPrinter: Initializing (resetting styling state)"); ResetStylingState(); return Task.CompletedTask; } /// public Task SetQuadrupleMode(bool enable) { _logger?.LogInformation("HtmlPrinter: {Action} quadruple mode", enable ? "Enabling" : "Disabling"); _isQuadrupleMode = enable; return Task.CompletedTask; } /// public Task SetRedColor(bool enable) { _logger?.LogInformation("HtmlPrinter: {Action} red color mode", enable ? "Enabling" : "Disabling"); _isRedColor = enable; return Task.CompletedTask; } /// public Task SetEmphasized(bool enable) { _logger?.LogInformation("HtmlPrinter: {Action} emphasized mode", enable ? "Enabling" : "Disabling"); _isEmphasized = enable; return Task.CompletedTask; } /// public Task SelectFont(EpsonCommands.PrinterFont font) { _logger?.LogInformation("HtmlPrinter: Selecting font {Font}", font); _currentFont = font; return Task.CompletedTask; } /// public Task SetBiggerFontTM220(bool biggerWidth, bool biggerHeight = false, bool secondaryFont = true) { _logger?.LogInformation("HtmlPrinter: Setting bigger font mode: Width={BiggerWidth}, Height={BiggerHeight}, SecondaryFont={SecondaryFont}", biggerWidth, biggerHeight, secondaryFont); _isBiggerFontWidthTM220 = biggerWidth; _isBiggerFontHeightTM220 = biggerHeight; // Note: secondaryFont is ignored in HTML simulation as it only affects the physical printer's font selection return Task.CompletedTask; } /// public Task PrintTextAsync(string text) { EnsureConnected(); _logger?.LogInformation("HtmlPrinter: Printing text ({Length} characters)", text.Length); var style = BuildCurrentStyle(); var escapedText = EscapeHtml(text); // Handle newlines escapedText = escapedText.TrimEnd('\n').Replace("\n", "
"); // Add margin-left for absolute positioning var positionStyle = _absolutePosition > 0 ? $"margin-left: {_absolutePosition}px;" : ""; _content.AppendLine($"{escapedText}"); // Reset absolute position after printing _absolutePosition = 0; WriteHtmlFile(); 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("HtmlPrinter: 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("HtmlPrinter: 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("HtmlPrinter: PrintBuffer (no-op for HTML)"); return Task.CompletedTask; } /// public Task FeedLinesAsync(int lines) { EnsureConnected(); _logger?.LogInformation("HtmlPrinter: Feeding {Lines} lines", lines); for (int i = 0; i < lines; i++) { _content.AppendLine($"
"); } WriteHtmlFile(); return Task.CompletedTask; } /// public Task CutAsync(bool fullCut = true) { EnsureConnected(); _logger?.LogInformation("HtmlPrinter: Cutting paper"); if(fullCut) WriteHtmlFile(); else { // add scissors icon for partial cut _content.AppendLine("
✂️
"); } return Task.CompletedTask; } /// public async Task LoadImageAsync(string imagePath, int maxWidth = 384) { _logger?.LogInformation("HtmlPrinter: 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("HtmlPrinter: Loading image from stream"); _loadedImage?.Dispose(); _loadedImage = await Image.LoadAsync(imageStream); _loadedImageMaxWidth = maxWidth; } /// public Task LoadImageAsync(Image image, int maxWidth = 384) { _logger?.LogInformation("HtmlPrinter: 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("HtmlPrinter: No image loaded to print"); return; } _logger?.LogInformation("HtmlPrinter: 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("HtmlPrinter: 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("HtmlPrinter: 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("HtmlPrinter: Printing image (bit mode) {Width}x{Height}", image.Width, image.Height); return RenderImageToHtml(image, maxWidth); } /// public Task SendRawCommandAsync(byte[] command) { _logger?.LogDebug("HtmlPrinter: SendRawCommandAsync ignored ({Length} bytes)", command.Length); // Raw commands are ignored in HTML output 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("HtmlPrinter: Setting absolute print position to {X}", x); _absolutePosition = x; return Task.CompletedTask; } /// public Task SetDefaultLineSpacing() { _logger?.LogInformation("HtmlPrinter: Setting default line spacing"); _lineSpacing = 14; // Default (scaled for screen) 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("HtmlPrinter: Setting custom line spacing to {Spacing}", spacing); // Convert dot spacing to approximate pixel spacing (scaled ~0.5x for screen) _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(); // Base font size from selected font var (baseWidth, baseHeight) = FontSizes[_currentFont]; // Apply multipliers 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"); // Letter spacing for width scaling (approximate) if (effectiveWidthMultiplier > 1) { int letterSpacing = (int)((effectiveWidthMultiplier) * baseWidth / 2); styles.Add($"letter-spacing: {letterSpacing}px"); } // Font weight if (_isEmphasized) { styles.Add("font-weight: bold"); } // Color if (_isRedColor) { styles.Add("color: #cc0000"); } // Line height based on current line spacing styles.Add($"line-height: {_lineSpacing}px"); return string.Join("; ", styles) + ";"; } private async Task RenderImageToHtml(Image sourceImage, int maxWidth) { // Clone and process the image (apply dithering like real printer) using var processedImage = sourceImage.Clone(); // Resize if needed if (processedImage.Width > maxWidth) { EpsonImageConverter.ResizeImage(processedImage, maxWidth); } // Convert to grayscale using var grayscaleImage = EpsonImageConverter.ConvertToGrayscale(processedImage); // Apply Floyd-Steinberg dithering EpsonImageConverter.ApplyFloydSteinbergDithering(grayscaleImage); // Convert back to RGBA for saving 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); } } }); // Save image to file _imageCounter++; var imageName = $"image_{_imageCounter:D4}.png"; var imagePath = Path.Combine(_imageDirectory, imageName); await ditheredImage.SaveAsync(imagePath, new PngEncoder()); // Get relative path for HTML var htmlDir = Path.GetDirectoryName(_outputPath) ?? "."; if(htmlDir=="") htmlDir="."; var relativeImagePath = Path.GetRelativePath(htmlDir, imagePath).Replace('\\', '/'); // Add to HTML content var positionStyle = _absolutePosition > 0 ? $"margin-left: {_absolutePosition}px;" : ""; _content.AppendLine($"
\"Printed
"); // Reset absolute position _absolutePosition = 0; WriteHtmlFile(); _logger?.LogInformation("HtmlPrinter: Image saved to {Path}", imagePath); } private void WriteHtmlFile() { var html = GenerateHtml(); File.WriteAllText(_outputPath, html, Encoding.UTF8); _logger?.LogDebug("HtmlPrinter: HTML file written to {Path}", _outputPath); } 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 }