Files
libgofunc/img.go
2026-04-08 15:31:23 +06:30

412 lines
9.2 KiB
Go

package libgofunc
import (
/*
#include <stdint.h>
*/
"C"
"bytes"
"fmt"
"html/template"
"image"
_ "image/png"
"os"
"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"
)
import "log"
const (
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(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)
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)
}
face, err := LoadFont(defalutFontSize)
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
}
body, height := BuildTree(bodyNode, canvasWidth, dummyDC, face)
// Second pass: actual rendering
dc := gg.NewContext(canvasWidth, height)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetFontFace(*face)
y := 0
renderNode(dc, canvasWidth, body, &y, *face)
err = dc.SavePNG(goPath)
if err != nil {
return NewErr(err)
}
// PrintReceipt(goPath)
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
if err := tmpl.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
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 := range n.Children {
renderNode(dc, canvasWidth, c, y, face)
}
}
func drawTextBlock(dc *gg.Context, n *Node, canvasWidth int, size float64, y *int, face font.Face) {
dc.SetFontFace(face)
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.DrawStringAnchored(line, padding, float64(*y), 0, 0.5)
// dc.DrawString(line, padding, float64(*y))
*y += int(size) + lineGap
}
}
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 *Node, y *int) {
src := n.getSrc()
file, err := os.Open(src)
if err != nil {
return
}
defer file.Close()
img, _, _ := image.Decode(file)
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)
if scale > 0 {
ix = ix / scale
iy = iy / scale
}
dc.DrawImage(img, int(ix), int(iy))
dc.Pop()
if float64(img.Bounds().Dy())*scale > h {
h = float64(img.Bounds().Dy()) * scale
}
*y += int(h)
} else {
dc.DrawImage(img, int(padding), *y)
*y += img.Bounds().Dy()
}
}
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) / colCount
border := table.Style.Border
for _, row := range rows {
x := padding
for _, cell := range row {
if border > 0 {
dc.SetLineWidth(border)
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), fontSize+defalutTableBorder)
}
dc.Stroke()
dc.SetRGB(0, 0, 0)
dc.DrawStringAnchored(cell, float64(x+8), float64(*y+20), 0, 0)
x += cellWidth
}
*y += int(fontSize) + defalutTableBorder
}
}
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) {
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 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 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
}
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
}