Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9c96971a7 | |||
| ecb03c1fb7 | |||
| 016cc5b6fe | |||
| 9a0d1d098c | |||
| 5d885361c0 | |||
| f472187217 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -26,3 +26,5 @@ go.work.sum
|
||||
.env
|
||||
|
||||
build
|
||||
assets
|
||||
libgofunc
|
||||
|
||||
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal 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.
Binary file not shown.
31
build_lib.sh
31
build_lib.sh
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
APP_NAME="libgofunc"
|
||||
VERSION="${1:-v0.1.0}"
|
||||
VERSION="${1:-v0.1.3}"
|
||||
OUTPUT_DIR="assets"
|
||||
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
|
||||
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" \
|
||||
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"
|
||||
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" \
|
||||
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"
|
||||
tar -czf "${OUTPUT_DIR}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" libgofunc_arm64.so
|
||||
ARCHIVE_NAME="${APP_NAME}-${VERSION}-${ARCH}.tar.gz"
|
||||
tar -czf "${OUTPUT_DIR}/${VERSION}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" ./${VERSION}/${ARCH}
|
||||
|
||||
ARCH="armv7a"
|
||||
echo "Building for Android armv7a..."
|
||||
CC="$TOOLCHAIN/armv7a-linux-androideabi$API-clang" \
|
||||
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"
|
||||
tar -czf "${OUTPUT_DIR}/${ARCHIVE_NAME}" -C "${BUILD_DIR}" libgofunc_armv7a.so
|
||||
|
||||
# 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
|
||||
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}
|
||||
else
|
||||
echo "Unsupported OS: $OS"
|
||||
exit 1
|
||||
|
||||
141
canvas.go
Normal file
141
canvas.go
Normal 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
109
cmd/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package libgofunc
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
11
go.mod
11
go.mod
@@ -1,16 +1,17 @@
|
||||
module libgofunc
|
||||
module gt.mokkon.com/sainw/libgofunc
|
||||
|
||||
go 1.22.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/fogleman/gg v1.3.0
|
||||
github.com/kenshaw/escpos v0.0.0-20221114190919-df06b682a8fc
|
||||
golang.org/x/image v0.24.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/image v0.38.0
|
||||
golang.org/x/net v0.52.0
|
||||
)
|
||||
|
||||
require (
|
||||
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
14
go.sum
@@ -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/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.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
|
||||
293
img.go
293
img.go
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package libgofunc
|
||||
|
||||
import (
|
||||
/*
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"image"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"embed"
|
||||
@@ -25,18 +24,24 @@ import (
|
||||
|
||||
"github.com/kenshaw/escpos"
|
||||
)
|
||||
import "log"
|
||||
|
||||
const (
|
||||
canvasWidth = 550
|
||||
padding = 5
|
||||
lineGap = 12
|
||||
defalutFontSize = 18.0
|
||||
defalutTableBorder = 5
|
||||
lineGap = 12
|
||||
elePSize = 18
|
||||
eleH1Size = 32
|
||||
eleH2Size = 24
|
||||
eleH3Size = 19
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var fontFs embed.FS
|
||||
|
||||
//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)
|
||||
goPayload := C.GoString(payload)
|
||||
goTmpl := C.GoString(tmpl)
|
||||
@@ -58,42 +63,29 @@ func GenPNG(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
|
||||
return NewErr(err)
|
||||
}
|
||||
|
||||
fontBytes, err := fontFs.ReadFile("static/Zawgyi-One.ttf")
|
||||
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,
|
||||
})
|
||||
face, err := LoadFont(defalutFontSize)
|
||||
if err != nil {
|
||||
return NewErr(err)
|
||||
}
|
||||
|
||||
// First pass: compute required height
|
||||
dummyDC := gg.NewContext(canvasWidth, 150)
|
||||
dummyDC.SetFontFace(face)
|
||||
dummyDC.SetFontFace(*face)
|
||||
bodyNode := findNode(root, "body")
|
||||
if bodyNode == nil {
|
||||
bodyNode = root
|
||||
}
|
||||
|
||||
computedHeight := computeHeight(bodyNode, padding, dummyDC, face)
|
||||
body, height := BuildTree(bodyNode, canvasWidth, dummyDC, face)
|
||||
|
||||
// Second pass: actual rendering
|
||||
dc := gg.NewContext(canvasWidth, computedHeight+50)
|
||||
dc := gg.NewContext(canvasWidth, height)
|
||||
dc.SetRGB(1, 1, 1)
|
||||
dc.Clear()
|
||||
dc.SetFontFace(face)
|
||||
dc.SetFontFace(*face)
|
||||
|
||||
y := padding
|
||||
renderNode(dc, bodyNode, &y, face)
|
||||
y := 0
|
||||
renderNode(dc, canvasWidth, body, &y, *face)
|
||||
|
||||
err = dc.SavePNG(goPath)
|
||||
if err != nil {
|
||||
@@ -104,6 +96,12 @@ func GenPNG(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
|
||||
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) {
|
||||
tmpl := template.Must(template.New("mytemplate").Parse(tmp))
|
||||
var buf bytes.Buffer
|
||||
@@ -113,34 +111,48 @@ func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func renderNode(dc *gg.Context, n *html.Node, y *int, face font.Face) {
|
||||
if n.Type == html.ElementNode {
|
||||
switch n.Data {
|
||||
case "h1":
|
||||
drawTextBlock(dc, getText(n), 28, y, face)
|
||||
case "p":
|
||||
drawTextBlock(dc, getText(n), 24, y, face)
|
||||
case "img":
|
||||
drawImage(dc, n, y)
|
||||
case "table":
|
||||
renderTable(dc, n, y, face)
|
||||
}
|
||||
func renderNode(dc *gg.Context, canvasWidth int, n *Node, y *int, face font.Face) {
|
||||
before := *y
|
||||
if n.Style.PaddingTop > 0 {
|
||||
*y += int(n.Style.PaddingTop)
|
||||
}
|
||||
switch n.Tag {
|
||||
case "h1":
|
||||
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":
|
||||
drawTextBlock(dc, n, canvasWidth, elePSize, y, face)
|
||||
case "hr":
|
||||
renderLine(dc, n, canvasWidth, y)
|
||||
case "img":
|
||||
drawImage(dc, n, y)
|
||||
case "table":
|
||||
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 := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
renderNode(dc, c, y, face)
|
||||
for _, c := range n.Children {
|
||||
renderNode(dc, canvasWidth, c, y, face)
|
||||
}
|
||||
}
|
||||
|
||||
func drawTextBlock(dc *gg.Context, text string, size float64, y *int, face font.Face) {
|
||||
func drawTextBlock(dc *gg.Context, n *Node, canvasWidth int, size float64, y *int, face font.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 {
|
||||
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 += 5
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func drawImage(dc *gg.Context, n *html.Node, y *int) {
|
||||
src := getAttr(n, "src")
|
||||
func drawImage(dc *gg.Context, n *Node, y *int) {
|
||||
src := n.getSrc()
|
||||
file, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
img, _, _ := image.Decode(file)
|
||||
widthStr := getAttr(n, "width")
|
||||
if widthStr != "" {
|
||||
w, _ := strconv.Atoi(widthStr)
|
||||
scale := float64(w) / float64(img.Bounds().Dx())
|
||||
padding := n.Style.PaddingLeft
|
||||
h := n.Style.Height
|
||||
if n.Style.Width > 0 {
|
||||
ix := padding
|
||||
iy := float64(*y)
|
||||
scale := n.Style.Width / float64(img.Bounds().Dx())
|
||||
dc.Push()
|
||||
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()
|
||||
*y += int(float64(img.Bounds().Dy()) * scale)
|
||||
if float64(img.Bounds().Dy())*scale > h {
|
||||
h = float64(img.Bounds().Dy()) * scale
|
||||
}
|
||||
*y += int(h)
|
||||
} else {
|
||||
dc.DrawImage(img, padding, *y)
|
||||
dc.DrawImage(img, int(padding), *y)
|
||||
*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)
|
||||
if len(rows) == 0 {
|
||||
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])
|
||||
cellWidth := (canvasWidth - padding*2) / colCount
|
||||
dc.SetFontFace(face)
|
||||
cellWidth := (canvasWidth - padding) / colCount
|
||||
border := table.Style.Border
|
||||
for _, row := range rows {
|
||||
x := padding
|
||||
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.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
|
||||
}
|
||||
*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 traverse func(*html.Node)
|
||||
traverse = func(n *html.Node) {
|
||||
@@ -247,15 +307,6 @@ func getText(n *html.Node) 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 {
|
||||
if n.Type == html.ElementNode && n.Data == tag {
|
||||
return n
|
||||
@@ -268,36 +319,6 @@ func findNode(n *html.Node, tag string) *html.Node {
|
||||
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 {
|
||||
imgFile, err := os.Open(imgPath)
|
||||
if err != nil {
|
||||
@@ -318,3 +339,73 @@ func printImg(prt *escpos.Escpos, imgPath string) error {
|
||||
_, err = prt.WriteRaw(data)
|
||||
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
51
main.go
@@ -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"))
|
||||
}
|
||||
65
printer.go
65
printer.go
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package libgofunc
|
||||
|
||||
import (
|
||||
/*
|
||||
@@ -17,12 +17,33 @@ 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)
|
||||
}
|
||||
|
||||
//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
|
||||
@@ -44,6 +65,40 @@ 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)
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -59,3 +114,9 @@ func PrintImg(printer *C.char, imagePath *C.char) *C.char {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return NewOk(nil)
|
||||
}
|
||||
|
||||
func Print(printer, imagePath string) string {
|
||||
result := PrintImg(C.CString(printer), C.CString(imagePath))
|
||||
r := C.GoString(result)
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user