Files
printer-driver/emulator/internal/escpos/render.go
T
2026-09-30 10:22:42 +02:00

598 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package escpos
import (
"fmt"
"image"
"strings"
"time"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/gomono"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
"golang.org/x/text/encoding/charmap"
"rsc.io/qr"
)
// mode es el estado de impresión que modifican los comandos.
type mode struct {
fontB bool
bold bool
doubleStrike bool
underline int // 0, 1 o 2 puntos
widthMul int // 1..8
heightMul int // 1..8
reverse bool
upsideDown bool
align int // 0 izquierda, 1 centro, 2 derecha
lineSpacing int // puntos
rightSpacing int // puntos entre caracteres
codePage byte
intl byte
leftMargin int
areaWidth int
tabs []int // posiciones en columnas
qrModule byte
qrECC byte
qrData []byte
barcodeHeight int
barcodeWidth int
hriPos byte
}
func defaultMode(width int) mode {
tabs := make([]int, 0, 32)
for i := 8; i <= 256 && len(tabs) < 32; i += 8 {
tabs = append(tabs, i)
}
return mode{
widthMul: 1, heightMul: 1, lineSpacing: 30, areaWidth: width,
tabs: tabs, qrModule: 3, qrECC: 48, barcodeHeight: 162, barcodeWidth: 3,
}
}
func (m mode) describe() string {
parts := []string{"Fuente A"}
if m.fontB {
parts[0] = "Fuente B"
}
parts = append(parts, fmt.Sprintf("tamaño %d×%d", m.widthMul, m.heightMul))
if m.bold || m.doubleStrike {
parts = append(parts, "negrita")
}
if m.underline > 0 {
parts = append(parts, fmt.Sprintf("subrayado %d", m.underline))
}
if m.reverse {
parts = append(parts, "invertido")
}
parts = append(parts, []string{"izquierda", "centro", "derecha"}[m.align])
name := "desconocida"
if cp, ok := codePages[m.codePage]; ok {
name = cp.name
}
parts = append(parts, fmt.Sprintf("página %d (%s)", m.codePage, name))
if m.intl != 0 {
parts = append(parts, fmt.Sprintf("juego internacional %d", m.intl))
}
parts = append(parts, fmt.Sprintf("interlineado %d", m.lineSpacing))
return strings.Join(parts, " · ")
}
type codePage struct {
name string
cm *charmap.Charmap
}
// Números de tabla según ESC t n en impresoras Epson.
var codePages = map[byte]codePage{
0: {"PC437 EE.UU.", charmap.CodePage437},
2: {"PC850 Multilingüe", charmap.CodePage850},
3: {"PC860 Portugués", charmap.CodePage860},
4: {"PC863 Canadá-Francés", charmap.CodePage863},
5: {"PC865 Nórdico", charmap.CodePage865},
16: {"WPC1252", charmap.Windows1252},
17: {"PC866 Cirílico", charmap.CodePage866},
18: {"PC852 Latin 2", charmap.CodePage852},
19: {"PC858 Euro", charmap.CodePage858},
}
// Juegos internacionales (ESC R n): sustituyen algunos bytes ASCII.
var intlSets = map[byte]map[byte]rune{
7: {0x23: '₧', 0x40: '@', 0x5B: '¡', 0x5C: 'Ñ', 0x5D: '¿', 0x7B: '¨', 0x7C: 'ñ', 0x7D: '}'},
11: {0x40: 'á', 0x5B: '¡', 0x5C: 'Ñ', 0x5D: '¿', 0x5E: 'é', 0x60: 'ü', 0x7B: 'í', 0x7C: 'ñ', 0x7D: 'ó', 0x7E: 'ú'},
12: {0x40: 'á', 0x5B: '¡', 0x5C: 'Ñ', 0x5D: '¿', 0x5E: 'é', 0x7B: 'í', 0x7C: 'ñ', 0x7D: 'ó', 0x7E: 'ú'},
}
var intlNames = map[byte]string{0: "EE.UU.", 7: "España I", 11: "Latinoamérica", 12: "España II"}
func (p *Printer) decode(b byte) rune {
if b < 0x80 {
if r, ok := intlSets[p.st.intl][b]; ok {
return r
}
return rune(b)
}
cp, ok := codePages[p.st.codePage]
if !ok {
cp = codePages[0]
}
return cp.cm.DecodeByte(b)
}
// ---------------------------------------------------------------------------
// Papel: mapa de bits monocromo que crece hacia abajo, 1 byte por punto.
type paper struct {
width int
height int
pix []byte
version int
}
func newPaper(width int) *paper { return &paper{width: width} }
func (pp *paper) grow(h int) int {
start := pp.height
if h <= 0 {
return start
}
pp.pix = append(pp.pix, make([]byte, h*pp.width)...)
pp.height += h
pp.version++
return start
}
func (pp *paper) blit(x, y int, bits []byte, w, h int) {
for row := 0; row < h; row++ {
py := y + row
if py < 0 || py >= pp.height {
continue
}
for col := 0; col < w; col++ {
px := x + col
if px < 0 || px >= pp.width || bits[row*w+col] == 0 {
continue
}
pp.pix[py*pp.width+px] = 1
}
}
pp.version++
}
func (pp *paper) image(from, to int) *image.Gray {
img := image.NewGray(image.Rect(0, 0, pp.width, to-from))
src := pp.pix[from*pp.width : to*pp.width]
for i, v := range src {
if v != 0 {
img.Pix[i] = 0x22
} else {
img.Pix[i] = 0xFC
}
}
return img
}
// cut separa las filas [0, at) como ticket y deja el resto en la impresora.
func (pp *paper) cut(at int) *image.Gray {
img := pp.image(0, at)
rest := append([]byte(nil), pp.pix[at*pp.width:]...)
pp.pix = rest
pp.height -= at
pp.version++
return img
}
// ---------------------------------------------------------------------------
// Línea en curso: celdas (caracteres o imágenes de bits) pendientes de imprimir.
type cell struct {
x, w, h int
bits []byte
}
type lineBuf struct {
cells []cell
x int
align int
}
func (p *Printer) areaWidth() int {
w := p.st.areaWidth
if p.st.leftMargin+w > p.profile.WidthDots {
w = p.profile.WidthDots - p.st.leftMargin
}
if w < 0 {
w = 0
}
return w
}
func (p *Printer) sideMargin() int { return (p.profile.PaperDots - p.profile.WidthDots) / 2 }
func (p *Printer) alignOffset(align, used int) int {
free := p.areaWidth() - used
if free <= 0 {
return 0
}
switch align {
case 1:
return free / 2
case 2:
return free
}
return 0
}
func (p *Printer) placeCell(c cell, wrap bool) {
area := p.areaWidth()
if p.line.x+c.w > area && wrap && len(p.line.cells) > 0 {
p.printLine(p.st.lineSpacing)
}
if p.line.x+c.w > area {
c = c.cropWidth(area - p.line.x)
if c.w <= 0 {
return
}
}
if len(p.line.cells) == 0 {
// ESC a solo tiene efecto si se procesa al principio de la línea.
p.line.align = p.st.align
}
c.x = p.line.x
p.line.cells = append(p.line.cells, c)
p.line.x += c.w
}
func (c cell) cropWidth(w int) cell {
if w >= c.w {
return c
}
if w <= 0 {
return cell{}
}
bits := make([]byte, w*c.h)
for y := 0; y < c.h; y++ {
copy(bits[y*w:(y+1)*w], c.bits[y*c.w:y*c.w+w])
}
return cell{w: w, h: c.h, bits: bits}
}
func (p *Printer) lineHeight() int {
h := 0
for _, c := range p.line.cells {
if c.h > h {
h = c.h
}
}
return h
}
// printLine imprime la línea en curso y avanza como mínimo feed puntos.
func (p *Printer) printLine(feed int) {
lineH := p.lineHeight()
block := max(lineH, feed)
start := p.paper.grow(block)
if len(p.line.cells) > 0 {
x0 := p.sideMargin() + p.st.leftMargin + p.alignOffset(p.line.align, p.line.x)
for _, c := range p.line.cells {
p.paper.blit(x0+c.x, start+lineH-c.h, c.bits, c.w, c.h)
}
}
p.line = lineBuf{}
p.checkPaperLength()
}
func (p *Printer) flushLine() {
if len(p.line.cells) > 0 {
p.printLine(p.st.lineSpacing)
}
p.line = lineBuf{}
}
// printBlock imprime un bloque gráfico (raster, QR...) respetando ESC a.
func (p *Printer) printBlock(bits []byte, w, h int) {
c := cell{w: w, h: h, bits: bits}.cropWidth(p.areaWidth())
start := p.paper.grow(c.h)
x0 := p.sideMargin() + p.st.leftMargin + p.alignOffset(p.st.align, c.w)
p.paper.blit(x0, start, c.bits, c.w, c.h)
p.checkPaperLength()
}
func (p *Printer) checkPaperLength() {
if p.paper.height > maxPaperDots {
p.finishTicket(p.paper.height, "arrancado")
p.event("Más de %d mm sin cortar: el papel se arranca solo", maxPaperDots/8)
}
}
func (p *Printer) finishTicket(at int, kind string) {
p.ticketSeq++
t := &Ticket{Seq: p.ticketSeq, Time: time.Now(), Cut: kind, Image: p.paper.cut(at)}
p.tickets = append(p.tickets, t)
if len(p.tickets) > maxTickets {
p.tickets = append([]*Ticket(nil), p.tickets[len(p.tickets)-maxTickets:]...)
}
p.newTix = append(p.newTix, t)
}
// cut imprime lo pendiente, avanza feed puntos y corta por la cuchilla.
func (p *Printer) cut(s *Session, kind string, feed int) {
p.flushLine()
p.paper.grow(feed)
at := p.paper.height - p.profile.CutterOffset
if at <= 0 {
p.warn(s, "Corte sin papel impreso por encima de la cuchilla: no sale ningún ticket")
return
}
if p.profile.CutterOffset > 0 && feed < p.profile.CutterOffset {
p.warn(s, "La cuchilla está %d mm por encima del cabezal: las últimas %d filas se quedan para el siguiente ticket (usa GS V 65 n / 66 n para avanzar antes de cortar)",
p.profile.CutterOffset/8, p.profile.CutterOffset)
}
p.finishTicket(at, kind)
}
// ---------------------------------------------------------------------------
// Caracteres
type glyphFont struct {
w, h int
face font.Face
baseline int
cache map[rune][]byte
}
func loadFonts() (*glyphFont, *glyphFont, error) {
f, err := opentype.Parse(gomono.TTF)
if err != nil {
return nil, nil, err
}
// Go Mono avanza 0,6 em: 20 px -> 12 puntos (fuente A 12×24), 15 px -> 9 (fuente B 9×17).
a, err := newGlyphFont(f, 20, 12, 24)
if err != nil {
return nil, nil, err
}
b, err := newGlyphFont(f, 15, 9, 17)
if err != nil {
return nil, nil, err
}
return a, b, nil
}
func newGlyphFont(f *opentype.Font, size float64, w, h int) (*glyphFont, error) {
face, err := opentype.NewFace(f, &opentype.FaceOptions{Size: size, DPI: 72, Hinting: font.HintingFull})
if err != nil {
return nil, err
}
m := face.Metrics()
asc, desc := m.Ascent.Ceil(), m.Descent.Ceil()
return &glyphFont{w: w, h: h, face: face, baseline: asc + (h-(asc+desc))/2, cache: map[rune][]byte{}}, nil
}
func (g *glyphFont) glyph(r rune) []byte {
if bits, ok := g.cache[r]; ok {
return bits
}
img := image.NewAlpha(image.Rect(0, 0, g.w, g.h))
x := fixed.I(0)
if adv, ok := g.face.GlyphAdvance(r); ok {
x = (fixed.I(g.w) - adv) / 2
}
d := font.Drawer{Dst: img, Src: image.Opaque, Face: g.face, Dot: fixed.Point26_6{X: x, Y: fixed.I(g.baseline)}}
d.DrawString(string(r))
bits := make([]byte, g.w*g.h)
for i, a := range img.Pix {
if a >= 0x80 {
bits[i] = 1
}
}
g.cache[r] = bits
return bits
}
func (p *Printer) currentFont() *glyphFont {
if p.st.fontB {
return p.fonts[1]
}
return p.fonts[0]
}
// charCell dibuja un carácter con los atributos actuales, como haría el
// firmware: glifo de mapa de bits ampliado punto a punto.
func (p *Printer) charCell(r rune) cell {
f := p.currentFont()
g := f.glyph(r)
mw, mh := p.st.widthMul, p.st.heightMul
bold := p.st.bold || p.st.doubleStrike
w := (f.w + p.st.rightSpacing) * mw
h := f.h * mh
bits := make([]byte, w*h)
for y := 0; y < f.h; y++ {
for x := 0; x < f.w; x++ {
on := g[y*f.w+x] != 0 || (bold && x > 0 && g[y*f.w+x-1] != 0)
if !on {
continue
}
for dy := 0; dy < mh; dy++ {
row := (y*mh + dy) * w
for dx := 0; dx < mw; dx++ {
bits[row+x*mw+dx] = 1
}
}
}
}
if p.st.underline > 0 && !p.st.reverse {
for t := 0; t < p.st.underline; t++ {
row := (h - 1 - t) * w
for x := 0; x < w; x++ {
bits[row+x] = 1
}
}
}
if p.st.reverse {
for i := range bits {
bits[i] ^= 1
}
}
return cell{w: w, h: h, bits: bits}
}
func (p *Printer) addChar(r rune) { p.placeCell(p.charCell(r), true) }
func (p *Printer) tab() {
adv := p.currentFont().w + p.st.rightSpacing
for _, t := range p.st.tabs {
if pos := t * adv; pos > p.line.x {
if pos <= p.areaWidth() {
p.line.x = pos
}
return
}
}
}
// textBits dibuja un texto corto con la fuente B (para rótulos del emulador).
func (p *Printer) textBits(s string) ([]byte, int, int) {
f := p.fonts[1]
runes := []rune(s)
w, h := len(runes)*f.w, f.h
bits := make([]byte, w*h)
for i, r := range runes {
g := f.glyph(r)
for y := 0; y < f.h; y++ {
copy(bits[y*w+i*f.w:y*w+(i+1)*f.w], g[y*f.w:(y+1)*f.w])
}
}
return bits, w, h
}
// ---------------------------------------------------------------------------
// Imágenes
// addBitImage procesa ESC * (imagen por columnas, se imprime con la línea).
func (p *Printer) addBitImage(m byte, k int, data []byte) {
hs, vs, bpc := 1, 1, 3
switch m {
case 0:
hs, vs, bpc = 2, 3, 1
case 1:
hs, vs, bpc = 1, 3, 1
case 32:
hs, vs, bpc = 2, 1, 3
}
w, h := k*hs, 8*bpc*vs
bits := make([]byte, w*h)
for col := 0; col < k; col++ {
for bi := 0; bi < bpc; bi++ {
v := data[col*bpc+bi]
for bit := 0; bit < 8; bit++ {
if v&(0x80>>bit) == 0 {
continue
}
y0, x0 := (bi*8+bit)*vs, col*hs
for dy := 0; dy < vs; dy++ {
for dx := 0; dx < hs; dx++ {
bits[(y0+dy)*w+x0+dx] = 1
}
}
}
}
}
p.placeCell(cell{w: w, h: h, bits: bits}, false)
}
// printRaster procesa GS v 0 (imagen por filas, 1 bit por punto, MSB a la izquierda).
func (p *Printer) printRaster(m byte, xBytes, rows int, data []byte) {
sx, sy := 1, 1
if m&1 != 0 {
sx = 2
}
if m&2 != 0 {
sy = 2
}
w, h := xBytes*8*sx, rows*sy
bits := make([]byte, w*h)
for y := 0; y < rows; y++ {
for xb := 0; xb < xBytes; xb++ {
v := data[y*xBytes+xb]
for bit := 0; bit < 8; bit++ {
if v&(0x80>>bit) == 0 {
continue
}
x0 := (xb*8 + bit) * sx
for dy := 0; dy < sy; dy++ {
for dx := 0; dx < sx; dx++ {
bits[(y*sy+dy)*w+x0+dx] = 1
}
}
}
}
}
p.flushLine()
p.printBlock(bits, w, h)
}
func (p *Printer) printQR(s *Session) {
if len(p.st.qrData) == 0 {
p.warn(s, "GS ( k 81: no hay datos QR almacenados (falta la función 80)")
return
}
level := map[byte]qr.Level{48: qr.L, 49: qr.M, 50: qr.Q, 51: qr.H}[p.st.qrECC]
code, err := qr.Encode(string(p.st.qrData), level)
if err != nil {
p.warn(s, "QR no generado: %v", err)
return
}
ms := int(p.st.qrModule)
w := code.Size * ms
bits := make([]byte, w*w)
for y := 0; y < code.Size; y++ {
for x := 0; x < code.Size; x++ {
if !code.Black(x, y) {
continue
}
for dy := 0; dy < ms; dy++ {
for dx := 0; dx < ms; dx++ {
bits[(y*ms+dy)*w+x*ms+dx] = 1
}
}
}
}
p.flushLine()
p.printBlock(bits, w, w)
}
var barcodeNames = map[byte]string{
0: "UPC-A", 1: "UPC-E", 2: "EAN13", 3: "EAN8", 4: "CODE39", 5: "ITF", 6: "CODABAR",
65: "UPC-A", 66: "UPC-E", 67: "EAN13", 68: "EAN8", 69: "CODE39", 70: "ITF", 71: "CODABAR",
72: "CODE93", 73: "CODE128",
}
// printBarcode dibuja un marcador: los códigos de barras no se emulan.
func (p *Printer) printBarcode(m byte, data []byte) {
name := barcodeNames[m]
if name == "" {
name = fmt.Sprintf("tipo %d", m)
}
label, lw, lh := p.textBits(fmt.Sprintf("[%s] %s", name, strings.ToValidUTF8(string(data), "?")))
w := min(max(lw+16, 200), p.areaWidth())
h := max(p.st.barcodeHeight, lh+8)
bits := make([]byte, w*h)
for x := 0; x < w; x++ {
bits[x], bits[(h-1)*w+x] = 1, 1
}
for y := 0; y < h; y++ {
bits[y*w], bits[y*w+w-1] = 1, 1
}
oy, ox := (h-lh)/2, 8
for y := 0; y < lh; y++ {
for x := 0; x < lw && ox+x < w-1; x++ {
bits[(oy+y)*w+ox+x] = label[y*lw+x]
}
}
p.flushLine()
p.printBlock(bits, w, h)
}