feat: add ESC/POS renderer
This commit is contained in:
parent
c561dae461
commit
6cc292d61a
84
src/engine/escpos.ts
Normal file
84
src/engine/escpos.ts
Normal file
@ -0,0 +1,84 @@
|
||||
// ESC/POS 指令常量
|
||||
export const ESC = 0x1B;
|
||||
export const GS = 0x1D;
|
||||
export const LF = 0x0A;
|
||||
export const CR = 0x0D;
|
||||
|
||||
// 初始化打印机
|
||||
export function initialize(): Uint8Array {
|
||||
return new Uint8Array([ESC, 0x40]);
|
||||
}
|
||||
|
||||
// 切纸
|
||||
export function cut(): Uint8Array {
|
||||
return new Uint8Array([GS, 0x56, 0x00]);
|
||||
}
|
||||
|
||||
// 文本编码
|
||||
export function text(str: string): Uint8Array {
|
||||
const encoder = new TextEncoder();
|
||||
return encoder.encode(str);
|
||||
}
|
||||
|
||||
// 换行
|
||||
export function newline(): Uint8Array {
|
||||
return new Uint8Array([LF]);
|
||||
}
|
||||
|
||||
// 加粗
|
||||
export function bold(on: boolean): Uint8Array {
|
||||
return new Uint8Array([ESC, 0x45, on ? 1 : 0]);
|
||||
}
|
||||
|
||||
// 下划线
|
||||
export function underline(on: boolean): Uint8Array {
|
||||
return new Uint8Array([ESC, 0x2D, on ? 1 : 0]);
|
||||
}
|
||||
|
||||
// 对齐
|
||||
export function align(align: 'left' | 'center' | 'right'): Uint8Array {
|
||||
const n = align === 'left' ? 0 : align === 'center' ? 1 : 2;
|
||||
return new Uint8Array([ESC, 0x61, n]);
|
||||
}
|
||||
|
||||
// 字体大小
|
||||
export function fontSize(size: 'small' | 'normal' | 'large' | 'xlarge'): Uint8Array {
|
||||
let n = 0;
|
||||
switch (size) {
|
||||
case 'small': n = 0; break;
|
||||
case 'normal': n = 0; break;
|
||||
case 'large': n = 0x11; break;
|
||||
case 'xlarge': n = 0x22; break;
|
||||
}
|
||||
return new Uint8Array([GS, 0x21, n]);
|
||||
}
|
||||
|
||||
// 恢复默认字体大小
|
||||
export function resetFontSize(): Uint8Array {
|
||||
return new Uint8Array([GS, 0x21, 0]);
|
||||
}
|
||||
|
||||
// 打印条码 (CODE128)
|
||||
export function barcodeCode128(data: string, height: number = 64): Uint8Array {
|
||||
const encoder = new TextEncoder();
|
||||
const dataBytes = encoder.encode(data);
|
||||
return new Uint8Array([
|
||||
GS, 0x68, height,
|
||||
GS, 0x6B, 0x49, dataBytes.length,
|
||||
...dataBytes,
|
||||
0
|
||||
]);
|
||||
}
|
||||
|
||||
// 打印 QR 码
|
||||
export function qrCode(data: string, size: number = 3): Uint8Array {
|
||||
const encoder = new TextEncoder();
|
||||
const dataBytes = encoder.encode(data);
|
||||
const len = dataBytes.length + 3;
|
||||
return new Uint8Array([
|
||||
GS, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x43, size,
|
||||
GS, 0x28, 0x6B, len & 0xFF, (len >> 8) & 0xFF, 0x31, 0x50, 0x30,
|
||||
...dataBytes,
|
||||
GS, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30
|
||||
]);
|
||||
}
|
||||
73
src/engine/render.ts
Normal file
73
src/engine/render.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import Mustache from 'mustache';
|
||||
import type { Template, Block } from '../types/template';
|
||||
import * as escpos from './escpos';
|
||||
|
||||
export function renderTemplate(template: Template, data: any): Uint8Array {
|
||||
const buffers: Uint8Array[] = [];
|
||||
|
||||
// 初始化打印机
|
||||
buffers.push(escpos.initialize());
|
||||
|
||||
// 渲染每个 block
|
||||
for (const block of template.blocks) {
|
||||
buffers.push(renderBlock(block, data, template.defaults));
|
||||
}
|
||||
|
||||
// 切纸
|
||||
buffers.push(escpos.cut());
|
||||
|
||||
return mergeBuffers(buffers);
|
||||
}
|
||||
|
||||
function renderBlock(block: Block, data: any, defaults: any = {}): Uint8Array {
|
||||
// 根据 block 类型渲染
|
||||
switch (block.type) {
|
||||
case 'text':
|
||||
return renderTextBlock(block, data);
|
||||
case 'divider':
|
||||
return renderDividerBlock(block);
|
||||
case 'space':
|
||||
return renderSpaceBlock(block);
|
||||
default:
|
||||
console.warn(`Unknown block type: ${(block as any).type}`);
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTextBlock(block: any, data: any): Uint8Array {
|
||||
const buffers: Uint8Array[] = [];
|
||||
buffers.push(escpos.align(block.align || 'left'));
|
||||
buffers.push(escpos.bold(block.bold || false));
|
||||
const content = Mustache.render(block.content, data);
|
||||
buffers.push(escpos.text(content));
|
||||
buffers.push(escpos.newline());
|
||||
buffers.push(escpos.bold(false));
|
||||
buffers.push(escpos.align('left'));
|
||||
return mergeBuffers(buffers);
|
||||
}
|
||||
|
||||
function renderDividerBlock(block: any): Uint8Array {
|
||||
const char = block.char || '-';
|
||||
const line = char.repeat(48);
|
||||
return mergeBuffers([escpos.text(line), escpos.newline()]);
|
||||
}
|
||||
|
||||
function renderSpaceBlock(block: any): Uint8Array {
|
||||
const lines = block.lines || 1;
|
||||
const buffers: Uint8Array[] = [];
|
||||
for (let i = 0; i < lines; i++) {
|
||||
buffers.push(escpos.newline());
|
||||
}
|
||||
return mergeBuffers(buffers);
|
||||
}
|
||||
|
||||
function mergeBuffers(buffers: Uint8Array[]): Uint8Array {
|
||||
const totalLength = buffers.reduce((sum, buf) => sum + buf.length, 0);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const buf of buffers) {
|
||||
result.set(buf, offset);
|
||||
offset += buf.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
52
tests/render.test.ts
Normal file
52
tests/render.test.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
import { renderTemplate } from '../src/engine/render';
|
||||
import type { Template } from '../src/types/template';
|
||||
|
||||
describe('Template Renderer', () => {
|
||||
it('should render text block', () => {
|
||||
const template: Template = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
width: '80mm',
|
||||
blocks: [
|
||||
{ type: 'text', content: 'Hello World', align: 'center' }
|
||||
]
|
||||
};
|
||||
|
||||
const data = {};
|
||||
const result = renderTemplate(template, data);
|
||||
expect(result).toBeInstanceOf(Uint8Array);
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should substitute mustache variables', () => {
|
||||
const template: Template = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
width: '80mm',
|
||||
blocks: [
|
||||
{ type: 'text', content: 'Hello {{name}}' }
|
||||
]
|
||||
};
|
||||
|
||||
const data = { name: 'Alice' };
|
||||
const result = renderTemplate(template, data);
|
||||
const text = new TextDecoder().decode(result);
|
||||
expect(text).toContain('Hello Alice');
|
||||
});
|
||||
|
||||
it('should render divider', () => {
|
||||
const template: Template = {
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
width: '80mm',
|
||||
blocks: [
|
||||
{ type: 'divider', char: '=' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = renderTemplate(template, {});
|
||||
const text = new TextDecoder().decode(result);
|
||||
expect(text).toContain('========');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user