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/apps/pos/src/raster.ts

64 lines
2.4 KiB
TypeScript

import { EscPos, bitonalFromRGBA, rasterToEscPos } from '@sims/printing'
/**
* Indic raster receipt (09-UX §5.3): ESC/POS text mode can't render Devanagari
* or Malayalam, so non-Latin receipts are drawn to an offscreen canvas and sent
* as a GS v 0 bit-image. Canvas is a browser API, so this lives in the app; the
* thresholding + bit-packing are the pure @sims/printing package (raster.ts).
*
* v1 caveat (documented): browsers fall back to the OS fonts for canvas text, so
* Devanagari/Malayalam coverage depends on the shop PC having a suitable font
* (Windows ships Nirmala UI). The raster *pipeline* is what S2 delivers; bundled
* canvas webfonts are a follow-up. Latin/ASCII always renders.
*/
/** Print-head pixel width: 384 dots for 2" (32 col), 576 for 3" (42 col). */
export function headWidth(cols: 32 | 42): number {
return cols === 32 ? 384 : 576
}
export function renderRasterReceipt(lines: string[], opts: { width: 32 | 42; kickDrawer?: boolean }): Uint8Array {
const W = headWidth(opts.width)
const fontPx = opts.width === 32 ? 20 : 22
const lineH = Math.round(fontPx * 1.35)
const padTop = 8
const padBottom = 24
const H = padTop + lines.length * lineH + padBottom
const canvas = document.createElement('canvas')
canvas.width = W
canvas.height = H
const ctx = canvas.getContext('2d')
if (ctx === null) throw new Error('Canvas 2D unavailable — cannot rasterize receipt')
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, W, H)
ctx.fillStyle = '#000'
ctx.textBaseline = 'top'
// A font stack the browser can fall back to for Indic scripts on the shop PC.
ctx.font = `${fontPx}px "Noto Sans", "Noto Sans Devanagari", "Nirmala UI", "Segoe UI", monospace`
lines.forEach((text, i) => {
const y = padTop + i * lineH
if (i === 0) {
ctx.textAlign = 'center'
ctx.fillText(text, W / 2, y)
} else {
ctx.textAlign = 'left'
ctx.fillText(text, 4, y)
}
})
const img = ctx.getImageData(0, 0, W, H)
const body = rasterToEscPos(bitonalFromRGBA(img.data, W, H))
const head = new EscPos().init()
if (opts.kickDrawer === true) head.drawerKick()
const prefix = head.align('left').build()
const tail = new EscPos().feed(4).cut().build()
const out = new Uint8Array(prefix.length + body.length + tail.length)
out.set(prefix, 0)
out.set(body, prefix.length)
out.set(tail, prefix.length + body.length)
return out
}