6 Commits
v0.1.0 ... main

Author SHA1 Message Date
f9c96971a7 use 'github.com/fogleman/gg' only 2026-04-20 12:22:30 +06:30
ecb03c1fb7 fix html to png 2026-04-08 15:31:23 +06:30
016cc5b6fe update build 2026-03-19 02:41:51 +06:30
9a0d1d098c add upload to space 2026-03-19 00:19:08 +06:30
5d885361c0 v0.1.1 2026-03-18 17:20:47 +06:30
f472187217 update build to include version 2026-03-18 17:19:12 +06:30
16 changed files with 557 additions and 183 deletions

2
.gitignore vendored
View File

@@ -26,3 +26,5 @@ go.work.sum
.env .env
build build
assets
libgofunc

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Dev",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd",
},
]
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
APP_NAME="libgofunc" APP_NAME="libgofunc"
VERSION="${1:-v0.1.0}" VERSION="${1:-v0.1.3}"
OUTPUT_DIR="assets" OUTPUT_DIR="assets"
BUILD_DIR="build" BUILD_DIR="build"
@@ -44,34 +44,37 @@ if [ "$OS" = "Darwin" ]; then
cp ./build/ios/libgofunc.a $HOME/ws/forward_pos/native/ios/x86_64/libgofunc.a cp ./build/ios/libgofunc.a $HOME/ws/forward_pos/native/ios/x86_64/libgofunc.a
elif [ "$OS" = "Linux" ]; then elif [ "$OS" = "Linux" ]; then
echo "Building for Android amd64..." mkdir -p "${OUTPUT_DIR}/${VERSION}"
ARCH="amd64"
echo "Building for Android $ARCH..."
CC="$TOOLCHAIN/x86_64-linux-android$API-clang" \ CC="$TOOLCHAIN/x86_64-linux-android$API-clang" \
CGO_ENABLED=1 GOOS=android GOARCH=amd64 \ CGO_ENABLED=1 GOOS=android GOARCH=amd64 \
go build -buildmode=c-shared -o $BUILD_DIR/libgofunc_amd64.so . go build -buildmode=c-shared -o $BUILD_DIR/$VERSION/$ARCH/libgofunc.so .
ARCHIVE_NAME="${APP_NAME}-${VERSION}-amd64.tar.gz" ARCHIVE_NAME="${APP_NAME}-${VERSION}-amd64.tar.gz"
tar -czf "${OUTPUT_DIR}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" libgofunc_amd64.so tar -czf "${OUTPUT_DIR}/${VERSION}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" ./${VERSION}/${ARCH}
echo "Building for Android arm64..." ARCH="arm64"
echo "Building for Android $ARCH..."
CC="$TOOLCHAIN/aarch64-linux-android$API-clang" \ CC="$TOOLCHAIN/aarch64-linux-android$API-clang" \
CGO_ENABLED=1 GOOS=android GOARCH=arm64 \ CGO_ENABLED=1 GOOS=android GOARCH=arm64 \
go build -buildmode=c-shared -o $BUILD_DIR/libgofunc_arm64.so . go build -buildmode=c-shared -o $BUILD_DIR/$VERSION/$ARCH/libgofunc.so .
ARCHIVE_NAME="${APP_NAME}-${VERSION}-arm64.tar.gz" ARCHIVE_NAME="${APP_NAME}-${VERSION}-${ARCH}.tar.gz"
tar -czf "${OUTPUT_DIR}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" libgofunc_arm64.so tar -czf "${OUTPUT_DIR}/${VERSION}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" ./${VERSION}/${ARCH}
ARCH="armv7a"
echo "Building for Android armv7a..." echo "Building for Android armv7a..."
CC="$TOOLCHAIN/armv7a-linux-androideabi$API-clang" \ CC="$TOOLCHAIN/armv7a-linux-androideabi$API-clang" \
CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 \ CGO_ENABLED=1 GOOS=android GOARCH=arm GOARM=7 \
go build -buildmode=c-shared -o $BUILD_DIR/libgofunc_armv7a.so . go build -buildmode=c-shared -o $BUILD_DIR/$VERSION/$ARCH/libgofunc.so .
ARCHIVE_NAME="${APP_NAME}-${VERSION}-armv7a.tar.gz" ARCHIVE_NAME="${APP_NAME}-${VERSION}-armv7a.tar.gz"
tar -czf "${OUTPUT_DIR}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" libgofunc_armv7a.so tar -czf "${OUTPUT_DIR}/${VERSION}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" ./${VERSION}/${ARCH}
# cp ./assets/libgofunc_x64.so $HOME/ws/forward_pos/native/android/x86_64/libgofunc.so
# cp ./assets/libgofunc_arm64.so $HOME/ws/forward_pos/native/android/arm64-v8a/libgofunc.so
# cp ./assets/libgofunc_armv7a.so $HOME/ws/forward_pos/native/android/armeabi-v7a/libgofunc.so
export HTTPS_PROXY="socks5://localhost:8080"
rclone copy ./assets/${VERSION} s3:mokkon/libs/libgofunc/${VERSION}
else else
echo "Unsupported OS: $OS" echo "Unsupported OS: $OS"
exit 1 exit 1

