initial commit

This commit is contained in:
2026-09-30 10:22:42 +02:00
commit 3b0f5e365f
32 changed files with 4498 additions and 0 deletions
+597
View File
@@ -0,0 +1,597 @@
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)
}