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 libgofunc
libgo libgo
cmd/out.png cmd/out.png
cmd/*.png

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
APP_NAME="libgofunc" APP_NAME="libgofunc"
VERSION="${1:-v0.1.7}" VERSION="${1:-v0.1.12}"
OUTPUT_DIR="../assets" OUTPUT_DIR="../assets"
BUILD_DIR="../build" 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 ( require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect 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 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/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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 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 h1:4JwmN2Scz1vR+hfSxkdy2IE/DzxX2Cftm2lhWHyN0k0=
github.com/kenshaw/escpos v0.0.0-20221114190919-df06b682a8fc/go.mod h1:M+GIBmg2MqaSWIJrXCZS+/wRFbr9fOguRz3SHn8DRPE= 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= golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=

101
img.go
View File

@@ -14,6 +14,7 @@ import (
"strings" "strings"
"embed" "embed"
_ "time/tzdata"
"github.com/fogleman/gg" "github.com/fogleman/gg"
"golang.org/x/image/font" "golang.org/x/image/font"
@@ -21,14 +22,14 @@ import (
"golang.org/x/net/html" "golang.org/x/net/html"
"encoding/json" "encoding/json"
"github.com/kenshaw/escpos"
) )
import ( import (
"log" "log"
"path" "path"
"strconv" "strconv"
"time"
"github.com/google/uuid"
"golang.org/x/text/language" "golang.org/x/text/language"
"golang.org/x/text/message" "golang.org/x/text/message"
) )
@@ -47,9 +48,9 @@ const (
var fontFs embed.FS var fontFs embed.FS
//export GenPNG //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) canvasWidth := int(width)
goPath := C.GoString(outputPath) goOutDir := C.GoString(outDir)
goPayload := C.GoString(payload) goPayload := C.GoString(payload)
goTmpl := C.GoString(tmpl) goTmpl := C.GoString(tmpl)
workingDir := C.GoString(workingDirC) 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) htmlStr = uni2zg(htmlStr)
root, err := html.Parse(strings.NewReader(htmlStr)) htmls := strings.Split(htmlStr, "<cut>")
result := make([]string, 0)
for _, html := range htmls {
out, err := GenPng(canvasWidth, goOutDir, html, workingDir)
if err != nil { if err != nil {
return NewErr(err) 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 "", err
}
face, err := LoadFont(defalutFontSize) face, err := LoadFont(defalutFontSize)
if err != nil { if err != nil {
return NewErr(err) return "", err
} }
// First pass: compute required height // First pass: compute required height
@@ -95,20 +109,19 @@ func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char, work
y := 0 y := 0
err = renderNode(dc, canvasWidth, body, &y, *face, workingDir) err = renderNode(dc, canvasWidth, body, &y, *face, workingDir)
if err != nil { 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 { if err != nil {
return NewErr(err) return "", err
}
return outPath, nil
} }
// PrintReceipt(goPath) func GenImg(width int, outputDir, payload, tmpl, workingDir string) string {
return NewOk(nil) result := GenPNG(C.int(width), C.CString(outputDir), C.CString(payload), C.CString(tmpl), C.CString(workingDir))
}
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))
r := C.GoString(result) r := C.GoString(result)
return r return r
} }
@@ -116,6 +129,7 @@ func GenImg(width int, outputPath, payload, tmpl, workingDir string) string {
var funcMap = template.FuncMap{ var funcMap = template.FuncMap{
"formatNumber": FormatNumber, "formatNumber": FormatNumber,
"div": Div, "div": Div,
"formatDateFromSec": formatDateFromSec,
} }
func renderTemplate(tmp string, data map[string]interface{}) (string, error) { 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 { if n.Style.PaddingBottom > 0 {
*y += int(n.Style.PaddingBottom) *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 { for _, c := range n.Children {
renderNode(dc, canvasWidth, c, y, face, workingDir) renderNode(dc, canvasWidth, c, y, face, workingDir)
@@ -360,30 +375,25 @@ func findNode(n *html.Node, tag string) *html.Node {
return nil return nil
} }
func printImg(prt *escpos.Escpos, imgPath string) error { func imgData(imgPath string) ([]byte, error) {
imgFile, err := os.Open(imgPath) imgFile, err := os.Open(imgPath)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return err return nil, err
} }
img, _, err := image.Decode(imgFile) img, _, err := image.Decode(imgFile)
defer imgFile.Close() defer imgFile.Close()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return err return nil, err
} }
data := []byte{0x1D, 0x4C, 0x00, 0x00} data := []byte{0x1D, 0x4C, 0x00, 0x00}
_, err = prt.WriteRaw(data)
if err != nil {
fmt.Println("error 0x1D, 0x4C:", err.Error())
}
gray := toMonochrome(img) gray := toMonochrome(img)
data = escposRaster(gray) imgData := escposRaster(gray)
data = append(data, imgData...)
_, err = prt.WriteRaw(data) return data, err
return err
} }
func LoadFont(size float64) (*font.Face, error) { 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 h = implicit
} }
y += h y += h
log.Printf("nodeHeight %s height: %d, \n", n.Data, y) // log.Printf("nodeHeight %s height: %d, \n", n.Data, y)
} }
return y return y
} }
@@ -518,3 +528,38 @@ func convertToFloat64(v any) float64 {
return 0 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" "time"
"errors"
"github.com/kenshaw/escpos" "github.com/kenshaw/escpos"
) )
import "errors" import "io"
//export PrintImg //export PrintImg
func PrintImg(printer *C.char, imagePath *C.char) *C.char { func PrintImg(printer *C.char, imagePath *C.char) *C.char {
goPrinter := C.GoString(printer) goPrinter := C.GoString(printer)
goImagePath := C.GoString(imagePath) goImagePath := C.GoString(imagePath)
// printer := "tcp:192.168.100.151:9100" err := Print(goPrinter, goImagePath)
// 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 { if err != nil {
log.Println("error dialing:", err.Error())
return NewErr(err) 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() defer conn.Close()
w = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn)) 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"))
}
prt := escpos.New(w) prt := escpos.New(w)
prt.Init() prt.Init()
prt.SetSmooth(1) prt.SetSmooth(1)
prt.SetAlign("left") prt.SetAlign("left")
err := printImg(prt, goImagePath) imgBytes, err := imgData(imagePath)
if err != nil { 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.WriteRaw([]byte{0x1B, 0x64, 0x03})
prt.Cut() prt.Cut()
prt.End() prt.End()
w.Flush() w.Flush()
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
return NewOk(nil) return nil
} }
func Print(printer, imagePath string) string { func Check(printer string) error {
result := PrintImg(C.CString(printer), C.CString(imagePath)) conn, err := connect(printer)
r := C.GoString(result) if err != nil {
return r 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
} }