304 lines
10 KiB
C#
304 lines
10 KiB
C#
using SixLabors.ImageSharp;
|
|
using SixLabors.ImageSharp.PixelFormats;
|
|
using SixLabors.ImageSharp.Processing;
|
|
|
|
namespace Inspectron.Epson;
|
|
|
|
/// <summary>
|
|
/// Helper class for converting images to ESC/POS raster format
|
|
/// </summary>
|
|
public static class EpsonImageConverter
|
|
{
|
|
/// <summary>
|
|
/// Convert an image to ESC/POS raster format
|
|
/// </summary>
|
|
/// <param name="image">Source image</param>
|
|
/// <param name="maxWidth">Maximum width in pixels (will maintain aspect ratio)</param>
|
|
/// <param name="finalWidth">Output: final width after processing (byte-aligned)</param>
|
|
/// <param name="finalHeight">Output: final height after processing</param>
|
|
/// <returns>Byte array containing monochrome raster data</returns>
|
|
public static byte[] ConvertToRasterData(
|
|
Image<Rgba32> image,
|
|
int maxWidth,
|
|
out int finalWidth,
|
|
out int finalHeight)
|
|
{
|
|
// Clone the image to avoid modifying the original
|
|
using var processedImage = image.Clone();
|
|
|
|
// Step 1: Resize if needed
|
|
ResizeImage(processedImage, maxWidth);
|
|
|
|
// Step 2: Convert to grayscale
|
|
using var grayscaleImage = ConvertToGrayscale(processedImage);
|
|
|
|
// Step 3: Apply dithering for better quality 1-bit conversion
|
|
ApplyFloydSteinbergDithering(grayscaleImage);
|
|
|
|
// Step 4: Calculate padded width (must be divisible by 8)
|
|
int originalWidth = grayscaleImage.Width;
|
|
int paddedWidth = ((originalWidth + 7) / 8) * 8;
|
|
|
|
finalWidth = paddedWidth;
|
|
finalHeight = grayscaleImage.Height;
|
|
|
|
// Step 5: Pack pixels into bytes
|
|
return PackPixelsToBytes(grayscaleImage, paddedWidth);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resize image to fit within max width while maintaining aspect ratio
|
|
/// </summary>
|
|
public static void ResizeImage(Image<Rgba32> image, int maxWidth)
|
|
{
|
|
|
|
int newWidth = maxWidth;
|
|
int newHeight = (int)((float)image.Height / image.Width * maxWidth);
|
|
|
|
image.Mutate(x => x.Resize(new ResizeOptions
|
|
{
|
|
Size = new Size(newWidth, newHeight),
|
|
Mode = ResizeMode.Max,
|
|
Sampler = KnownResamplers.Lanczos3
|
|
}));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert RGBA image to grayscale
|
|
/// </summary>
|
|
public static Image<L8> ConvertToGrayscale(Image<Rgba32> image)
|
|
{
|
|
var grayscaleImage = new Image<L8>(image.Width, image.Height);
|
|
|
|
image.ProcessPixelRows(grayscaleImage, (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++)
|
|
{
|
|
var pixel = sourceRow[x];
|
|
|
|
// Standard luminance calculation (ITU-R BT.709)
|
|
byte luminance = (byte)(
|
|
0.2126 * pixel.R +
|
|
0.7152 * pixel.G +
|
|
0.0722 * pixel.B
|
|
);
|
|
|
|
targetRow[x] = new L8(luminance);
|
|
}
|
|
}
|
|
});
|
|
|
|
return grayscaleImage;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply Floyd-Steinberg dithering algorithm for smooth 1-bit conversion
|
|
/// </summary>
|
|
public static void ApplyFloydSteinbergDithering(Image<L8> image)
|
|
{
|
|
int width = image.Width;
|
|
int height = image.Height;
|
|
|
|
// Create a copy of pixel data to work with
|
|
var pixels = new float[height, width];
|
|
|
|
image.ProcessPixelRows(accessor =>
|
|
{
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
var row = accessor.GetRowSpan(y);
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
pixels[y, x] = row[x].PackedValue;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Apply Floyd-Steinberg dithering
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
float oldPixel = pixels[y, x];
|
|
float newPixel = oldPixel < 128 ? 0 : 255;
|
|
pixels[y, x] = newPixel;
|
|
|
|
float error = oldPixel - newPixel;
|
|
|
|
// Distribute error to neighboring pixels
|
|
if (x + 1 < width)
|
|
pixels[y, x + 1] += error * 7 / 16;
|
|
|
|
if (y + 1 < height)
|
|
{
|
|
if (x > 0)
|
|
pixels[y + 1, x - 1] += error * 3 / 16;
|
|
|
|
pixels[y + 1, x] += error * 5 / 16;
|
|
|
|
if (x + 1 < width)
|
|
pixels[y + 1, x + 1] += error * 1 / 16;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write dithered pixels back to image
|
|
image.ProcessPixelRows(accessor =>
|
|
{
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
var row = accessor.GetRowSpan(y);
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
byte value = (byte)Math.Clamp(pixels[y, x], 0, 255);
|
|
row[x] = new L8(value);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert image to column format bit image data for ESC * command
|
|
/// </summary>
|
|
/// <param name="image">Source image</param>
|
|
/// <param name="maxWidth">Maximum width in pixels</param>
|
|
/// <param name="mode">Bit image mode (8-dot or 24-dot)</param>
|
|
/// <param name="finalWidth">Output: final width in dots</param>
|
|
/// <param name="finalHeight">Output: final height in dots</param>
|
|
/// <returns>Column format bit image data</returns>
|
|
public static byte[] ConvertToColumnFormat(
|
|
Image<Rgba32> image,
|
|
int maxWidth,
|
|
EpsonCommands.BitImageMode mode,
|
|
out int finalWidth,
|
|
out int finalHeight)
|
|
{
|
|
// Clone the image to avoid modifying the original
|
|
using var processedImage = image.Clone();
|
|
|
|
// Resize if needed
|
|
ResizeImage(processedImage, maxWidth);
|
|
|
|
// Convert to grayscale
|
|
using var grayscaleImage = ConvertToGrayscale(processedImage);
|
|
|
|
// Apply dithering
|
|
ApplyFloydSteinbergDithering(grayscaleImage);
|
|
|
|
int bitsPerColumn = mode == EpsonCommands.BitImageMode.SingleDensity8Dot ||
|
|
mode == EpsonCommands.BitImageMode.DoubleDensity8Dot ? 8 : 24;
|
|
|
|
// Calculate padded height (must be divisible by column height)
|
|
int originalHeight = grayscaleImage.Height;
|
|
int paddedHeight = ((originalHeight + bitsPerColumn - 1) / bitsPerColumn) * bitsPerColumn;
|
|
|
|
finalWidth = grayscaleImage.Width;
|
|
finalHeight = paddedHeight;
|
|
|
|
// Pack pixels into column format
|
|
return PackPixelsToColumnFormat(grayscaleImage, bitsPerColumn, paddedHeight);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pack pixels into column format (vertical bytes, left to right)
|
|
/// </summary>
|
|
private static byte[] PackPixelsToColumnFormat(Image<L8> image, int bitsPerColumn, int paddedHeight)
|
|
{
|
|
int width = image.Width;
|
|
int height = image.Height;
|
|
int bytesPerColumn = bitsPerColumn / 8;
|
|
int totalColumns = width;
|
|
int totalSlices = paddedHeight / bitsPerColumn;
|
|
|
|
var columnData = new byte[totalColumns * totalSlices * bytesPerColumn];
|
|
int dataIndex = 0;
|
|
|
|
// Process image slice by slice (for 24-dot mode, we process 24 rows at a time)
|
|
for (int slice = 0; slice < totalSlices; slice++)
|
|
{
|
|
int sliceStartY = slice * bitsPerColumn;
|
|
|
|
// Process each column (x position) in this slice
|
|
for (int x = 0; x < width; x++)
|
|
{
|
|
// For each byte in the column (1 byte for 8-dot, 3 bytes for 24-dot)
|
|
for (int byteInColumn = 0; byteInColumn < bytesPerColumn; byteInColumn++)
|
|
{
|
|
byte columnByte = 0;
|
|
|
|
// Pack 8 vertical pixels into one byte
|
|
for (int bit = 0; bit < 8; bit++)
|
|
{
|
|
int y = sliceStartY + (byteInColumn * 8) + bit;
|
|
|
|
// Get pixel value (white if outside image bounds)
|
|
byte pixelValue = 255; // Default to white
|
|
if (y < height)
|
|
{
|
|
image.ProcessPixelRows(accessor =>
|
|
{
|
|
var row = accessor.GetRowSpan(y);
|
|
pixelValue = row[x].PackedValue;
|
|
});
|
|
}
|
|
|
|
// Threshold: < 128 = black (1), >= 128 = white (0)
|
|
bool isBlack = pixelValue < 128;
|
|
|
|
if (isBlack)
|
|
{
|
|
columnByte |= (byte)(1 << (7 - bit)); // MSB = top pixel
|
|
}
|
|
}
|
|
|
|
columnData[dataIndex++] = columnByte;
|
|
}
|
|
}
|
|
}
|
|
|
|
return columnData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pack grayscale pixels into monochrome bytes (8 pixels per byte)
|
|
/// </summary>
|
|
private static byte[] PackPixelsToBytes(Image<L8> image, int paddedWidth)
|
|
{
|
|
int bytesPerRow = paddedWidth / 8;
|
|
int height = image.Height;
|
|
var rasterData = new byte[bytesPerRow * height];
|
|
|
|
image.ProcessPixelRows(accessor =>
|
|
{
|
|
for (int y = 0; y < height; y++)
|
|
{
|
|
var row = accessor.GetRowSpan(y);
|
|
int rowOffset = y * bytesPerRow;
|
|
|
|
for (int x = 0; x < paddedWidth; x++)
|
|
{
|
|
// Get pixel value (0 or 255 after dithering)
|
|
byte pixelValue = x < image.Width ? row[x].PackedValue : (byte)255;
|
|
|
|
// Threshold: < 128 = black (1), >= 128 = white (0)
|
|
bool isBlack = pixelValue < 128;
|
|
|
|
if (isBlack)
|
|
{
|
|
int byteIndex = rowOffset + (x / 8);
|
|
int bitPosition = 7 - (x % 8); // MSB first
|
|
rasterData[byteIndex] |= (byte)(1 << bitPosition);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return rasterData;
|
|
}
|
|
}
|