Hace ya algunos meses me tocó trabajar remotamente con una impresora térmica que se describe como:
Impresora térmica TYP-5801 58mm portátil TypStar®

Comencé a hacer las pruebas básicas y me encontré con el hecho de que la impresora no puede imprimir texto plano con comandos ESC POS.
En este post voy a compartir lo que hice para poder imprimir tickets de manera programada en esta impresora que solo soporta imágenes y algunos pensamientos sobre este método.
Impresora no imprime texto
Por más que compartí la impresora, le cambié de nombre, la instalé como genérica, con sus controladores, etcétera la impresora no imprimía texto plano o texto nativo ESC POS.
Fue hasta que probé en el playground de mi plugin HTTP a ESC POS imprimiendo un ticket de prueba con texto e imágenes y noté que no se imprimía ningún texto, solamente el código QR que había colocado.
Lo que me alegró fue que sí podía imprimir imágenes, así que aunque tuviera que generar imágenes y luego imprimirlas (lo cual terminé haciendo) sabía que la impresora sí iba a funcionar.
He visto impresoras que hacen justamente lo contrario: solo soportan texto pero nada de imágenes o solo cierto algoritmo de impresión de imagen.
Creando imagen a partir de texto
Como estoy usando Golang voy a usar el mismo método ya presentado previamente para agregar texto a una imagen con Golang usando la maravillosa librería de fogleman
Tengo esta función ayudante:
func dibujarTextoDevolviendoImagen(ancho, tamañoFuente float64, espaciadoDeLineas float64, texto string, alineacion int, ubicacionFuente string) (image.Image, float64, error) {
alto, err := altoDeImagenNecesarioSegunFuente(ubicacionFuente, tamañoFuente, ancho, texto, espaciadoDeLineas)
if err != nil {
return nil, 0, err
}
dc := gg.NewContext(int(ancho), int(alto))
dc.SetRGB(1, 1, 1) // Fondo
dc.Clear()
dc.SetRGB(0, 0, 0) // Color del texto
if err := dc.LoadFontFace(ubicacionFuente, tamañoFuente); err != nil {
return nil, 0, err
}
dc.DrawStringWrapped(
texto,
0, 0,
0, 0,
ancho, espaciadoDeLineas,
gg.Align(alineacion))
return dc.Image(), alto, nil
}
Esa función solo dibuja el texto que le indico y se encarga de calcular el alto de la imagen ya que la misma será creada desde cero y su altura estará definida por la cantidad de texto:
func altoDeImagenNecesarioSegunFuente(ubicacionFuente string, tamañoFuente float64, ancho float64, texto string, espaciadoDeLineas float64) (float64, error) {
fontFace, err := gg.LoadFontFace(ubicacionFuente, tamañoFuente)
if err != nil {
return 0.0, err
}
dc := gg.NewContext(int(ancho), 10) // El alto inicial no importa
dc.SetFontFace(fontFace)
lines := dc.WordWrap(texto, ancho)
h := float64(len(lines)) * dc.FontHeight() * espaciadoDeLineas
h -= (espaciadoDeLineas - 1) * espaciadoDeLineas
return h, nil
}
Una vez que ya tengo esas funciones simplemente las invoco a través de una función ayudante que dibuja texto en una imagen según la alineación y devuelve un image.Image:
func dibujarTextoConValoresPorDefecto(ancho float64, alineacion int, texto string) (image.Image, float64, error) {
tamañoFuente := 24.00
espaciado := 1.5
return dibujarTextoDevolviendoImagen(ancho, tamañoFuente, espaciado, texto, alineacion, "cour.ttf")
}
Para que finalmente y ahora sí, convierta esa image.Image en un montón de bytes entendible por la impresora térmica.
func obtenerBytesDeImagenSegunTexto(alineacion int, texto string) ([]byte, error) {
ancho := 384.00
imagen, alto, err := dibujarTextoConValoresPorDefecto(ancho, alineacion, texto)
if err != nil {
return nil, err
}
return obtenerBytesParaBitImage(imagen, int(ancho), int(alto)), nil
}
La ventaja de esto es que no importa cuánto manipulemos la imagen. Podemos cargarla desde una plantilla, crearla desde cero, montar 100 imágenes, etcétera, ya que a esta función solo le interesa recibir un image.Image.
Otra cosa que noto es que, si bien no es imposible y de hecho hasta tenemos más libertad en ese aspecto, es complejo aplicarle formato al texto. Con texto ESC POS nativo podemos cambiar la alineación, el enfatizado, etcétera y la impresora se encarga de calcular todo.
Por otro lado con este método tenemos más libertad porque podemos modificar cualquier pixel de la imagen antes de imprimirla, cargar más fuentes, cambiar el tamaño de la fuente, sobreponer imágenes, etcétera pero es más tedioso.
Finalmente, con el siguiente código:
func onInicioConfiguracionTurnoImagen() error {
texto := fmt.Sprintf("********************************\nMensaje\n********************************\nIngrese el Turno\n********************************")
return imprimirTextoComoImagen(1, texto)
}
Obtengo el siguiente resultado:

Como puedes ver es totalmente legible.
Conclusiones sobre este método
Quiero comentar que la impresión es realmente rápida. Aunque desde mi punto de vista se ve como algo lento el tener que crear una imagen, cargarla en memoria, escribirle texto, convertirla a bytes ESC POS y luego enviar esos bytes a la impresora en realidad es algo que Golang hace en milisegundos que ni siquiera se nota.
Con respecto a la calidad, no se nota a simple vista que el texto impreso no es nativo. De hecho mucho software y métodos de impresión se evitan imprimir texto nativo con comandos ESC POS y en su lugar generan una imagen y la imprimen, lo cual es totalmente válido porque no importa si la impresora soporta o no la impresión de texto plano.
Nota importante: desde el inicio aclaré que trabajé con la impresora de forma remota. No puedo asegurar al 100 % que la impresora realmente haya sido instalada como genérica y/o con sus controladores originales. Escribo este artículo también para mostrar una alternativa cuando no es posible imprimir texto plano en las impresoras térmicas.