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:
137
Inspectron.Epson.Tests/BitImagePrintingTests.cs
Normal file
137
Inspectron.Epson.Tests/BitImagePrintingTests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -608,6 +608,16 @@ public class EpsonPrinter : IEpsonPrinter
|
||||
|
||||
_logger?.LogDebug("Printing in {SliceCount} slices of {BitsPerSlice} dots each", totalSlices, bitsPerSlice);
|
||||
|
||||
// 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
|
||||
{
|
||||
// Print each horizontal slice
|
||||
for (int slice = 0; slice < totalSlices; slice++)
|
||||
{
|
||||
@@ -628,6 +638,11 @@ public class EpsonPrinter : IEpsonPrinter
|
||||
await SendRawAsync(EpsonCommands.LineFeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
await SetDefaultLineSpacing();
|
||||
}
|
||||
|
||||
_logger?.LogInformation("Image printed successfully using bit-image mode");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user