Files
Print_server/Inspectron.Epson/EpsonImageConverter.cs
2026-01-13 09:06:47 +01:00

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;
}
}