hardlink/printer/printer.go
2025-05-22 12:08:55 +01:00

344 lines
8.0 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 printer
import (
"bytes"
"encoding/xml"
"fmt"
"image"
"image/color"
"image/draw"
_ "image/jpeg"
_ "image/png"
"os"
"path/filepath"
"github.com/alexbrainman/printer"
log "github.com/sirupsen/logrus"
imagedraw "golang.org/x/image/draw"
)
type (
RoomDetailsRec struct {
XMLName xml.Name `xml:"roomdetails"`
Name string `xml:"customername"`
Checkout string `xml:"checkoutdatetime"`
RoomID string `xml:"roomno"`
Map string `xml:"roommap"`
Directions string `xml:"roomdirections"`
}
LayoutOptions struct {
XMLName xml.Name `xml:"LayoutOptions"`
LogoPath string `xml:"LogoPath"`
TopMostText string `xml:"TopMostText"`
BeforeRoomNumberText string `xml:"BeforeRoomNumberText"`
BeforeDirectionsText string `xml:"BeforeDirectionsText"`
HotelSpecificDetails string `xml:"HotelSpecificDetails"`
CheckOutTimeText string `xml:"CheckOutTimeText"`
RoomMapFolderPath string `xml:"RoomMapFolderPath"`
}
RoomTicket struct {
Logo string
TopMostText string
Name string
BeforeRoomNumberText string
RoomID string
BeforeDirectionsText string
Directions string
HotelSpecificDetails string
Map string
CheckOutTimeText string
Checkout string
}
)
var (
Layout LayoutOptions
PrinterName string
)
func BuildRoomTicket(details RoomDetailsRec) ([]byte, error) {
var buf bytes.Buffer
// shortcuts
write := func(b []byte) { buf.Write(b) }
writeStr := func(s string) { buf.WriteString(s) }
const (
ESC = 0x1B
GS = 0x1D
CENTER = 0x01
BOLD_ON = 0x01
BOLD_OFF = 0x00
LARGE_FONT = 0x11
WIDER_FONT = 0x10
NORMAL_FONT = 0x00
)
// 0) Hotel logo at top
logoBytes, err := printLogo(Layout.LogoPath)
if err != nil {
log.Printf("map render: %s", err.Error())
}
write(logoBytes)
writeStr("\n\n")
// 1) TopMostText
write([]byte{GS, '!', NORMAL_FONT})
write([]byte{ESC, 'a', CENTER})
writeStr(Layout.TopMostText + "\n\n")
// 2) Guest name
write([]byte{ESC, 'a', CENTER})
writeStr(details.Name + "\n\n")
// 3) "Your room number is"
write([]byte{ESC, 'a', CENTER})
writeStr(Layout.BeforeRoomNumberText + "\n\n")
// 4) RoomID in bold
write([]byte{ESC, 'a', CENTER})
write([]byte{GS, '!', WIDER_FONT})
writeStr(details.RoomID + "\n")
write([]byte{GS, '!', NORMAL_FONT})
writeStr("\n")
// 5) Directions label
write([]byte{ESC, 'a', CENTER})
writeStr(Layout.BeforeDirectionsText + "\n")
// 6) Directions text
write([]byte{ESC, 'a', CENTER})
writeStr(details.Directions + "\n\n")
// 7) Hotel-specific details
write([]byte{ESC, 'a', CENTER})
writeStr(Layout.HotelSpecificDetails + "\n\n")
// 8) Room map image
mapPath := filepath.Join(Layout.RoomMapFolderPath, details.Map)
mapBytes, err := printMap(mapPath)
if err != nil {
log.Printf("map render: %s", err.Error())
}
write(mapBytes)
writeStr("\n\n")
// 9) CheckOutTimeText label in bold
write([]byte{ESC, 'a', CENTER})
write([]byte{GS, '!', WIDER_FONT})
writeStr(Layout.CheckOutTimeText + "\n")
// 10) Actual Checkout value in bold
write([]byte{ESC, 'a', CENTER})
writeStr(details.Checkout + "\n\n\n")
write([]byte{GS, '!', NORMAL_FONT})
// 11) Final feed + cut
write([]byte{ESC, 'd', 3})
write([]byte{GS, 'V', 1})
return buf.Bytes(), nil
}
func printLogo(path string) ([]byte, error) {
const maxLogoWidth = 384
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open logo %q: %w", path, err)
}
defer f.Close()
srcImg, _, err := image.Decode(f)
if err != nil {
return nil, fmt.Errorf("decode logo %q: %w", path, err)
}
// 2) Composite over white
bounds := srcImg.Bounds()
whiteBg := image.NewRGBA(bounds)
draw.Draw(whiteBg, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src)
draw.Draw(whiteBg, bounds, srcImg, bounds.Min, draw.Over)
// 3) Scale if too wide
w, h := bounds.Dx(), bounds.Dy()
if w > maxLogoWidth {
ratio := float64(maxLogoWidth) / float64(w)
newW := maxLogoWidth
newH := int(float64(h) * ratio)
dst := image.NewRGBA(image.Rect(0, 0, newW, newH))
imagedraw.NearestNeighbor.Scale(dst, dst.Bounds(), whiteBg, bounds, imagedraw.Over, nil)
whiteBg = dst
w, h = newW, newH
}
// 4) Dither (FloydSteinberg)
gray := make([][]float32, h)
for y := 0; y < h; y++ {
gray[y] = make([]float32, w)
for x := 0; x < w; x++ {
r, g, b, _ := whiteBg.At(x, y).RGBA()
l := 0.299*float32(r)/65535 +
0.587*float32(g)/65535 +
0.114*float32(b)/65535
gray[y][x] = l
}
}
bin := make([][]bool, h)
for y := 0; y < h; y++ {
bin[y] = make([]bool, w)
for x := 0; x < w; x++ {
old := gray[y][x]
newV := float32(0.0)
if old > 0.5 {
newV = 1.0
}
bin[y][x] = (newV == 0.0)
err := old - newV
if x+1 < w {
gray[y][x+1] += err * 7 / 16
}
if y+1 < h {
if x > 0 {
gray[y+1][x-1] += err * 3 / 16
}
gray[y+1][x] += err * 5 / 16
if x+1 < w {
gray[y+1][x+1] += err * 1 / 16
}
}
}
}
// 5) Pack bits
widthBytes := (w + 7) / 8
var bitmap []byte
for y := 0; y < h; y++ {
for xb := 0; xb < widthBytes; xb++ {
var b byte
for bit := 0; bit < 8; bit++ {
px := xb*8 + bit
if px < w && bin[y][px] {
b |= 1 << (7 - bit)
}
}
bitmap = append(bitmap, b)
}
}
// 6) GS v 0 header
xL := byte(widthBytes & 0xFF)
xH := byte(widthBytes >> 8)
yL := byte(h & 0xFF)
yH := byte(h >> 8)
buf := bytes.NewBuffer(nil)
buf.Write([]byte{0x1D, 0x76, 0x30, 0x00, xL, xH, yL, yH})
buf.Write(bitmap)
return buf.Bytes(), nil
}
func printMap(path string) ([]byte, error) {
// 1) Open and decode the source image
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open image %q: %w", path, err)
}
defer f.Close()
srcImg, _, err := image.Decode(f)
if err != nil {
return nil, fmt.Errorf("decode image %q: %w", path, err)
}
// 2) Composite over white background to handle transparency
bounds := srcImg.Bounds()
whiteBg := image.NewRGBA(bounds)
draw.Draw(whiteBg, bounds, &image.Uniform{C: color.White}, image.Point{}, draw.Src)
draw.Draw(whiteBg, bounds, srcImg, bounds.Min, draw.Over)
// 3) Build monochrome bitmap
w, h := bounds.Dx(), bounds.Dy()
widthBytes := (w + 7) / 8
var bitmap []byte
for y := 0; y < h; y++ {
for xb := 0; xb < widthBytes; xb++ {
var b byte
for bit := 0; bit < 8; bit++ {
x := xb*8 + bit
if x >= w {
continue
}
gray := color.GrayModel.Convert(whiteBg.At(bounds.Min.X+x, bounds.Min.Y+y)).(color.Gray)
if gray.Y < 128 {
b |= 1 << (7 - bit)
}
}
bitmap = append(bitmap, b)
}
}
// 4) Prefix with ESC/POS raster image header (GS v 0)
xL := byte(widthBytes & 0xFF)
xH := byte((widthBytes >> 8) & 0xFF)
yL := byte(h & 0xFF)
yH := byte((h >> 8) & 0xFF)
cmd := bytes.NewBuffer(nil)
cmd.Write([]byte{0x1D, 0x76, 0x30, 0x00, xL, xH, yL, yH})
cmd.Write(bitmap)
return cmd.Bytes(), nil
}
func SendToPrinter(data []byte) error {
// Open the printer by its Windows name
h, err := printer.Open(PrinterName)
if err != nil {
return fmt.Errorf("printer.Open(%q): %w", PrinterName, err)
}
defer h.Close()
// Start a new RAW document
if err := h.StartDocument("ReceiptPrintJob", "RAW"); err != nil {
return fmt.Errorf("StartDocument RAW: %w", err)
}
defer h.EndDocument()
// Start the page
if err := h.StartPage(); err != nil {
return fmt.Errorf("StartPage: %w", err)
}
// Write the main receipt data
n, err := h.Write(data)
if err != nil {
return fmt.Errorf("Write data: %w", err)
}
if n < len(data) {
return fmt.Errorf("partial write: %d of %d bytes", n, len(data))
}
// Feed a few lines and issue a partial cut
// ESC d 3 (feeds 3 lines) -> 0x1B 0x64 0x03
// GS V 66 0 (partial cut) -> 0x1D 0x56 0x42 0x00
// feedAndCut := []byte{
// 0x1B, 0x64, 0x03, // Feed 3 lines
// 0x1D, 0x56, 0x42, 0x00, // Partial cut
// }
// if _, err := h.Write(feedAndCut); err != nil {
// return fmt.Errorf("Write feed+cut command: %w", err)
// }
// End the page
if err := h.EndPage(); err != nil {
return fmt.Errorf("EndPage: %w", err)
}
return nil
}