141
canvas.go Normal file
View File

@@ -0,0 +1,141 @@
package libgofunc
import (
"strconv"
"strings"
"github.com/fogleman/gg"
"golang.org/x/image/font"
"golang.org/x/net/html"
)
// -------------------- STYLE --------------------
type Style struct {
PaddingLeft float64
PaddingRight float64
PaddingTop float64
PaddingBottom float64
FontSize float64
Width float64
Height float64
TextAlign string
VerticalAlign string
Border float64
}
func parseStyle(styleStr string) Style {
style := Style{
FontSize: 16,
TextAlign: "left",
VerticalAlign: "top",
}
parts := strings.Split(styleStr, ";")
for _, p := range parts {
kv := strings.SplitN(strings.TrimSpace(p), ":", 2)
if len(kv) != 2 {
continue
}
key := strings.TrimSpace(kv[0])
val := strings.TrimSpace(kv[1])
switch key {
case "padding":
v := parsePx(val)
style.PaddingLeft, style.PaddingRight, style.PaddingTop, style.PaddingBottom = v, v, v, v
case "padding-left":
v := parsePx(val)
style.PaddingLeft = v
case "padding-right":
v := parsePx(val)
style.PaddingRight = v
case "padding-top":
v := parsePx(val)
style.PaddingTop = v
case "padding-bottom":
v := parsePx(val)
style.PaddingBottom = v
case "font-size":
style.FontSize = parsePx(val)
case "width":
style.Width = parsePx(val)
case "height":
style.Height = parsePx(val)
case "text-align":
style.TextAlign = val
case "vertical-align":
style.VerticalAlign = val
case "border":
style.Border = parsePx(val)
}
}
return style
}
func parsePx(v string) float64 {
v = strings.ReplaceAll(v, "px", "")
f, _ := strconv.ParseFloat(v, 64)
return f
}
// -------------------- NODE --------------------
type Node struct {
Tag string
Text string
Attr map[string]string
Style Style
Children []*Node
}
func (n Node) getSrc() string {
for k, v := range n.Attr {
if k == "src" {
return v
}
}
return ""
}
func (n Node) getHeight() int {
return int(n.Style.Height)
}
func BuildTree(n *html.Node, canvasWidth int, dc *gg.Context, face *font.Face) (*Node, int) {
if n.Type != html.ElementNode {
return nil, 0
}
text := ""
if n.Data == "h1" || n.Data == "h2" || n.Data == "h3" ||
n.Data == "p" || n.Data == "td" || n.Data == "th" ||
n.Type == html.TextNode {
text = getText(n)
}
node := &Node{
Tag: n.Data,
Text: text,
Attr: map[string]string{},
}
for _, a := range n.Attr {
node.Attr[a.Key] = a.Val
}
if s, ok := node.Attr["style"]; ok {
node.Style = parseStyle(s)
}
xPadding := node.Style.PaddingLeft + node.Style.PaddingRight
yPadding := node.Style.PaddingTop + node.Style.PaddingBottom
fontSize := node.Style.FontSize
h := nodeHeight(n, dc, canvasWidth, int(xPadding), int(yPadding),
face, fontSize, node.getHeight())
for c := n.FirstChild; c != nil; c = c.NextSibling {
child, ch := BuildTree(c, canvasWidth, dc, face)
if child != nil {
node.Children = append(node.Children, child)
}
h += ch
}
return node, h
}

