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) }