fix html to png
This commit is contained in:
293
img.go
293
img.go
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package libgofunc
|
||||
|
||||
import (
|
||||
/*
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"image"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"embed"
|
||||
@@ -25,18 +24,24 @@ import (
|
||||
|
||||
"github.com/kenshaw/escpos"
|
||||
)
|
||||
import "log"
|
||||
|
||||
const (
|
||||
canvasWidth = 550
|
||||
padding = 5
|
||||
lineGap = 12
|
||||
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(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
|
||||
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)
|
||||
@@ -58,42 +63,29 @@ func GenPNG(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
|
||||
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,
|
||||
})
|
||||
face, err := LoadFont(defalutFontSize)
|
||||
if err != nil {
|
||||
return NewErr(err)
|
||||
}
|
||||
|
||||
// First pass: compute required height
|
||||
dummyDC := gg.NewContext(canvasWidth, 150)
|
||||
dummyDC.SetFontFace(face)
|
||||
dummyDC.SetFontFace(*face)
|
||||
bodyNode := findNode(root, "body")
|
||||
if bodyNode == nil {
|
||||
bodyNode = root
|
||||
}
|
||||
|
||||
computedHeight := computeHeight(bodyNode, padding, dummyDC, face)
|
||||
body, height := BuildTree(bodyNode, canvasWidth, dummyDC, face)
|
||||
|
||||
// Second pass: actual rendering
|
||||
dc := gg.NewContext(canvasWidth, computedHeight+50)
|
||||
dc := gg.NewContext(canvasWidth, height)
|
||||
dc.SetRGB(1, 1, 1)
|
||||
dc.Clear()
|
||||
dc.SetFontFace(face)
|
||||
dc.SetFontFace(*face)
|
||||
|
||||
y := padding
|
||||
renderNode(dc, bodyNode, &y, face)
|
||||
y := 0
|
||||
renderNode(dc, canvasWidth, body, &y, *face)
|
||||
|
||||
err = dc.SavePNG(goPath)
|
||||
if err != nil {
|
||||
@@ -104,6 +96,12 @@ func GenPNG(outputPath *C.char, payload *C.char, tmpl *C.char) *C.char {
|
||||
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
|
||||
@@ -113,34 +111,48 @@ func renderTemplate(tmp string, data map[string]interface{}) (string, error) {
|
||||
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)
|
||||
}
|
||||
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 := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
renderNode(dc, c, y, face)
|
||||
for _, c := range n.Children {
|
||||
renderNode(dc, canvasWidth, c, y, face)
|
||||
}
|
||||
}
|
||||
|
||||
func drawTextBlock(dc *gg.Context, text string, size float64, y *int, face font.Face) {
|
||||
func drawTextBlock(dc *gg.Context, n *Node, canvasWidth int, size float64, y *int, face font.Face) {
|
||||
dc.SetFontFace(face)
|
||||
lines := wordWrap(dc, text, canvasWidth-padding*2)
|
||||
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.DrawString(line, padding, float64(*y))
|
||||
dc.DrawStringAnchored(line, padding, float64(*y), 0, 0.5)
|
||||
// dc.DrawString(line, padding, float64(*y))
|
||||
*y += int(size) + lineGap
|
||||
}
|
||||
*y += 5
|
||||
}
|
||||
|
||||
func wordWrap(dc *gg.Context, text string, maxWidth int) []string {
|
||||
@@ -164,53 +176,101 @@ func wordWrap(dc *gg.Context, text string, maxWidth int) []string {
|
||||
return lines
|
||||
}
|
||||
|
||||
func drawImage(dc *gg.Context, n *html.Node, y *int) {
|
||||
src := getAttr(n, "src")
|
||||
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)
|
||||
widthStr := getAttr(n, "width")
|
||||
if widthStr != "" {
|
||||
w, _ := strconv.Atoi(widthStr)
|
||||
scale := float64(w) / float64(img.Bounds().Dx())
|
||||
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)
|
||||
dc.DrawImage(img, padding, *y)
|
||||
if scale > 0 {
|
||||
ix = ix / scale
|
||||
iy = iy / scale
|
||||
}
|
||||
dc.DrawImage(img, int(ix), int(iy))
|
||||
dc.Pop()
|
||||
*y += int(float64(img.Bounds().Dy()) * scale)
|
||||
if float64(img.Bounds().Dy())*scale > h {
|
||||
h = float64(img.Bounds().Dy()) * scale
|
||||
}
|
||||
*y += int(h)
|
||||
} else {
|
||||
dc.DrawImage(img, padding, *y)
|
||||
dc.DrawImage(img, int(padding), *y)
|
||||
*y += img.Bounds().Dy()
|
||||
}
|
||||
*y += 10
|
||||
}
|
||||
|
||||
func renderTable(dc *gg.Context, table *html.Node, y *int, face font.Face) {
|
||||
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*2) / colCount
|
||||
dc.SetFontFace(face)
|
||||
cellWidth := (canvasWidth - padding) / colCount
|
||||
border := table.Style.Border
|
||||
for _, row := range rows {
|
||||
x := padding
|
||||
for _, cell := range row {
|
||||
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), 30)
|
||||
if border > 0 {
|
||||
dc.SetLineWidth(border)
|
||||
dc.DrawRectangle(float64(x), float64(*y), float64(cellWidth), fontSize+defalutTableBorder)
|
||||
}
|
||||
dc.Stroke()
|
||||
dc.SetRGB(0, 0, 0)
|
||||
dc.DrawString(cell, float64(x+8), float64(*y+20))
|
||||
dc.DrawStringAnchored(cell, float64(x+8), float64(*y+20), 0, 0)
|
||||
x += cellWidth
|
||||
}
|
||||
*y += 30
|
||||
*y += int(fontSize) + defalutTableBorder
|
||||
}
|
||||
*y += 10
|
||||
}
|
||||
|
||||
func extractRows(table *html.Node) [][]string {
|
||||
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) {
|
||||
@@ -247,15 +307,6 @@ func getText(n *html.Node) string {
|
||||
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
|
||||
@@ -268,36 +319,6 @@ func findNode(n *html.Node, tag string) *html.Node {
|
||||
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 {
|
||||
@@ -318,3 +339,73 @@ func printImg(prt *escpos.Escpos, imgPath string) error {
|
||||
_, 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user