7 Commits

Author SHA1 Message Date
016dad446f add CheckPrinter 2026-10-02 22:35:05 +06:30
06f38b707e v0.1.11 2026-09-09 22:43:57 +06:30
5f4a427f3b split big chunk of data to write to printer 2026-09-08 17:24:54 +06:30
7bb8778611 pump to 0.1.10 2026-09-03 09:23:58 +06:30
58c029b5f2 fix format time to MM timezone 2026-09-03 09:00:13 +06:30
8a23c7037f v0.1.9 - add <cut> element 2026-07-06 16:11:31 +06:30
29ec943d13 add date format 2026-06-04 11:16:13 +06:30
7 changed files with 255 additions and 102 deletions

1
.gitignore vendored
View File

@@ -30,3 +30,4 @@ assets
libgofunc
libgo
cmd/out.png
cmd/*.png

View File

@@ -1,7 +1,7 @@
#!/bin/bash
APP_NAME="libgofunc"
VERSION="${1:-v0.1.7}"
VERSION="${1:-v0.1.12}"
OUTPUT_DIR="../assets"
BUILD_DIR="../build"

File diff suppressed because one or more lines are too long

1
go.mod
View File

@@ -12,5 +12,6 @@ require (
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/uuid v1.6.0
golang.org/x/text v0.35.0 // indirect
)

2
go.sum
View File

@@ -4,6 +4,8 @@ github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kenshaw/escpos v0.0.0-20221114190919-df06b682a8fc h1:4JwmN2Scz1vR+hfSxkdy2IE/DzxX2Cftm2lhWHyN0k0=
github.com/kenshaw/escpos v0.0.0-20221114190919-df06b682a8fc/go.mod h1:M+GIBmg2MqaSWIJrXCZS+/wRFbr9fOguRz3SHn8DRPE=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=

103
img.go
View File

@@ -14,6 +14,7 @@ import (
"strings"
"embed"
_ "time/tzdata"
"github.com/fogleman/gg"
"golang.org/x/image/font"
@@ -21,14 +22,14 @@ import (
"golang.org/x/net/html"
"encoding/json"
"github.com/kenshaw/escpos"
)
import (
"log"
"path"
"strconv"
"time"
"github.com/google/uuid"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
@@ -47,9 +48,9 @@ const (
var fontFs embed.FS
//export GenPNG
func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char, workingDirC *C.char) *C.char {
func GenPNG(width C.int, outDir *C.char, payload *C.char, tmpl *C.char, workingDirC *C.char) *C.char {
canvasWidth := int(width)
goPath := C.GoString(outputPath)
goOutDir := C.GoString(outDir)
goPayload := C.GoString(payload)
goTmpl := C.GoString(tmpl)
workingDir := C.GoString(workingDirC)
@@ -66,14 +67,27 @@ func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char, work
}
htmlStr = uni2zg(htmlStr)
htmls := strings.Split(htmlStr, "<cut>")
result := make([]string, 0)
for _, html := range htmls {
out, err := GenPng(canvasWidth, goOutDir, html, workingDir)
if err != nil {
return NewErr(err)
}
result = append(result, out)
}
return NewOk(result)
}
func GenPng(canvasWidth int, outDir string, htmlStr string, workingDir string) (string, error) {
root, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
return NewErr(err)
return "", err
}
face, err := LoadFont(defalutFontSize)
if err != nil {
return NewErr(err)
return "", err
}
// First pass: compute required height
@@ -95,27 +109,27 @@ func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char, work
y := 0
err = renderNode(dc, canvasWidth, body, &y, *face, workingDir)
if err != nil {
return NewErr(err)
return "", err
}
err = dc.SavePNG(goPath)
outPath := path.Join(outDir, uuid.NewString()+".png")
err = dc.SavePNG(outPath)
if err != nil {
return NewErr(err)
return "", err
}
// PrintReceipt(goPath)
return NewOk(nil)
return outPath, nil
}
func GenImg(width int, outputPath, payload, tmpl, workingDir string) string {
result := GenPNG(C.int(width), C.CString(outputPath), C.CString(payload), C.CString(tmpl), C.CString(workingDir))
func GenImg(width int, outputDir, payload, tmpl, workingDir string) string {
result := GenPNG(C.int(width), C.CString(outputDir), C.CString(payload), C.CString(tmpl), C.CString(workingDir))
r := C.GoString(result)
return r
}
var funcMap = template.FuncMap{
"formatNumber": FormatNumber,
"div": Div,
"formatNumber": FormatNumber,
"div": Div,
"formatDateFromSec": formatDateFromSec,
}
func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
@@ -160,7 +174,8 @@ func renderNode(dc *gg.Context, canvasWidth int, n *Node, y *int, face font.Face
if n.Style.PaddingBottom > 0 {
*y += int(n.Style.PaddingBottom)
}
log.Printf("render %s y, y', height: %d, %d, %d\n", n.Tag, before, *y, *y-before)
_ = before
// log.Printf("render %s y, y', height: %d, %d, %d\n", n.Tag, before, *y, *y-before)
for _, c := range n.Children {
renderNode(dc, canvasWidth, c, y, face, workingDir)
@@ -360,30 +375,25 @@ func findNode(n *html.Node, tag string) *html.Node {
return nil
}
func printImg(prt *escpos.Escpos, imgPath string) error {
func imgData(imgPath string) ([]byte, error) {
imgFile, err := os.Open(imgPath)
if err != nil {
fmt.Println(err)
return err
return nil, err
}
img, _, err := image.Decode(imgFile)
defer imgFile.Close()
if err != nil {
fmt.Println(err)
return err
return nil, err
}
data := []byte{0x1D, 0x4C, 0x00, 0x00}
_, err = prt.WriteRaw(data)
if err != nil {
fmt.Println("error 0x1D, 0x4C:", err.Error())
}
gray := toMonochrome(img)
data = escposRaster(gray)
_, err = prt.WriteRaw(data)
return err
imgData := escposRaster(gray)
data = append(data, imgData...)
return data, err
}
func LoadFont(size float64) (*font.Face, error) {
@@ -451,7 +461,7 @@ func nodeHeight(n *html.Node, dc *gg.Context, canvasWidth int, xPadding, yPaddin
h = implicit
}
y += h
log.Printf("nodeHeight %s height: %d, \n", n.Data, y)
// log.Printf("nodeHeight %s height: %d, \n", n.Data, y)
}
return y
}
@@ -518,3 +528,38 @@ func convertToFloat64(v any) float64 {
return 0
}
}
func formatDateFromSec(input any, layout string) string {
var sec int64
switch v := input.(type) {
case int64:
sec = v
case int:
sec = int64(v)
case float64:
sec = int64(v)
case float32:
sec = int64(v)
case string:
parsed, err := strconv.ParseInt(v, 10, 64)
if err != nil {
if parsedFloat, errFloat := strconv.ParseFloat(v, 64); errFloat == nil {
sec = int64(parsedFloat)
} else {
return fmt.Sprintf("[Error: invalid time string %q]", v)
}
} else {
sec = parsed
}
default:
return "[Error: unsupported time type]"
}
t := time.UnixMilli(sec * 1000)
loc, err := time.LoadLocation("Asia/Rangoon")
if err != nil {
return fmt.Sprintf("[Error: unable to load TZ %v]", err)
}
t = t.In(loc)
return t.Format(layout)
}

View File

@@ -15,92 +15,122 @@ import (
"time"
"errors"
"github.com/kenshaw/escpos"
)
import "errors"
import "io"
//export PrintImg
func PrintImg(printer *C.char, imagePath *C.char) *C.char {
goPrinter := C.GoString(printer)
goImagePath := C.GoString(imagePath)
// printer := "tcp:192.168.100.151:9100"
// printer := "usb:/dev/usb/lp1"
var w *bufio.ReadWriter
if strings.HasPrefix(goPrinter, "tcp:") {
location := strings.TrimLeft(goPrinter, "tcp:")
conn, err := net.Dial("tcp", location)
if err != nil {
log.Println("error dialing:", err.Error())
return NewErr(err)
}
defer conn.Close()
w = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
} else if strings.HasPrefix(goPrinter, "usb:") {
location := strings.TrimLeft(goPrinter, "usb:")
f, err := os.OpenFile(location, os.O_RDWR, os.ModeDevice)
if err != nil {
log.Println("error OpenFile usb:", err.Error())
return NewErr(err)
}
defer f.Close()
w = bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f))
// } else if strings.HasPrefix(goPrinter, "int:") {
// ctx := gousb.NewContext()
// // location := strings.TrimLeft(goPrinter, "int:")
// targetBus := 1
// targetAddr := 5
// devs, err := ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool {
// return int(desc.Bus) == targetBus && int(desc.Address) == targetAddr
// })
// if err != nil || len(devs) == 0 {
// log.Fatal("Could not find or open the device")
// }
// dev := devs[0]
// defer dev.Close()
// dev.SetAutoDetach(true)
// // 2. Claim the default interface (usually 0 for printers)
// // Note: This may require detaching the kernel driver on Linux
// intf, done, err := dev.DefaultInterface()
// if err != nil {
// log.Fatalf("Failed to claim interface: %v", err)
// }
// defer done()
// // 3. Open the Bulk Output Endpoint (usually endpoint #1 or #2)
// // You may need to inspect desc.Endpoints to find the correct Bulk Out ID
// out, err = intf.OutEndpoint(1)
// if err != nil {
// log.Fatalf("Failed to open OUT endpoint: %v", err)
// }
// // w = bufio.NewReadWriter(bufio.NewReader(outPort), bufio.NewWriter(f))
// rw := &USBReadWriter{out: out}
// reader := bufio.NewReader(rw)
// writer := bufio.NewWriter(rw)
// w = bufio.NewReadWriter(reader, writer)
} else {
return NewErr(errors.New("invalid printer"))
err := Print(goPrinter, goImagePath)
if err != nil {
return NewErr(err)
}
return NewOk(nil)
}
//export CheckPrinter
func CheckPrinter(printer *C.char) *C.char {
goPrinter := C.GoString(printer)
err := Check(goPrinter)
if err != nil {
return NewErr(err)
}
return NewOk(nil)
}
func Print(printer, imagePath string) error {
conn, err := connect(printer)
if err != nil {
return err
}
defer conn.Close()
w := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
prt := escpos.New(w)
prt.Init()
prt.SetSmooth(1)
prt.SetAlign("left")
err := printImg(prt, goImagePath)
imgBytes, err := imgData(imagePath)
if err != nil {
return NewErr(err)
return err
}
err = writeChunk(w, prt, imgBytes)
if err != nil {
return err
}
time.Sleep(500 * time.Millisecond)
w.Flush()
prt.WriteRaw([]byte{0x1B, 0x64, 0x03})
prt.Cut()
prt.End()
w.Flush()
time.Sleep(1 * time.Second)
return NewOk(nil)
return nil
}
func Print(printer, imagePath string) string {
result := PrintImg(C.CString(printer), C.CString(imagePath))
r := C.GoString(result)
return r
func Check(printer string) error {
conn, err := connect(printer)
if err != nil {
return err
}
defer conn.Close()
var w *bufio.ReadWriter
w = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
prt := escpos.New(w)
prt.Init()
prt.SetSmooth(1)
prt.SetAlign("left")
time.Sleep(100 * time.Millisecond)
return w.Flush()
}
func connect(printer string) (io.ReadWriteCloser, error) {
if strings.HasPrefix(printer, "tcp:") {
location := strings.TrimLeft(printer, "tcp:")
conn, err := net.Dial("tcp", location)
if err != nil {
log.Println("error net.Dial tcp:", err.Error())
return nil, err
}
return conn, nil
} else if strings.HasPrefix(printer, "usb:") {
location := strings.TrimLeft(printer, "usb:")
f, err := os.OpenFile(location, os.O_RDWR, os.ModeDevice)
if err != nil {
log.Println("error os.OpenFile usb:", err.Error())
return nil, err
}
return f, nil
}
return nil, errors.New("invalid printer")
}
// writeChunk breaks the large byte array into smaller chunks
func writeChunk(w *bufio.ReadWriter, prt *escpos.Escpos, data []byte) error {
chunkSize := 1024 // 1 KB chunks work best for network printers
for i := 0; i < len(data); i += chunkSize {
end := min(i+chunkSize, len(data))
_, err := prt.WriteRaw(data[i:end])
if err != nil {
log.Println("Error writing image chunk:", err)
return err
}
if err := w.Flush(); err != nil {
log.Println("Error flushing buffer:", err)
return err
}
// Give the printer's network chip a microsecond to breathe
time.Sleep(5 * time.Millisecond)
}
return nil
}