/** * Raster (bit-image) receipt bytes. ESC/POS text mode can't render Devanagari * or Malayalam (09-UX §5.3), so non-Latin receipts print as a 1-bit raster via * GS v 0. This module is the pure part — thresholding pixels and packing bits; * the canvas that draws the glyphs lives in the app (apps/pos/src/raster.ts), * because canvas is a browser API and byte-packing must stay testable. */ const GS = 0x1d /** 1 bit per pixel, row-major; `bits[y*width + x]` is 1 for a printed (black) dot. */ export interface Bitmap { width: number height: number bits: Uint8Array } /** * Pack one pixel row (0/1 values) into bytes, most-significant bit = leftmost * pixel, padded with zeros to a whole byte. 9 pixels → 2 bytes. */ export function packRasterRow(row: ArrayLike): number[] { const bytes = new Array(Math.ceil(row.length / 8)).fill(0) for (let i = 0; i < row.length; i++) { if (row[i]) bytes[i >> 3]! |= 0x80 >> (i & 7) } return bytes } /** * Threshold RGBA pixel data (canvas `ImageData.data`) into a 1-bpp bitmap. * A dot prints when it is opaque and darker than `threshold` (Rec.601 luma). */ export function bitonalFromRGBA( data: ArrayLike, width: number, height: number, threshold = 128, ): Bitmap { const bits = new Uint8Array(width * height) for (let i = 0; i < width * height; i++) { const r = data[i * 4]!, g = data[i * 4 + 1]!, b = data[i * 4 + 2]!, a = data[i * 4 + 3]! const luma = 0.299 * r + 0.587 * g + 0.114 * b bits[i] = a >= 128 && luma < threshold ? 1 : 0 } return { width, height, bits } } /** GS v 0 raster bit-image command for a bitmap (m=0, normal density). */ export function rasterToEscPos(bmp: Bitmap): Uint8Array { const bytesPerRow = Math.ceil(bmp.width / 8) const out: number[] = [GS, 0x76, 0x30, 0, bytesPerRow & 0xff, (bytesPerRow >> 8) & 0xff, bmp.height & 0xff, (bmp.height >> 8) & 0xff] for (let y = 0; y < bmp.height; y++) { const row = bmp.bits.subarray(y * bmp.width, (y + 1) * bmp.width) out.push(...packRasterRow(row)) } return Uint8Array.from(out) }