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