8 Commits

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
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
17 changed files with 686 additions and 200 deletions

6
.gitignore vendored
View File

@@ -25,4 +25,8 @@ go.work.sum
# env file # env file
.env .env
build build
assets
libgofunc
libgo
cmd/out.png

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,9 +1,9 @@
#!/bin/bash #!/bin/bash
APP_NAME="libgofunc" APP_NAME="libgofunc"
VERSION="${1:-v0.1.0}" VERSION="${1:-v0.1.7}"
OUTPUT_DIR="assets" OUTPUT_DIR="../assets"
BUILD_DIR="build" BUILD_DIR="../build"
# need Android NDK # need Android NDK
NDK_HOME="$HOME/Android/Sdk/ndk/28.2.13676358" # <--- CHECK YOUR VERSION 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" TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
OS="$(uname -s)" OS="$(uname -s)"
cd cmd
if [ "$OS" = "Darwin" ]; then if [ "$OS" = "Darwin" ]; then
export IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path) export IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
export IOS_SIM_SDK=$(xcrun --sdk iphonesimulator --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)" \ CC="$(xcrun --sdk iphonesimulator --find clang)" \
CGO_CFLAGS="-isysroot $IOS_SIM_SDK -arch x86_64" \ CGO_CFLAGS="-isysroot $IOS_SIM_SDK -arch x86_64" \
CGO_LDFLAGS="-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 \ # xcodebuild -create-xcframework \
# -library build/ios/device/libgofunc_arm64.a -headers build/ios/device/ \ # -library build/ios/device/libgofunc_arm64.a -headers build/ios/device/ \
@@ -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
}

69
cmd/main.go Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
package main package libgofunc
import ( import (
"image" "image"
@@ -91,7 +91,8 @@ func toMonochrome(img image.Image) *image.Gray {
for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ { for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, _ := img.At(x, y).RGBA() 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 { if grayValue > 128 {
gray.Set(x, y, color.White) gray.Set(x, y, color.White)
} else { } else {
@@ -138,7 +139,6 @@ func escposRaster(img *image.Gray) []byte {
data = append(data, b) data = append(data, b)
} }
} }
return data return data
} }

10
go.mod
View File

@@ -1,16 +1,16 @@
module libgofunc module gt.mokkon.com/sainw/libgofunc
go 1.22.0 go 1.26.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 golang.org/x/text v0.35.0 // indirect
) )

12
go.sum
View File

@@ -6,9 +6,9 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
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/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=

416
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,21 +24,35 @@ import (
"github.com/kenshaw/escpos" "github.com/kenshaw/escpos"
) )
import (
"log"
"path"
"strconv"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
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, workingDirC *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)
workingDir := C.GoString(workingDirC)
data := make(map[string]interface{}) data := make(map[string]interface{})
err := json.Unmarshal([]byte(goPayload), &data) err := json.Unmarshal([]byte(goPayload), &data)
@@ -58,42 +71,32 @@ 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) err = renderNode(dc, canvasWidth, body, &y, *face, workingDir)
if err != nil {
return NewErr(err)
}
err = dc.SavePNG(goPath) err = dc.SavePNG(goPath)
if err != nil { if err != nil {
@@ -104,8 +107,22 @@ 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, 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) { 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 var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil { if err := tmpl.Execute(&buf, data); err != nil {
return "", err return "", err
@@ -113,34 +130,55 @@ 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, workingDir string) error {
if n.Type == html.ElementNode { before := *y
switch n.Data { if n.Style.PaddingTop > 0 {
case "h1": *y += int(n.Style.PaddingTop)
drawTextBlock(dc, getText(n), 28, y, face) }
case "p": switch n.Tag {
drawTextBlock(dc, getText(n), 24, y, face) case "h1":
case "img": drawTextBlock(dc, n, canvasWidth, eleH1Size, y, face)
drawImage(dc, n, y) case "h2":
case "table": drawTextBlock(dc, n, canvasWidth, eleH2Size, y, face)
renderTable(dc, n, y, face) case "h3":
drawTextBlock(dc, n, canvasWidth, eleH3Size, y, face)
case "p":
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":
if err := drawImage(dc, n, y, workingDir); err != nil {
return err
}
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 { for _, c := range n.Children {
renderNode(dc, c, y, face) renderNode(dc, canvasWidth, c, y, face, workingDir)
} }
return nil
} }
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) 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 +202,116 @@ 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, workingDir string) error {
src := getAttr(n, "src") src := n.getSrc()
file, err := os.Open(src) s := path.Join(workingDir, src)
file, err := os.Open(s)
if err != nil { if err != nil {
return return fmt.Errorf("open file src: '%s', working directory: '%s', error : %s", src, workingDir, err.Error())
} }
defer file.Close() defer file.Close()
img, _, _ := image.Decode(file) img, _, err := image.Decode(file)
widthStr := getAttr(n, "width") if err != nil {
if widthStr != "" { return err
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.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 return nil
} }
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) headers, 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 i, cell := range row {
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), 30) header := headers[i]
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 if w := header.Style.Width; w > 0 {
x += int(w)
} else {
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) ([]*Node, [][]string) {
var rows [][]string
var headers []*Node
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 td.Tag == "th" {
headers = append(headers, td)
}
}
if len(row) > 0 {
rows = append(rows, row)
}
}
for _, c := range n.Children {
traverse(c)
}
}
traverse(table)
return headers, 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 +348,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 +360,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 {
@@ -311,10 +373,148 @@ func printImg(prt *escpos.Escpos, imgPath string) error {
fmt.Println(err) fmt.Println(err)
return 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) gray := toMonochrome(img)
data := escposRaster(gray) data = escposRaster(gray)
_, 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
}
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)
}
})
}
}

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,12 @@ import (
"github.com/kenshaw/escpos" "github.com/kenshaw/escpos"
) )
import "errors"
//export PrintImg //export PrintImg
func PrintImg(printer *C.char, imagePath *C.char) *C.char { func PrintImg(printer *C.char, imagePath *C.char) *C.char {
goPrinter := C.GoString(printer) goPrinter := C.GoString(printer)
goImagePath := C.GoString(imagePath) goImagePath := C.GoString(imagePath)
// printer := "tcp:192.168.100.151:9100" // 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,18 +44,63 @@ 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)
} else {
return NewErr(errors.New("invalid printer"))
} }
prt := escpos.New(w) prt := escpos.New(w)
prt.Init() prt.Init()
prt.SetSmooth(1) prt.SetSmooth(1)
prt.SetAlign("left")
err := printImg(prt, goImagePath) err := printImg(prt, goImagePath)
if err != nil { if err != nil {
return NewErr(err) return NewErr(err)
} }
prt.WriteRaw([]byte{0x1B, 0x64, 0x03})
prt.Cut() prt.Cut()
prt.End() prt.End()
w.Flush() w.Flush()
time.Sleep(100 * time.Millisecond) time.Sleep(1 * time.Second)
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"

8
vo.go
View File

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