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.
64 lines
2.4 KiB
TypeScript
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
|
|
}
|