From 36eab4b4b2ce2c0c9ada61861a591dd519ab8191 Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Thu, 3 Sep 2026 14:11:17 +0200 Subject: [PATCH] fix ESC * bit-image banding: match line spacing to the slice height The POS895 logo printed with a white line under every band. Root cause is in PrintImageBitModeAsync, not in the adapter: between slices it advances with a bare LF, which moves the paper by the CURRENT line spacing rather than by the slice height. ESC @ leaves that spacing at 1/6 inch, about 33.8 dots at 203 dpi, so each pass painted 24 dot rows and then advanced ~34, leaving a ~10 dot gap. The code assumed LF advances exactly one slice; nothing ever made that true. Confirmed on hardware before changing anything: printed two identical solid 576x96 blocks, one at default spacing and one after ESC 3 24. The first came out striped, the second solid. Fixed by setting the spacing to the slice height for the duration of the image and restoring the default in a finally. The restore matters as much as the fix - leaving it at 24 would compress every following line of text, turning a logo defect into a whole-receipt one. Fixed here rather than in POS895 because the assumption is wrong for any caller of this method. Blast radius is nil: PrintImageBitModeAsync has no other production consumer, since TM_T30IIITranslated uses the raster path and TM_U220IITranslated.PrintImageAsync is an empty stub. Three regression tests drive a real EpsonPrinter against a loopback TCP listener and assert on the wire bytes: ESC 3 before the first ESC *, ESC 2 after the last, and the spacing tracking the mode (ESC 3 8 for 8-dot) rather than a hardcoded 24. All three failed before the fix. Verified on the printer: the logo now prints solid. Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref --- .../BitImagePrintingTests.cs | 137 ++++++++++++++++++ Inspectron.Epson/EpsonPrinter.cs | 47 ++++-- 2 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 Inspectron.Epson.Tests/BitImagePrintingTests.cs diff --git a/Inspectron.Epson.Tests/BitImagePrintingTests.cs b/Inspectron.Epson.Tests/BitImagePrintingTests.cs new file mode 100644 index 0000000..492d577 --- /dev/null +++ b/Inspectron.Epson.Tests/BitImagePrintingTests.cs @@ -0,0 +1,137 @@ +using System.Net; +using System.Net.Sockets; +using Inspectron.Epson; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Inspectron.Epson.Tests; + +/// +/// Byte-level tests for ESC * bit-image printing. EpsonPrinter writes to a TCP +/// socket, so these point it at a loopback listener and assert on what actually +/// goes on the wire. +/// +public class BitImagePrintingTests +{ + private const byte ESC = 0x1B; + + /// + /// Runs an EpsonPrinter against a loopback listener and returns every byte it sent. + /// + private static async Task CaptureAsync(Func work) + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + + var captured = new List(); + var pump = Task.Run(async () => + { + using var client = await listener.AcceptTcpClientAsync(); + using var stream = client.GetStream(); + + var buffer = new byte[16384]; + int read; + while ((read = await stream.ReadAsync(buffer)) > 0) + { + captured.AddRange(buffer.AsSpan(0, read).ToArray()); + } + }); + + await using (var printer = new EpsonPrinter()) + { + await printer.ConnectAsync("127.0.0.1", port); + await work(printer); + } + + await pump; + listener.Stop(); + + return captured.ToArray(); + } + + private static int IndexOf(byte[] haystack, byte[] needle, int start = 0) + { + for (int i = start; i <= haystack.Length - needle.Length; i++) + { + bool match = true; + for (int j = 0; j < needle.Length; j++) + { + if (haystack[i + j] != needle[j]) { match = false; break; } + } + + if (match) return i; + } + + return -1; + } + + private static int LastIndexOf(byte[] haystack, byte[] needle) + { + int found = -1, at = 0; + while ((at = IndexOf(haystack, needle, at)) >= 0) + { + found = at; + at++; + } + + return found; + } + + [Fact] + public async Task Sets_line_spacing_to_the_slice_height_before_the_first_slice() + { + // Each ESC * 33 pass paints 24 dot rows, but the LF between passes advances + // by the current line spacing - 1/6 inch, about 34 dots at 203 dpi - unless + // told otherwise. Left at the default, every band is followed by a ~10 dot + // white gap and the image comes out striped. Confirmed on real hardware. + byte[] wire = await CaptureAsync(async printer => + { + using var image = new Image(64, 48); // two 24-dot slices + await printer.PrintImageBitModeAsync( + image, EpsonCommands.BitImageMode.DoubleDensity24Dot, 64); + }); + + int spacing = IndexOf(wire, new byte[] { ESC, 0x33, 24 }); // ESC 3 24 + int firstSlice = IndexOf(wire, new byte[] { ESC, 0x2A }); // ESC * + + Assert.True(spacing >= 0, "expected ESC 3 24 to set line spacing to the slice height"); + Assert.True(firstSlice >= 0, "expected at least one ESC * bit-image slice"); + Assert.True(spacing < firstSlice, + "line spacing must be set BEFORE the first slice, or the first gap still appears"); + } + + [Fact] + public async Task Restores_default_line_spacing_after_the_last_slice() + { + // Leaving the image's tight spacing set would compress every subsequent + // line of text on the receipt. + byte[] wire = await CaptureAsync(async printer => + { + using var image = new Image(64, 48); + await printer.PrintImageBitModeAsync( + image, EpsonCommands.BitImageMode.DoubleDensity24Dot, 64); + }); + + int lastSlice = LastIndexOf(wire, new byte[] { ESC, 0x2A }); + int restore = LastIndexOf(wire, new byte[] { ESC, 0x32 }); // ESC 2 + + Assert.True(restore >= 0, "expected ESC 2 to restore the default line spacing"); + Assert.True(restore > lastSlice, "default spacing must be restored after the last slice"); + } + + [Fact] + public async Task Uses_the_eight_dot_slice_height_for_eight_dot_modes() + { + // The spacing has to track the mode, not be hardcoded to 24. + byte[] wire = await CaptureAsync(async printer => + { + using var image = new Image(64, 16); // two 8-dot slices + await printer.PrintImageBitModeAsync( + image, EpsonCommands.BitImageMode.SingleDensity8Dot, 64); + }); + + Assert.True(IndexOf(wire, new byte[] { ESC, 0x33, 8 }) >= 0, + "expected ESC 3 8 for an 8-dot bit-image mode"); + } +} diff --git a/Inspectron.Epson/EpsonPrinter.cs b/Inspectron.Epson/EpsonPrinter.cs index d99319d..0d86085 100644 --- a/Inspectron.Epson/EpsonPrinter.cs +++ b/Inspectron.Epson/EpsonPrinter.cs @@ -608,26 +608,41 @@ public class EpsonPrinter : IEpsonPrinter _logger?.LogDebug("Printing in {SliceCount} slices of {BitsPerSlice} dots each", totalSlices, bitsPerSlice); - // Print each horizontal slice - for (int slice = 0; slice < totalSlices; slice++) + // Each ESC * pass paints bitsPerSlice dot rows, but the LF between passes + // advances by the CURRENT line spacing, which ESC @ leaves at 1/6 inch - + // about 34 dots at 203 dpi. Left alone, every 24-dot band is followed by a + // ~10 dot white gap and the image prints striped. Match the spacing to the + // slice height for the duration of the image, then put it back: leaving it + // tight would compress every following line of text on the receipt. + await SetCustomLineSpacing(bitsPerSlice); + + try { - int sliceOffset = slice * finalWidth * bytesPerColumn; - int sliceLength = finalWidth * bytesPerColumn; - - var sliceData = new byte[sliceLength]; - Array.Copy(columnData, sliceOffset, sliceData, 0, sliceLength); - - // Create and send ESC * command for this slice - var command = EpsonCommands.CreateBitImageCommand(mode, sliceData, finalWidth); - - await SendRawAsync(command); - - // Move to next line after each slice (except the last one) - if (slice < totalSlices - 1) + // Print each horizontal slice + for (int slice = 0; slice < totalSlices; slice++) { - await SendRawAsync(EpsonCommands.LineFeed); + int sliceOffset = slice * finalWidth * bytesPerColumn; + int sliceLength = finalWidth * bytesPerColumn; + + var sliceData = new byte[sliceLength]; + Array.Copy(columnData, sliceOffset, sliceData, 0, sliceLength); + + // Create and send ESC * command for this slice + var command = EpsonCommands.CreateBitImageCommand(mode, sliceData, finalWidth); + + await SendRawAsync(command); + + // Move to next line after each slice (except the last one) + if (slice < totalSlices - 1) + { + await SendRawAsync(EpsonCommands.LineFeed); + } } } + finally + { + await SetDefaultLineSpacing(); + } _logger?.LogInformation("Image printed successfully using bit-image mode"); }