Compare commits
8 Commits
99189e4b51
...
v0.1.13
| Author | SHA1 | Date | |
|---|---|---|---|
| fa8691c04e | |||
| 016dad446f | |||
| 06f38b707e | |||
| 5f4a427f3b | |||
| 7bb8778611 | |||
| 58c029b5f2 | |||
| 8a23c7037f | |||
| 29ec943d13 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,3 +30,4 @@ assets
|
||||
libgofunc
|
||||
libgo
|
||||
cmd/out.png
|
||||
cmd/*.png
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
APP_NAME="libgofunc"
|
||||
VERSION="${1:-v0.1.7}"
|
||||
VERSION="${1:-v0.1.13}"
|
||||
OUTPUT_DIR="../assets"
|
||||
BUILD_DIR="../build"
|
||||
|
||||
|
||||
88
cmd/main.go
88
cmd/main.go
File diff suppressed because one or more lines are too long
1
go.mod
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
99
img.go
99
img.go
@@ -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)
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
return NewErr(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
}
|
||||
@@ -116,6 +129,7 @@ func GenImg(width int, outputPath, payload, tmpl, workingDir string) string {
|
||||
var funcMap = template.FuncMap{
|
||||
"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)
|
||||
}
|
||||
|
||||
152
printer.go
152
printer.go
@@ -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)
|
||||
err := Print(goPrinter, goImagePath)
|
||||
if err != nil {
|
||||
log.Println("error dialing:", err.Error())
|
||||
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))
|
||||
} 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)
|
||||
w := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
|
||||
|
||||
// // 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.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_WRONLY, 0666)
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user