598 lines
14 KiB
Go
598 lines
14 KiB
Go
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)
|
||
}
|