109
cmd/main.go Normal file
View File

@@ -0,0 +1,109 @@
package main
import (
/*
#include <stdint.h>
*/
"C"
"fmt"
_ "image/png"
"github.com/google/gousb"
)
import (
"github.com/google/gousb/usbid"
"gt.mokkon.com/sainw/libgofunc"
)
//export Sum
func Sum(a, b int) int {
return a + b
}
func main() {
payload := `{"Name":"Ko Myo","Amount":3000}`
const temp = `
<img src="logo.png" style="width:230;height:200;padding-left:130px;padding-top:30px"/>
<h1 style="padding-left:150px">New Day Energy</h1>
<p style="padding-left:10px">Address: မင်္ဂလာပါ {{.Name}}</p>
<p style="padding-left:10px">Receipt: RCPT001</p>
<p style="padding-left:10px">Phone: 0977777777</p>
<p style="padding-left:10px">Date: 4 Jan 2026 15:38:38</p>
<p style="padding-left:10px">Car No.: 3J/3883</p>
<p style="padding-left:10px">Casher: မနှင်းနှင်း</p>
<p style="padding-left:10px">MOP: B2B</p>
<p style="padding-left:10px">Tier: 1</p>
<p style="padding-left:10px">Deal No.: RR</p>
<hr style="height:1px;padding-left:10px;padding-right:10px;padding-bottom:10px"/>
<table style="padding-left:10px;padding-bottom:10px;border:0px;font-size:16px;border:0px;">
<tr>
<th>Item</th>
<th>Pump</th>
<th>Price</th>
<th>Liter</th>
<th>Gallon</th>
<th>Amount</th>
</tr>
<tr>
<td>95 RON</td>
<td>P8</td>
<td>2,000</td>
<td>3.25</td>
<td>0.715</td>
<td>6,500</td>
</tr>
</table>
<p style="padding-left:0px;padding-top:50px;">
စက်သုံးဆီ အရေအတွက် နှင့် အရည်အသွေးစျေးနှုန်းများ အားသံသယရှိပါက ph 09450539099, 09765421033, 09765421029 သို့တိုင်ကြား နိုင်ပါသည်။</p>
`
result := libgofunc.GenImg(550, "./out.png", payload, temp)
fmt.Println("Result:", result)
// PrintImg(C.CString("usb:/dev/usb/lp1"), C.CString("build/out.png"))
// libgofunc.Print("int:/dev/bus/usb/001/046", "./out.png")
libgofunc.Print("tcp:192.168.100.151:9100", "./out.png")
// printer := "tcp:192.168.100.151:9100"
// ListUSB()
}
func ListUSB() {
ctx := gousb.NewContext()
defer ctx.Close()
// OpenDevices returns all devices that return 'true' in the filter
devices, _ := ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool {
// fmt.Println(usbid.Describe(desc))
// path := fmt.Sprintf("/dev/bus/usb/%03d/%03d", desc.Bus, desc.Address)
// fmt.Printf("Device Path: %s\n", path)
// switch desc.Class {
// case 7:
// fmt.Println("Found a standard printer")
// case 0xFF:
// fmt.Println("Found a vendor-specific device (likely your printer)")
// }
return true // Open every device to read its name
})
defer func() {
for _, d := range devices {
d.Close()
}
}()
for _, d := range devices {
fmt.Println(usbid.Describe(d.Desc))
fmt.Printf("Bus: %d, Address: %d\n", d.Desc.Bus, d.Desc.Address)
// Read human-readable names from the device strings
manufacturer, _ := d.Manufacturer()
product, _ := d.Product()
serial, _ := d.SerialNumber()
// d.Desc.Path
fmt.Printf("ID: %s:%s | Manufacturer: %s | Product: %s | Serial: %s\n",
d.Desc.Vendor, d.Desc.Product, manufacturer, product, serial)
}
}

