321 lines
6.5 KiB
Go
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
|
|
}
|