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 <sliceHeight> 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
This commit is contained in:
EugeneTes
2026-09-03 14:11:17 +02:00
parent ac72e9ac4e
commit 36eab4b4b2
2 changed files with 168 additions and 16 deletions

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public class BitImagePrintingTests
{
private const byte ESC = 0x1B;
/// <summary>
/// Runs an EpsonPrinter against a loopback listener and returns every byte it sent.
/// </summary>
private static async Task<byte[]> CaptureAsync(Func<EpsonPrinter, Task> work)
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
var captured = new List<byte>();
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<Rgba32>(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<Rgba32>(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<Rgba32>(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");
}
}

View File

@@ -608,26 +608,41 @@ public class EpsonPrinter : IEpsonPrinter
_logger?.LogDebug("Printing in {SliceCount} slices of {BitsPerSlice} dots each", totalSlices, bitsPerSlice); _logger?.LogDebug("Printing in {SliceCount} slices of {BitsPerSlice} dots each", totalSlices, bitsPerSlice);
// Print each horizontal slice // Each ESC * pass paints bitsPerSlice dot rows, but the LF between passes
for (int slice = 0; slice < totalSlices; slice++) // 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; // Print each horizontal slice
int sliceLength = finalWidth * bytesPerColumn; for (int slice = 0; slice < totalSlices; slice++)
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); 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"); _logger?.LogInformation("Image printed successfully using bit-image mode");
} }