View File

@@ -1,4 +1,4 @@
package main package libgofunc
import ( import (
"image" "image"

11
go.mod
View File

@@ -1,16 +1,17 @@
module libgofunc module gt.mokkon.com/sainw/libgofunc
go 1.22.0 go 1.25.0
require ( require (
github.com/dlclark/regexp2 v1.11.5 github.com/dlclark/regexp2 v1.11.5
github.com/fogleman/gg v1.3.0 github.com/fogleman/gg v1.3.0
github.com/kenshaw/escpos v0.0.0-20221114190919-df06b682a8fc github.com/kenshaw/escpos v0.0.0-20221114190919-df06b682a8fc
golang.org/x/image v0.24.0 golang.org/x/image v0.38.0
golang.org/x/net v0.34.0 golang.org/x/net v0.52.0
) )
require ( require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/text v0.22.0 // indirect github.com/google/gousb v1.1.3
golang.org/x/text v0.35.0 // indirect
) )

14
go.sum
View File

@@ -4,11 +4,13 @@ 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/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 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.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=

285
img.go
View File

@@ -1,4 +1,4 @@
package main package libgofunc
import ( import (
/* /*
@@ -11,7 +11,6 @@ import (
"image" "image"
_ "image/png" _ "image/png"
"os" "os"
"strconv"
"strings" "strings"
"embed" "embed"
@@ -25,18 +24,24 @@ import (
"github.com/kenshaw/escpos" "github.com/kenshaw/escpos"
) )
import "log"
const ( const (
canvasWidth = 550 defalutFontSize = 18.0
padding = 5 defalutTableBorder = 5
lineGap = 12 lineGap = 12
elePSize = 18
eleH1Size = 32
eleH2Size = 24
eleH3Size = 19
) )
//go:embed static/* //go:embed static/*
var fontFs embed.FS var fontFs embed.FS
//export GenPNG //export GenPNG
func GenPNG(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) *C.char {
canvasWidth := int(width)
goPath := C.GoString(outputPath) goPath := C.GoString(outputPath)
goPayload := C.GoString(payload) goPayload := C.GoString(payload)
goTmpl := C.GoString(tmpl) goTmpl := C.GoString(tmpl)
@@ -58,42 +63,29 @@ func GenPNG(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
return NewErr(err) return NewErr(err)
} }
fontBytes, err := fontFs.ReadFile("static/Zawgyi-One.ttf") face, err := LoadFont(defalutFontSize)
if err != nil {
return NewErr(err)
}
ttfFont, err := opentype.Parse(fontBytes)
if err != nil {
return NewErr(err)
}
face, err := opentype.NewFace(ttfFont, &opentype.FaceOptions{
Size: 24,
DPI: 72,
Hinting: font.HintingFull,
})
if err != nil { if err != nil {
return NewErr(err) return NewErr(err)
} }
// First pass: compute required height // First pass: compute required height
dummyDC := gg.NewContext(canvasWidth, 150) dummyDC := gg.NewContext(canvasWidth, 150)
dummyDC.SetFontFace(face) dummyDC.SetFontFace(*face)
bodyNode := findNode(root, "body") bodyNode := findNode(root, "body")
if bodyNode == nil { if bodyNode == nil {
bodyNode = root bodyNode = root
} }
computedHeight := computeHeight(bodyNode, padding, dummyDC, face) body, height := BuildTree(bodyNode, canvasWidth, dummyDC, face)
// Second pass: actual rendering // Second pass: actual rendering
dc := gg.NewContext(canvasWidth, computedHeight+50) dc := gg.NewContext(canvasWidth, height)
dc.SetRGB(1, 1, 1) dc.SetRGB(1, 1, 1)
dc.Clear() dc.Clear()
dc.SetFontFace(face) dc.SetFontFace(*face)
y := padding y := 0
renderNode(dc, bodyNode, &y, face) renderNode(dc, canvasWidth, body, &y, *face)
err = dc.SavePNG(goPath) err = dc.SavePNG(goPath)
if err != nil { if err != nil {
@@ -104,6 +96,12 @@ func GenPNG(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
return NewOk(nil) 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))
r := C.GoString(result)
return r
}
func renderTemplate(tmp string, data map[string]interface{}) (string, error) { func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
tmpl := template.Must(template.New("mytemplate").Parse(tmp)) tmpl := template.Must(template.New("mytemplate").Parse(tmp))
var buf bytes.Buffer var buf bytes.Buffer
@@ -113,34 +111,48 @@ func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
return buf.String(), nil return buf.String(), nil
} }
func renderNode(dc *gg.Context, n *html.Node, y *int, face font.Face) { func renderNode(dc *gg.Context, canvasWidth int, n *Node, y *int, face font.Face) {
if n.Type == html.ElementNode { before := *y
switch n.Data { if n.Style.PaddingTop > 0 {
*y += int(n.Style.PaddingTop)
}
switch n.Tag {
case "h1": case "h1":
drawTextBlock(dc, getText(n), 28, y, face) drawTextBlock(dc, n, canvasWidth, eleH1Size, y, face)
case "h2":
drawTextBlock(dc, n, canvasWidth, eleH2Size, y, face)
case "h3":
drawTextBlock(dc, n, canvasWidth, eleH3Size, y, face)
case "p": case "p":
drawTextBlock(dc, getText(n), 24, y, face) drawTextBlock(dc, n, canvasWidth, elePSize, y, face)
case "hr":
renderLine(dc, n, canvasWidth, y)
case "img": case "img":
drawImage(dc, n, y) drawImage(dc, n, y)
case "table": case "table":
renderTable(dc, n, y, face) renderTable(dc, canvasWidth, n, y, 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)
for _, c := range n.Children {
renderNode(dc, canvasWidth, c, y, face)
} }
} }
for c := n.FirstChild; c != nil; c = c.NextSibling { func drawTextBlock(dc *gg.Context, n *Node, canvasWidth int, size float64, y *int, face font.Face) {
renderNode(dc, c, y, face)
}
}
func drawTextBlock(dc *gg.Context, text string, size float64, y *int, face font.Face) {
dc.SetFontFace(face) dc.SetFontFace(face)
lines := wordWrap(dc, text, canvasWidth-padding*2) padding := n.Style.PaddingLeft + n.Style.PaddingRight
lines := wordWrap(dc, n.Text, canvasWidth-int(padding))
SetFontSize(dc, size)
for _, line := range lines { for _, line := range lines {
dc.SetRGB(0, 0, 0) dc.SetRGB(0, 0, 0)
dc.DrawString(line, padding, float64(*y)) dc.DrawStringAnchored(line, padding, float64(*y), 0, 0.5)
// dc.DrawString(line, padding, float64(*y))
*y += int(size) + lineGap *y += int(size) + lineGap
} }
*y += 5
} }
func wordWrap(dc *gg.Context, text string, maxWidth int) []string { func wordWrap(dc *gg.Context, text string, maxWidth int) []string {
@@ -164,53 +176,101 @@ func wordWrap(dc *gg.Context, text string, maxWidth int) []string {
return lines return lines
} }
func drawImage(dc *gg.Context, n *html.Node, y *int) { func drawImage(dc *gg.Context, n *Node, y *int) {
src := getAttr(n, "src") src := n.getSrc()
file, err := os.Open(src) file, err := os.Open(src)
if err != nil { if err != nil {
return return
} }
defer file.Close() defer file.Close()
img, _, _ := image.Decode(file) img, _, _ := image.Decode(file)
widthStr := getAttr(n, "width") padding := n.Style.PaddingLeft
if widthStr != "" { h := n.Style.Height
w, _ := strconv.Atoi(widthStr) if n.Style.Width > 0 {
scale := float64(w) / float64(img.Bounds().Dx()) ix := padding
iy := float64(*y)
scale := n.Style.Width / float64(img.Bounds().Dx())
dc.Push() dc.Push()
dc.Scale(scale, scale) dc.Scale(scale, scale)
dc.DrawImage(img, padding, *y) if scale > 0 {
ix = ix / scale
iy = iy / scale
}
dc.DrawImage(img, int(ix), int(iy))
dc.Pop() dc.Pop()
*y += int(float64(img.Bounds().Dy()) * scale) if float64(img.Bounds().Dy())*scale > h {
h = float64(img.Bounds().Dy()) * scale
}
*y += int(h)
} else { } else {
dc.DrawImage(img, padding, *y) dc.DrawImage(img, int(padding), *y)
*y += img.Bounds().Dy() *y += img.Bounds().Dy()
} }
*y += 10
} }
func renderTable(dc *gg.Context, table *html.Node, y *int, face font.Face) { func renderTable(dc *gg.Context, canvasWidth int, table *Node, y *int, face font.Face) {
rows := extractRows(table) rows := extractRows(table)
if len(rows) == 0 { if len(rows) == 0 {
return return
} }
fontSize := defalutFontSize
if table.Style.FontSize > 0 {
fontSize = table.Style.FontSize
}
SetFontSize(dc, fontSize)
padding := int(table.Style.PaddingLeft + table.Style.PaddingRight)
colCount := len(rows[0]) colCount := len(rows[0])
cellWidth := (canvasWidth - padding*2) / colCount cellWidth := (canvasWidth - padding) / colCount
dc.SetFontFace(face) border := table.Style.Border
for _, row := range rows { for _, row := range rows {
x := padding x := padding
for _, cell := range row { for _, cell := range row {
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), 30) if border > 0 {
dc.SetLineWidth(border)
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), fontSize+defalutTableBorder)
}
dc.Stroke() dc.Stroke()
dc.SetRGB(0, 0, 0) dc.SetRGB(0, 0, 0)
dc.DrawString(cell, float64(x+8), float64(*y+20)) dc.DrawStringAnchored(cell, float64(x+8), float64(*y+20), 0, 0)
x += cellWidth x += cellWidth
} }
*y += 30 *y += int(fontSize) + defalutTableBorder
} }
*y += 10
} }
func extractRows(table *html.Node) [][]string { func renderLine(dc *gg.Context, line *Node, canvasWidth int, y *int) {
height := line.Style.Height
xPadding := line.Style.PaddingLeft + line.Style.PaddingRight
dc.SetLineWidth(height)
dc.DrawLine(line.Style.PaddingLeft, float64(*y), float64(canvasWidth)-xPadding, float64(*y))
*y += int(height)
}
func extractRows(table *Node) [][]string {
var rows [][]string
var traverse func(*Node)
traverse = func(n *Node) {
if n.Tag == "tr" {
var row []string
for _, td := range n.Children {
if td.Tag == "td" || td.Tag == "th" {
row = append(row, td.Text)
}
}
if len(row) > 0 {
rows = append(rows, row)
}
}
for _, c := range n.Children {
traverse(c)
}
}
traverse(table)
return rows
}
func extractNodeRows(table *html.Node) [][]string {
var rows [][]string var rows [][]string
var traverse func(*html.Node) var traverse func(*html.Node)
traverse = func(n *html.Node) { traverse = func(n *html.Node) {
@@ -247,15 +307,6 @@ func getText(n *html.Node) string {
return buf.String() return buf.String()
} }
func getAttr(n *html.Node, key string) string {
for _, a := range n.Attr {
if a.Key == key {
return a.Val
}
}
return ""
}
func findNode(n *html.Node, tag string) *html.Node { func findNode(n *html.Node, tag string) *html.Node {
if n.Type == html.ElementNode && n.Data == tag { if n.Type == html.ElementNode && n.Data == tag {
return n return n
@@ -268,36 +319,6 @@ func findNode(n *html.Node, tag string) *html.Node {
return nil return nil
} }
func computeHeight(n *html.Node, currentY int, dc *gg.Context, face font.Face) int {
y := currentY
if n.Type == html.ElementNode {
switch n.Data {
case "h1":
dc.SetFontFace(face)
lines := wordWrap(dc, getText(n), canvasWidth-padding*2)
y += len(lines)*28 + len(lines)*lineGap + 5
case "p":
dc.SetFontFace(face)
lines := wordWrap(dc, getText(n), canvasWidth-padding*2)
y += len(lines)*18 + len(lines)*lineGap + 5
case "table":
rows := extractRows(n)
y += len(rows)*30 + 10
case "img":
widthStr := getAttr(n, "width")
h := 100
if widthStr != "" {
h = 100
}
y += h + 10
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
y = computeHeight(c, y, dc, face)
}
return y
}
func printImg(prt *escpos.Escpos, imgPath string) error { func printImg(prt *escpos.Escpos, imgPath string) error {
imgFile, err := os.Open(imgPath) imgFile, err := os.Open(imgPath)
if err != nil { if err != nil {
@@ -318,3 +339,73 @@ func printImg(prt *escpos.Escpos, imgPath string) error {
_, err = prt.WriteRaw(data) _, err = prt.WriteRaw(data)
return err return err
} }
func LoadFont(size float64) (*font.Face, error) {
fontBytes, err := fontFs.ReadFile("static/Zawgyi-One.ttf")
if err != nil {
return nil, err
}
ttfFont, err := opentype.Parse(fontBytes)
if err != nil {
return nil, err
}
face, err := opentype.NewFace(ttfFont, &opentype.FaceOptions{
Size: size,
DPI: 72,
Hinting: font.HintingFull,
})
if err != nil {
return nil, err
}
return &face, nil
}
func SetFontSize(dc *gg.Context, size float64) {
if size < 5 {
return
}
face, err := LoadFont(size)
if err != nil {
log.Println("SetFontSize: ", err.Error())
}
dc.SetFontFace(*face)
}
func nodeHeight(n *html.Node, dc *gg.Context, canvasWidth int, xPadding, yPadding int, face *font.Face, fontSize float64, implicit int) int {
y := 0
if n.Type == html.ElementNode {
h := 0
switch n.Data {
case "h1":
dc.SetFontFace(*face)
lines := wordWrap(dc, getText(n), canvasWidth-xPadding)
h = (len(lines) * eleH1Size) + (len(lines) * lineGap) + yPadding
case "h2":
dc.SetFontFace(*face)
lines := wordWrap(dc, getText(n), canvasWidth-xPadding)
h = (len(lines) * eleH2Size) + (len(lines) * lineGap) + yPadding
case "h3":
dc.SetFontFace(*face)
lines := wordWrap(dc, getText(n), canvasWidth-xPadding)
h = (len(lines) * eleH3Size) + (len(lines) * lineGap) + yPadding
case "p":
dc.SetFontFace(*face)
lines := wordWrap(dc, getText(n), canvasWidth-xPadding)
h = (len(lines) * elePSize) + (len(lines) * lineGap) + yPadding
case "hr":
h = yPadding
case "table":
rows := extractNodeRows(n)
h = (len(rows) * int(fontSize+defalutTableBorder)) + yPadding
case "img":
h = implicit + yPadding
}
if implicit > h {
h = implicit
}
y += h
log.Printf("nodeHeight %s height: %d, \n", n.Data, y)
}
return y
}

51
main.go
View File

@@ -1,51 +0,0 @@
package main
import (
/*
#include <stdint.h>
*/
"C"
"fmt"
_ "image/png"
)
//export Sum
func Sum(a, b int) int {
return a + b
}
func main() {
payload := `{"Name":"Ko Myo","Amount":3000}`
const temp = `
<table>
<tr>
<th><img src="static/logo.png" width="80"/></th>
<th><h1 style="font-size:28">မြန်မာစာသည်တို့စာ (Invoice)</h1></th>
</tr>
</table>
<p>မင်္ဂလာပါ {{.Name}}, သင်၏ အိုင်ဗွိုင်းစိန်း အချက်အလက်များပါသည်။</p>
<p>အထက်ပါ အကွက်နှစ်ကွက်ကတော့ အချိန်နဲ့ တပြေးညီ ဖောင့်ပြောင်းပေးတဲ့ မြန်မာဖောင့် ကွန်ဗာတာပဲ ဖြစ်ပါတယ်။ စာရိုက်ထည့်တာနဲ့ဖြစ်ဖြစ် ဒါမှမဟုတ် ကူးထည့်တာနဲ့ဖြစ်ဖြစ် မြန်မာဖောင့် တစ်ခုကနေ တစ်ခုကို ပြောင်းပေးပါတယ်။ မြန်မာ ယူနီကုဒ်ကနေ ပြောင်းချင်တယ်ဆို မြန်မာ ယူနီကုဒ်ဘက်မှာ ရိုက်ထည့်၊ ကူးထည့်လိုက်တာနဲ့ ဇော်ဂျီဝမ်းဘက်မှာ ဇော်ဂျီဖောင့်ကိုပြောင်းပြီးသား တိုက်ရိုက်ထွက်လာပါမယ်။ အပြန်အလှန်ပါပဲ၊ ဇော်ဂျီကနေပြောင်းချင်တယ်ဆိုရင် ဇော်ဂျီဝမ်းဘက်မှာ ရိုက်ထည့်၊ ကူးထည့်တာနဲ့ မြန်မာ ယူနီကုဒ်ဖောင့်ကို ပြောင်းပြီးသားက မြန်မာ ယူနီကုဒ်အကွက်ထဲမှာ ပေါ်လာမှာဖြစ်ပါတယ်။</p>
<table border="1">
<tr>
<th>ပစ္စည်း</th>
<th>အရေအတွက်</th>
<th>ဈေးနှုန်း</th>
</tr>
<tr>
<td>Hosting</td>
<td>1</td>
<td>{{.Amount}}</td>
</tr>
<tr>
<td>Domain registration</td>
<td>1</td>
<td>$15</td>
</tr>
</table>`
result := GenPNG(C.CString("build/out.png"), C.CString(payload), C.CString(temp))
goResult := C.GoString(result)
fmt.Println("Result:", goResult)
PrintImg(C.CString("usb:/dev/usb/lp1"), C.CString("build/out.png"))
}

View File

@@ -1,4 +1,4 @@
package main package libgofunc
import ( import (
/* /*
@@ -17,12 +17,33 @@ import (
"github.com/kenshaw/escpos" "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)
}
//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)
var out *gousb.OutEndpoint
// printer := "tcp:192.168.100.151:9100" // printer := "tcp:192.168.100.151:9100"
// printer := "usb:/dev/usb/lp1" // printer := "usb:/dev/usb/lp1"
var w *bufio.ReadWriter var w *bufio.ReadWriter
@@ -44,6 +65,40 @@ func PrintImg(printer *C.char, imagePath *C.char) *C.char {
} }
defer f.Close() defer f.Close()
w = bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f)) 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)
} }
prt := escpos.New(w) prt := escpos.New(w)
@@ -59,3 +114,9 @@ func PrintImg(printer *C.char, imagePath *C.char) *C.char {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
return NewOk(nil) return NewOk(nil)
} }
func Print(printer, imagePath string) string {
result := PrintImg(C.CString(printer), C.CString(imagePath))
r := C.GoString(result)
return r
}

View File

@@ -1,4 +1,4 @@
package main package libgofunc
import ( import (
"encoding/json" "encoding/json"

2
vo.go
View File

@@ -1,4 +1,4 @@
package main package libgofunc
import ( import (
/* /*