3 Commits
main ... no-usb

Author SHA1 Message Date
99189e4b51 add working dir 2026-06-04 00:10:32 +06:30
d8781c3981 add template functions 2026-06-03 00:23:08 +06:30
d7ffc17d71 update to latest 2026-06-02 10:08:24 +06:30
10 changed files with 269 additions and 157 deletions

2
.gitignore vendored
View File

@@ -28,3 +28,5 @@ go.work.sum
build
assets
libgofunc
libgo
cmd/out.png

View File

@@ -1,9 +1,9 @@
#!/bin/bash
APP_NAME="libgofunc"
VERSION="${1:-v0.1.3}"
OUTPUT_DIR="assets"
BUILD_DIR="build"
VERSION="${1:-v0.1.7}"
OUTPUT_DIR="../assets"
BUILD_DIR="../build"
# need Android NDK
NDK_HOME="$HOME/Android/Sdk/ndk/28.2.13676358" # <--- CHECK YOUR VERSION
@@ -11,7 +11,7 @@ API=21
TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
OS="$(uname -s)"
cd cmd
if [ "$OS" = "Darwin" ]; then
export IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
export IOS_SIM_SDK=$(xcrun --sdk iphonesimulator --show-sdk-path)
@@ -27,7 +27,7 @@ if [ "$OS" = "Darwin" ]; then
CC="$(xcrun --sdk iphonesimulator --find clang)" \
CGO_CFLAGS="-isysroot $IOS_SIM_SDK -arch x86_64" \
CGO_LDFLAGS="-isysroot $IOS_SIM_SDK -arch x86_64" \
go build -buildmode=c-archive -o build/ios/sim/libgofunc_arm64_sim.a .
go build ./cmd -buildmode=c-archive -o build/ios/sim/libgofunc_arm64_sim.a .
# xcodebuild -create-xcframework \
# -library build/ios/device/libgofunc_arm64.a -headers build/ios/device/ \
@@ -74,7 +74,7 @@ elif [ "$OS" = "Linux" ]; then
tar -czf "${OUTPUT_DIR}/${VERSION}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" ./${VERSION}/${ARCH}
export HTTPS_PROXY="socks5://localhost:8080"
rclone copy ./assets/${VERSION} s3:mokkon/libs/libgofunc/${VERSION}
rclone copy ../assets/${VERSION} s3:mokkon/libs/libgofunc/${VERSION}
else
echo "Unsupported OS: $OS"
exit 1

File diff suppressed because one or more lines are too long

View File

