You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/packages/printing/src/raster.ts

55 lines
2.0 KiB
TypeScript

/**
* 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>): number[] {
const bytes = new Array<number>(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<number>, 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)
}