Files
libgofunc/img.go
2026-03-18 14:36:16 +06:30

321 lines
6.5 KiB
Go

package main
import (
/*
#include <stdint.h>
*/
"C"
"bytes"
"fmt"
"html/template"
"image"
_ "image/png"
"os"
"strconv"
"strings"
"embed"
"github.com/fogleman/gg"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/net/html"
"encoding/json"
"github.com/kenshaw/escpos"
)
const (
canvasWidth = 550
padding = 5
lineGap = 12
)
//go:embed static/*
var fontFs embed.FS
//export GenPNG
func GenPNG(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
goPath := C.GoString(outputPath)
goPayload := C.GoString(payload)
goTmpl := C.GoString(tmpl)
data := make(map[string]interface{})
err := json.Unmarshal([]byte(goPayload), &data)
if err != nil {
return NewErr(err)
}
htmlStr, err := renderTemplate(goTmpl, data)
if err != nil {
return NewErr(err)
}
htmlStr = uni2zg(htmlStr)
root, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
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,
})
if err != nil {
return NewErr(err)
}
// First pass: compute required height
dummyDC := gg.NewContext(canvasWidth, 150)
dummyDC.SetFontFace(face)
bodyNode := findNode(root, "body")
if bodyNode == nil {
bodyNode = root
}
computedHeight := computeHeight(bodyNode, padding, dummyDC, face)
// Second pass: actual rendering
dc := gg.NewContext(canvasWidth, computedHeight+50)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetFontFace(face)
y := padding
renderNode(dc, bodyNode, &y, face)
err = dc.SavePNG(goPath)
if err != nil {
return NewErr(err)
}
// PrintReceipt(goPath)
return NewOk(nil)
}
func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
tmpl := template.Must(template.New("mytemplate").Parse(tmp))
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", err
}
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)
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
renderNode(dc, c, y, face)
}
}
func drawTextBlock(dc *gg.Context, text string, size float64, y *int, face font.Face) {
dc.SetFontFace(face)
lines := wordWrap(dc, text, canvasWidth-padding*2)
for _, line := range lines {
dc.SetRGB(0, 0, 0)
dc.DrawString(line, padding, float64(*y))
*y += int(size) + lineGap
}
*y += 5
}
func wordWrap(dc *gg.Context, text string, maxWidth int) []string {
words := strings.Fields(text)
if len(words) == 0 {
return []string{}
}
var lines []string
current := words[0]
for _, w := range words[1:] {
test := current + " " + w
wWidth, _ := dc.MeasureString(test)
if int(wWidth) > maxWidth {
lines = append(lines, current)
current = w
} else {
current = test
}
}
lines = append(lines, current)
return lines
}
func drawImage(dc *gg.Context, n *html.Node, y *int) {
src := getAttr(n, "src")
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())
dc.Push()
dc.Scale(scale, scale)
dc.DrawImage(img, padding, *y)
dc.Pop()
*y += int(float64(img.Bounds().Dy()) * scale)
} else {
dc.DrawImage(img, padding, *y)
*y += img.Bounds().Dy()
}
*y += 10
}
func renderTable(dc *gg.Context, table *html.Node, y *int, face font.Face) {
rows := extractRows(table)
if len(rows) == 0 {
return
}
colCount := len(rows[0])
cellWidth := (canvasWidth - padding*2) / colCount
dc.SetFontFace(face)
for _, row := range rows {
x := padding
for _, cell := range row {
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), 30)
dc.Stroke()
dc.SetRGB(0, 0, 0)
dc.DrawString(cell, float64(x+8), float64(*y+20))
x += cellWidth
}
*y += 30
}
*y += 10
}
func extractRows(table *html.Node) [][]string {
var rows [][]string
var traverse func(*html.Node)
traverse = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "tr" {
var row []string
for td := n.FirstChild; td != nil; td = td.NextSibling {
if td.Type == html.ElementNode && (td.Data == "td" || td.Data == "th") {
row = append(row, getText(td))
}
}
if len(row) > 0 {
rows = append(rows, row)
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
traverse(c)
}
}
traverse(table)
return rows
}
func getText(n *html.Node) string {
if n == nil {
return ""
}
if n.Type == html.TextNode {
return strings.TrimSpace(n.Data)
}
var buf strings.Builder
for c := n.FirstChild; c != nil; c = c.NextSibling {
buf.WriteString(getText(c))
}
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
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
if res := findNode(c, tag); res != nil {
return res
}
}
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 {
fmt.Println(err)
return err
}
img, _, err := image.Decode(imgFile)
defer imgFile.Close()
if err != nil {
fmt.Println(err)
return err
}
gray := toMonochrome(img)
data := escposRaster(gray)
_, err = prt.WriteRaw(data)
return err
}