@@ -91,7 +91,8 @@ func toMonochrome(img image.Image) *image.Gray {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, _ := img.At(x, y).RGBA()
grayValue := uint8((r + g + b) / 3 >> 8)
// grayValue := uint8((r + g + b) / 3 >> 8)
grayValue := uint8((0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 256.0)
if grayValue > 128 {
gray.Set(x, y, color.White)
} else {
@@ -138,7 +139,6 @@ func escposRaster(img *image.Gray) []byte {
data = append(data, b)
}
}
return data
}

3
go.mod
View File

@@ -1,6 +1,6 @@
module gt.mokkon.com/sainw/libgofunc
go 1.25.0
go 1.26.0
require (
github.com/dlclark/regexp2 v1.11.5
@@ -12,6 +12,5 @@ require (
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/gousb v1.1.3
golang.org/x/text v0.35.0 // indirect
)

2
go.sum
View File

@@ -4,8 +4,6 @@ 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/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o=
github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg=
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=

149
img.go
View File

@@ -24,7 +24,14 @@ import (
"github.com/kenshaw/escpos"
)
import "log"
import (
"log"
"path"
"strconv"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
const (
defalutFontSize = 18.0
@@ -40,11 +47,12 @@ const (
var fontFs embed.FS
//export GenPNG
func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char, workingDirC *C.char) *C.char {
canvasWidth := int(width)
goPath := C.GoString(outputPath)
goPayload := C.GoString(payload)
goTmpl := C.GoString(tmpl)
workingDir := C.GoString(workingDirC)
data := make(map[string]interface{})
err := json.Unmarshal([]byte(goPayload), &data)
@@ -85,7 +93,10 @@ func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char) *C.c
dc.SetFontFace(*face)
y := 0
renderNode(dc, canvasWidth, body, &y, *face)
err = renderNode(dc, canvasWidth, body, &y, *face, workingDir)
if err != nil {
return NewErr(err)
}
err = dc.SavePNG(goPath)
if err != nil {
@@ -96,14 +107,22 @@ func GenPNG(width C.int, outputPath *C.char, payload *C.char, tmpl *C.char) *C.c
return NewOk(nil)
}
func GenImg(width int, outputPath, payload, tmpl string) string {
result := GenPNG(C.int(width), C.CString(outputPath), C.CString(payload), C.CString(tmpl))
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)
return r
}
var funcMap = template.FuncMap{
"formatNumber": FormatNumber,
"div": Div,
}
func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
tmpl := template.Must(template.New("mytemplate").Parse(tmp))
tmpl, err := template.New("mytemplate").Funcs(funcMap).Parse(tmp)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", err
@@ -111,7 +130,7 @@ func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
return buf.String(), nil
}
func renderNode(dc *gg.Context, canvasWidth int, n *Node, y *int, face font.Face) {
func renderNode(dc *gg.Context, canvasWidth int, n *Node, y *int, face font.Face, workingDir string) error {
before := *y
if n.Style.PaddingTop > 0 {
*y += int(n.Style.PaddingTop)
@@ -124,11 +143,17 @@ func renderNode(dc *gg.Context, canvasWidth int, n *Node, y *int, face font.Face
case "h3":
drawTextBlock(dc, n, canvasWidth, eleH3Size, y, face)
case "p":
drawTextBlock(dc, n, canvasWidth, elePSize, y, face)
size := float64(elePSize)
if n.Style.FontSize > 0 {
size = n.Style.FontSize
}
drawTextBlock(dc, n, canvasWidth, size, y, face)
case "hr":
renderLine(dc, n, canvasWidth, y)
case "img":
drawImage(dc, n, y)
if err := drawImage(dc, n, y, workingDir); err != nil {
return err
}
case "table":
renderTable(dc, canvasWidth, n, y, face)
}
@@ -138,8 +163,9 @@ func renderNode(dc *gg.Context, canvasWidth int, n *Node, y *int, face font.Face
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)
renderNode(dc, canvasWidth, c, y, face, workingDir)
}
return nil
}
func drawTextBlock(dc *gg.Context, n *Node, canvasWidth int, size float64, y *int, face font.Face) {
@@ -176,14 +202,18 @@ func wordWrap(dc *gg.Context, text string, maxWidth int) []string {
return lines
}
func drawImage(dc *gg.Context, n *Node, y *int) {
func drawImage(dc *gg.Context, n *Node, y *int, workingDir string) error {
src := n.getSrc()
file, err := os.Open(src)
s := path.Join(workingDir, src)
file, err := os.Open(s)
if err != nil {
return
return fmt.Errorf("open file src: '%s', working directory: '%s', error : %s", src, workingDir, err.Error())
}
defer file.Close()
img, _, _ := image.Decode(file)
img, _, err := image.Decode(file)
if err != nil {
return err
}
padding := n.Style.PaddingLeft
h := n.Style.Height
if n.Style.Width > 0 {
@@ -206,10 +236,11 @@ func drawImage(dc *gg.Context, n *Node, y *int) {
dc.DrawImage(img, int(padding), *y)
*y += img.Bounds().Dy()
}
return nil
}
func renderTable(dc *gg.Context, canvasWidth int, table *Node, y *int, face font.Face) {
rows := extractRows(table)
headers, rows := extractRows(table)
if len(rows) == 0 {
return
}
@@ -223,9 +254,11 @@ func renderTable(dc *gg.Context, canvasWidth int, table *Node, y *int, face font
colCount := len(rows[0])
cellWidth := (canvasWidth - padding) / colCount
border := table.Style.Border
for _, row := range rows {
x := padding
for _, cell := range row {
for i, cell := range row {
header := headers[i]
if border > 0 {
dc.SetLineWidth(border)
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), fontSize+defalutTableBorder)
@@ -233,7 +266,11 @@ func renderTable(dc *gg.Context, canvasWidth int, table *Node, y *int, face font
dc.Stroke()
dc.SetRGB(0, 0, 0)
dc.DrawStringAnchored(cell, float64(x+8), float64(*y+20), 0, 0)
x += cellWidth
if w := header.Style.Width; w > 0 {
x += int(w)
} else {
x += cellWidth
}
}
*y += int(fontSize) + defalutTableBorder
}
@@ -247,8 +284,9 @@ func renderLine(dc *gg.Context, line *Node, canvasWidth int, y *int) {
*y += int(height)
}
func extractRows(table *Node) [][]string {
func extractRows(table *Node) ([]*Node, [][]string) {
var rows [][]string
var headers []*Node
var traverse func(*Node)
traverse = func(n *Node) {
if n.Tag == "tr" {
@@ -257,6 +295,9 @@ func extractRows(table *Node) [][]string {
if td.Tag == "td" || td.Tag == "th" {
row = append(row, td.Text)
}
if td.Tag == "th" {
headers = append(headers, td)
}
}
if len(row) > 0 {
rows = append(rows, row)
@@ -267,7 +308,7 @@ func extractRows(table *Node) [][]string {
}
}
traverse(table)
return rows
return headers, rows
}
func extractNodeRows(table *html.Node) [][]string {
@@ -332,9 +373,14 @@ func printImg(prt *escpos.Escpos, imgPath string) error {
fmt.Println(err)
return 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)
data = escposRaster(gray)
_, err = prt.WriteRaw(data)
return err
@@ -409,3 +455,66 @@ func nodeHeight(n *html.Node, dc *gg.Context, canvasWidth int, xPadding, yPaddin
}
return y
}
type RealNumber interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
func FormatNumber(precision int, b any) string {
return GenericFormatNumber(precision, convertToFloat64(b))
}
func GenericFormatNumber[T RealNumber](precision int, v T) string {
pEnglish := message.NewPrinter(language.English)
n := float64(v)
s := fmt.Sprintf("%.*f", precision, n)
parts := strings.Split(s, ".")
intPart := parts[0]
i, err := strconv.Atoi(intPart)
if err != nil {
return ""
}
out := pEnglish.Sprintf("%d", i)
if len(parts) > 1 {
return out + "." + parts[1]
}
return out
}
func Div(a, b any) float64 {
return GenericDiv(convertToFloat64(a), convertToFloat64(b))
}
func GenericDiv[T RealNumber](a, b T) float64 {
floatB := float64(b)
if floatB == 0 {
return 0
}
return float64(a) / floatB
}
func convertToFloat64(v any) float64 {
switch t := v.(type) {
case float64:
return t
case float32:
return float64(t)
case int:
return float64(t)
case int64:
return float64(t)
case int32:
return float64(t)
case uint:
return float64(t)
case uint64:
return float64(t)
default:
return 0
}
}

60
img_test.go Normal file
View File

@@ -0,0 +1,60 @@
package libgofunc
import (
"math"
"testing"
)
func TestDiv(t *testing.T) {
tests := []struct {
name string
a any
b any
want float64
}{
{"Float division", 10.5, 2.0, 5.25},
{"Float division", 4975, 1000, 4.975},
{"Integer division", 10, 4, 2.5},
{"Mixed types", int64(100), float64(4.0), 25.0},
{"Division by zero (float)", 10.0, 0.0, 0.0},
{"Division by zero (int)", 5, 0, 0.0},
{"Unsupported type defaults to zero", "string", 2, 0.0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Div(tt.a, tt.b)
if got != tt.want {
t.Errorf("TemplateDiv() = %v, want %v", got, tt.want)
}
})
}
}
func TestFormatNumber(t *testing.T) {
tests := []struct {
name string
precision int
val any
want string
}{
{"Standard float with commas", 2, 1234567.891, "1,234,567.89"},
{"Standard float with commas", 3, 4.975, "4.975"},
{"Integer input with commas", 0, 5000000, "5,000,000"},
{"Integer input forced decimals", 2, 5000, "5,000.00"},
{"Negative float commas", 2, -9876543.21, "-9,876,543.21"},
{"Small float rounding up", 2, 0.128, "0.13"},
{"Small float rounding down", 2, 0.123, "0.12"},
{"Handling NaN", 2, math.NaN(), ""},
{"Handling Inf", 2, math.Inf(1), ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FormatNumber(tt.precision, tt.val)
if got != tt.want {
t.Errorf("TemplateFloat() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -17,33 +17,12 @@ import (
"github.com/kenshaw/escpos"
)
import (
"fmt"
"github.com/google/gousb"
)
type USBReadWriter struct {
out *gousb.OutEndpoint
in *gousb.InEndpoint // Optional, can be nil if you only write
}
func (urw *USBReadWriter) Write(p []byte) (n int, err error) {
return urw.out.Write(p)
}
func (urw *USBReadWriter) Read(p []byte) (n int, err error) {
if urw.in == nil {
return 0, fmt.Errorf("read not supported")
}
return urw.in.Read(p)
}
import "errors"
//export PrintImg
func PrintImg(printer *C.char, imagePath *C.char) *C.char {
goPrinter := C.GoString(printer)
goImagePath := C.GoString(imagePath)
var out *gousb.OutEndpoint
// printer := "tcp:192.168.100.151:9100"
// printer := "usb:/dev/usb/lp1"
var w *bufio.ReadWriter
@@ -65,53 +44,58 @@ func PrintImg(printer *C.char, imagePath *C.char) *C.char {
}
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)
// } 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()
// // 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)
// // 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)
if err != nil {
return NewErr(err)
}
prt.WriteRaw([]byte{0x1B, 0x64, 0x03})
prt.Cut()
prt.End()
w.Flush()
time.Sleep(100 * time.Millisecond)
time.Sleep(1 * time.Second)
return NewOk(nil)
}

6
vo.go
View File

@@ -10,13 +10,13 @@ import (
)
type Reply struct {
Status int `json:"status"`
Status string `json:"status"` // "ok", "error"
Err string `json:"err"`
Result interface{} `json:"result"`
}
func NewErr(err error) *C.char {
e := Reply{Status: 1, Err: err.Error()}
e := Reply{Status: "error", Err: err.Error()}
b, err := json.Marshal(e)
if err != nil {
log.Println("Error json.Marshal:", err.Error())
@@ -25,7 +25,7 @@ func NewErr(err error) *C.char {
}
func NewOk(data interface{}) *C.char {
e := Reply{Status: 0, Result: data}
e := Reply{Status: "ok", Result: data}
b, err := json.Marshal(e)
if err != nil {
log.Println("Error json.Marshal:", err.Error())