using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; namespace Inspectron.Epson; /// /// Helper class for converting images to ESC/POS raster format /// public static class EpsonImageConverter { /// /// Convert an image to ESC/POS raster format /// /// Source image /// Maximum width in pixels (will maintain aspect ratio) /// Output: final width after processing (byte-aligned) /// Output: final height after processing /// Byte array containing monochrome raster data public static byte[] ConvertToRasterData( Image 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); } /// /// Resize image to fit within max width while maintaining aspect ratio /// public static void ResizeImage(Image 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 })); } /// /// Convert RGBA image to grayscale /// public static Image ConvertToGrayscale(Image image) { var grayscaleImage = new Image(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; } /// /// Apply Floyd-Steinberg dithering algorithm for smooth 1-bit conversion /// public static void ApplyFloydSteinbergDithering(Image 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); } } }); } /// /// Convert image to column format bit image data for ESC * command /// /// Source image /// Maximum width in pixels /// Bit image mode (8-dot or 24-dot) /// Output: final width in dots /// Output: final height in dots /// Column format bit image data public static byte[] ConvertToColumnFormat( Image 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); } /// /// Pack pixels into column format (vertical bytes, left to right) /// private static byte[] PackPixelsToColumnFormat(Image 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; } /// /// Pack grayscale pixels into monochrome bytes (8 pixels per byte) /// private static byte[] PackPixelsToBytes(Image 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; } }