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 (Floyd–Steinberg) 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 }