hardlink/printer/printer.go

475 lines
11 KiB
Go
Raw Permalink 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"
"strings"
"github.com/alexbrainman/printer"
log "github.com/sirupsen/logrus"
imagedraw "golang.org/x/image/draw"
)
type (
CardholderReceipt struct {
XMLName xml.Name `xml:"ReceiptData"`
ReceiptEntries struct {
ReceiptEntry []ReceiptEntryXML `xml:"ReceiptEntry"`
} `xml:"ReceiptEntries"`
}
ReceiptEntryXML struct {
ReceiptEntryId string `xml:"ReceiptEntryId"`
Value string `xml:"Value"`
Label string `xml:"Label"`
ReceiptItemType string `xml:"ReceiptItemType"`
Priority string `xml:"Priority"`
}
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
}
)
const (
ESC = 0x1B
GS = 0x1D
CENTER = 0x01
BOLD_ON = 0x01
BOLD_OFF = 0x00
LARGE_FONT = 0x11
WIDER_FONT = 0x10
NORMAL_FONT = 0x00
)
var (
Layout LayoutOptions
PrinterName string
)
func ParseCardholderReceipt(data []byte) ([]ReceiptEntryXML, error) {
var tr CardholderReceipt
if err := xml.Unmarshal(data, &tr); err != nil {
return nil, fmt.Errorf("XML unmarshal: %w", err)
}
return tr.ReceiptEntries.ReceiptEntry, nil
}
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) }
// 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 PrintCardholderReceipt(cardholderReceipt string) error {
receiptEntries, err := ParseCardholderReceipt([]byte(cardholderReceipt))
if err != nil {
return fmt.Errorf("ParseCardholderReceipt: %w", err)
}
data, err := BuildCardholderReceipt(receiptEntries)
if err != nil {
return fmt.Errorf("BuildCardholderReceipt: %w", err)
}
// Send to the Windows Epson TM-T82II via the printer package
if err := SendToPrinter(data); err != nil {
return fmt.Errorf("SendToPrinter: %w", err)
}
log.Info("Cardholder receipt printed successfully")
return nil
}
func BuildCardholderReceipt(entries []ReceiptEntryXML) ([]byte, error) {
var buf bytes.Buffer
write := func(b []byte) { buf.Write(b) }
// writeStr := func(s string) { buf.WriteString(s) }
writeln := func(s string) {
buf.WriteString(s)
buf.WriteByte('\n')
}
// Build a lookup map by ReceiptEntryId
m := make(map[string]ReceiptEntryXML, len(entries))
for _, e := range entries {
m[e.ReceiptEntryId] = e
}
// Utility to center text
// center := func(s string) {
// write([]byte{ESC, 'a', CENTER})
// writeln(s)
// write([]byte{ESC, 'a', 0})
// }
// Utility for bold lines
// boldOn := func() { write([]byte{GS, '!', WIDER_FONT}) }
// boldOff := func() { write([]byte{GS, '!', NORMAL_FONT}) }
// boldOn()
// 1) Header: CARDHOLDER COPY
writeln(m["Recipient"].Value)
// writeStr("\n\n")
// 2) Merchant name
writeln(m["MerchantName"].Value)
// writeStr("\n\n")
// 3) AID
writeln("AID: " + m["Aid"].Value)
// 4) Card scheme & pan
writeln(fmt.Sprintf("%s Card: %s", m["CardScheme"].Value, m["PanMasked"].Value))
// 5) PAN Seq No
writeln("PAN Seq No: " + m["PanSequence"].Value)
// 6) Transaction source
writeln(m["TransactionSource"].Value)
// 7) Transaction type (Sale/Account Verification)
writeln(m["TransactionType"].Value)
// 8) Total
// assuming value like "GBP1.50" — insert space after currency
total := m["TotalAmount"].Value
if !strings.HasPrefix(total, "GBP") && len(total) > 3 {
total = total[:3] + " " + total[3:]
}
writeln("TOTAL: " + total)
// 9) Cardholder verification
writeln(m["CardholderVerification"].Value)
// 10) Approved/Declined
writeln(m["TransactionResult"].Value)
// 11) Auth code
writeln("Auth Code: " + m["AuthCode"].Value)
// 12) Reference
writeln("Ref: " + m["AuthReference"].Value)
// 13) Merchant & terminal IDs
writeln("MID: " + m["MerchantIdMasked"].Value)
writeln("TID: " + m["TerminalIdMasked"].Value)
// 14) Date/time
writeln(m["AuthDateTime"].Value)
// 15) Retention message
writeln(m["RetentionMessage"].Value)
// boldOff()
// finally feed & cut
write([]byte{ESC, 'd', 7}) // Feed 5 lines
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
}