package libgofunc import ( /* #include */ "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 }