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($"");
_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
}