Files
own-tools/pkg/zitie/logic/pdf.go
T
haopengzhan 5c185da0a9 chore: optimize SEO and fix golangci-lint issues
- Implement dynamic SEO meta tags and sitemap.xml for toolbox.pengzhan.dev
- Fix SPA title/meta refresh issue in layout.html
- Address golangci-lint findings:
  - Refactor switch-case for tool path and SVG token parsing
  - Replace math.Pow with direct multiplication for performance
  - Fix unhandled error returns and unused imports
  - Omit redundant type declarations
2026-03-01 09:54:04 -08:00

410 lines
11 KiB
Go

package logic
import (
"bytes"
"math"
"regexp"
"strconv"
"github.com/signintech/gopdf"
"golang.org/x/image/font/sfnt"
)
var (
embeddedFont []byte
fontMap = make(map[string]string)
sfntMap = make(map[string]*sfnt.Font)
)
func SetFontData(data []byte) { embeddedFont = data }
func RegisterFontPath(id, path string) { fontMap[id] = path }
func RegisterFontSFNT(id string, f *sfnt.Font) { sfntMap[id] = f }
// HasGlyph 检查字体是否包含某个字符
func HasGlyph(fontId string, char rune) bool {
f, ok := sfntMap[fontId]
if !ok {
return false
}
var buffer sfnt.Buffer
idx, err := f.GlyphIndex(&buffer, char)
return err == nil && idx != 0
}
// GeneratePDFExtended 支持动态字体切换和缺字降级
func GeneratePDFExtended(chars []HanziData, mode, paperSize, fontId string) ([]byte, error) {
pdf := &gopdf.GoPdf{}
rect := gopdf.Rect{W: 595.28, H: 841.89}
if paperSize == "Letter" {
rect = gopdf.Rect{W: 612, H: 792}
}
pdf.Start(gopdf.Config{PageSize: rect})
if len(embeddedFont) > 0 {
_ = pdf.AddTTFFontData("font", embeddedFont)
}
// 注册所有可用字体
for id, path := range fontMap {
_ = pdf.AddTTFFont(id, path)
}
switch mode {
case "step":
addStepPages(pdf, chars, true, rect.W, rect.H)
case "manuscript":
addManuscriptPages(pdf, chars, rect.W, rect.H, fontId)
default:
addTeachingPages(pdf, chars, true, rect.W, rect.H)
}
var buf bytes.Buffer
_, err := pdf.WriteTo(&buf)
return buf.Bytes(), err
}
func addTeachingPages(pdf *gopdf.GoPdf, chars []HanziData, flipY bool, pW, pH float64) {
cols := 2
gap, margin := 17.0, 40.0
size := math.Min((pW-2*margin-gap)/2, (pH-2*margin-2*gap)/3)
totalW, totalH := 2*size+gap, 3*size+2*gap
marginX, marginY := (pW-totalW)/2, (pH-totalH)/2
for i := 0; i < len(chars); i += 6 {
pdf.AddPage()
end := i + 6
if end > len(chars) {
end = len(chars)
}
for idx, data := range chars[i:end] {
drawCharacter(pdf, data, marginX+float64(idx%cols)*(size+gap), marginY+float64(idx/cols)*(size+gap), size, flipY, len(data.Strokes), true)
}
}
}
func addStepPages(pdf *gopdf.GoPdf, chars []HanziData, flipY bool, pW, pH float64) {
cols, margin := 9, 30.0
size := (pW - 2*margin) / float64(cols)
pdf.AddPage()
currY := margin
for _, data := range chars {
numS := len(data.Strokes)
rowsN := 1
if numS > 8 {
rowsN = 1 + (numS-8+7)/8
}
if currY+float64(rowsN)*size > pH-margin {
pdf.AddPage()
currY = margin
}
totalG := rowsN * cols
strokeC := 0
for gIdx := 0; gIdx < totalG; gIdx++ {
r, c := gIdx/cols, gIdx%cols
x, y := margin+float64(c)*size, currY+float64(r)*size
if r == 0 && c == 0 {
drawCharacter(pdf, data, x, y, size, flipY, len(data.Strokes), false)
strokeC = 1
} else if r > 0 && c == 0 {
drawMiZiGe(pdf, x, y, size)
} else if strokeC <= numS {
drawStepBox(pdf, data, x, y, size, flipY, strokeC)
strokeC++
} else {
drawMiZiGe(pdf, x, y, size)
}
}
currY += float64(rowsN) * size
}
}
func addManuscriptPages(pdf *gopdf.GoPdf, chars []HanziData, pW, pH float64, targetFont string) {
margin := 40.0
cols, rows := 10, 15
colW, rowH := (pW-2*margin)/float64(cols), (pH-2*margin)/float64(rows)
fontSize := colW * 0.75
pdf.AddPage()
drawManuscriptGrid(pdf, pW, pH, margin, cols, colW)
cIdx, rIdx := 0, 0
for _, data := range chars {
if data.Character == "\n" {
cIdx++
rIdx = 0
if cIdx >= cols {
pdf.AddPage()
cIdx = 0
drawManuscriptGrid(pdf, pW, pH, margin, cols, colW)
}
continue
}
if rIdx >= rows {
cIdx++
rIdx = 0
if cIdx >= cols {
pdf.AddPage()
cIdx = 0
drawManuscriptGrid(pdf, pW, pH, margin, cols, colW)
}
}
x, y := pW-margin-float64(cIdx+1)*colW, margin+float64(rIdx)*rowH
charRune := []rune(data.Character)[0]
// 智能字体选择
renderFont := targetFont
isFallback := false
if !HasGlyph(targetFont, charRune) {
// 尝试降级到字库最全的宋体或楷体
if HasGlyph("songti", charRune) {
renderFont = "songti"
isFallback = true
} else if HasGlyph("kaiti", charRune) {
renderFont = "kaiti"
isFallback = true
} else {
// 全部缺失,保留空白
rIdx++
continue
}
}
pdf.SetFillColor(200, 200, 200)
if isFallback {
// 降级显示:小字 (1/2 大小),右上角对齐
smallSize := fontSize * 0.5
_ = pdf.SetFont(renderFont, "", smallSize)
// 计算右上角位置:X靠右,Y靠上
pdf.SetXY(x+colW-smallSize-2, y+2)
_ = pdf.Cell(nil, data.Character)
} else {
// 正常显示
_ = pdf.SetFont(renderFont, "", fontSize)
pdf.SetXY(x+(colW-fontSize*0.9)/2, y+(rowH-fontSize)/2)
_ = pdf.Cell(nil, data.Character)
}
rIdx++
}
}
func drawManuscriptGrid(pdf *gopdf.GoPdf, pW, pH, margin float64, cols int, colW float64) {
pdf.SetStrokeColor(200, 0, 0)
pdf.SetLineWidth(0.6)
for c := 0; c <= cols; c++ {
x := pW - margin - float64(c)*colW
pdf.Line(x, margin, x, pH-margin)
}
pdf.Line(margin, margin, pW-margin, margin)
pdf.Line(margin, pH-margin, pW-margin, pH-margin)
}
func drawCharacter(pdf *gopdf.GoPdf, data HanziData, x, y, size float64, flipY bool, strokeLimit int, showAnnotations bool) {
if showAnnotations {
drawGrid(pdf, x, y, size)
} else {
drawMiZiGe(pdf, x, y, size)
}
p, drawS := size*0.12, size-(size*0.12*2)
scale := drawS / 1024.0
if strokeLimit == len(data.Strokes) {
if showAnnotations {
pdf.SetFillColor(240, 240, 240)
} else {
pdf.SetFillColor(0, 0, 0)
}
} else {
pdf.SetFillColor(180, 180, 180)
}
for sIdx := 0; sIdx < len(data.Strokes); sIdx++ {
if sIdx >= strokeLimit && strokeLimit != len(data.Strokes) {
break
}
pts := parseSVGPath(data.Strokes[sIdx], scale, x+p, y+p, flipY)
if len(pts) > 2 {
pdf.Polygon(pts, "F")
}
}
if showAnnotations && strokeLimit == len(data.Strokes) {
fS := size * 0.035
_ = pdf.SetFont("font", "", fS)
for idx, median := range data.Medians {
mP := transformPoints(median, scale, x+p, y+p, flipY)
if len(mP) < 2 {
continue
}
pdf.SetStrokeColor(255, 0, 0)
pdf.SetLineWidth(0.6)
for j := 0; j < len(mP)-1; j++ {
pdf.Line(mP[j].X, mP[j].Y, mP[j+1].X, mP[j+1].Y)
}
pdf.SetFillColor(255, 0, 0)
drawArrow(pdf, mP[len(mP)-1], mP[len(mP)-2], size*0.035)
r := size * 0.025
dx, dy := mP[1].X-mP[0].X, mP[1].Y-mP[0].Y
dist := math.Sqrt(dx*dx + dy*dy)
ux, uy := -1.0, -1.0
if dist > 0.001 {
ux, uy = dx/dist, dy/dist
}
cX, cY := mP[0].X-ux*r, mP[0].Y-uy*r
pdf.SetStrokeColor(255, 0, 0)
pdf.SetLineWidth(0.5)
pdf.Oval(cX-r, cY-r, cX+r, cY+r)
numS := strconv.Itoa(idx + 1)
tw := fS * 0.6 * float64(len(numS)) / 2
if len(numS) > 1 {
tw = fS * 0.5
}
pdf.SetFillColor(255, 0, 0)
pdf.SetXY(cX-tw/2, cY-fS/2)
_ = pdf.Cell(nil, numS)
}
}
}
func drawStepBox(pdf *gopdf.GoPdf, data HanziData, x, y, size float64, flipY bool, limit int) {
drawMiZiGe(pdf, x, y, size)
p, scale := size*0.12, (size-(size*0.12*2))/1024.0
pdf.SetFillColor(180, 180, 180)
for i := 0; i < limit; i++ {
pts := parseSVGPath(data.Strokes[i], scale, x+p, y+p, flipY)
if len(pts) > 2 {
pdf.Polygon(pts, "F")
}
}
}
func drawGrid(pdf *gopdf.GoPdf, x, y, size float64) {
pdf.SetStrokeColor(220, 220, 220)
pdf.SetLineWidth(0.4)
pdf.RectFromUpperLeft(x, y, size, size)
mid := size / 2
drawDashedLine(pdf, x, y+mid, x+size, y+mid)
drawDashedLine(pdf, x+mid, y, x+mid, y+size)
}
func drawMiZiGe(pdf *gopdf.GoPdf, x, y, size float64) {
pdf.SetStrokeColor(220, 220, 220)
pdf.SetLineWidth(0.4)
pdf.RectFromUpperLeft(x, y, size, size)
mid := size / 2
drawDashedLine(pdf, x, y+mid, x+size, y+mid)
drawDashedLine(pdf, x+mid, y, x+mid, y+size)
drawDashedLine(pdf, x, y, x+size, y+size)
drawDashedLine(pdf, x, y+size, x+size, y)
}
func drawDashedLine(pdf *gopdf.GoPdf, x1, y1, x2, y2 float64) {
dash := 2.0
dx, dy := x2-x1, y2-y1
dist := math.Sqrt(dx*dx + dy*dy)
if dist < 0.001 {
return
}
ux, uy := dx/dist, dy/dist
for i := 0.0; i < dist; i += dash * 2 {
end := i + dash
if end > dist {
end = dist
}
pdf.Line(x1+ux*i, y1+uy*i, x1+ux*end, y1+uy*end)
}
}
func drawArrow(pdf *gopdf.GoPdf, target, prev gopdf.Point, length float64) {
angle := math.Atan2(target.Y-prev.Y, target.X-prev.X)
arrowA := math.Pi / 6
p1X := target.X + length*math.Cos(angle+math.Pi+arrowA)
p1Y := target.Y + length*math.Sin(angle+math.Pi+arrowA)
p2X := target.X + length*math.Cos(angle+math.Pi-arrowA)
p2Y := target.Y + length*math.Sin(angle+math.Pi-arrowA)
pdf.Polygon([]gopdf.Point{target, {X: p1X, Y: p1Y}, {X: p2X, Y: p2Y}}, "F")
}
func transformPoints(pts []Point, scale, ox, oy float64, flipY bool) []gopdf.Point {
res := make([]gopdf.Point, len(pts))
for i, p := range pts {
y := p[1]
if flipY {
y = 1024 - y
}
res[i] = gopdf.Point{X: ox + p[0]*scale, Y: oy + y*scale}
}
return res
}
var reSVG = regexp.MustCompile(`([MLQCZ])|(-?\d+\.?\d*)`)
func parseSVGPath(path string, scale, ox, oy float64, flipY bool) []gopdf.Point {
var pts []gopdf.Point
matches := reSVG.FindAllStringSubmatch(path, -1)
var lastX, lastY float64
for idx := 0; idx < len(matches); {
item := matches[idx][0]
switch item {
case "M", "L":
if idx+2 < len(matches) {
x, _ := strconv.ParseFloat(matches[idx+1][0], 64)
y, _ := strconv.ParseFloat(matches[idx+2][0], 64)
lastX, lastY = x, y
if flipY {
y = 1024 - y
}
pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale})
idx += 3
} else {
idx++
}
case "C":
if idx+6 < len(matches) {
x1, _ := strconv.ParseFloat(matches[idx+1][0], 64)
y1, _ := strconv.ParseFloat(matches[idx+2][0], 64)
x2, _ := strconv.ParseFloat(matches[idx+3][0], 64)
y2, _ := strconv.ParseFloat(matches[idx+4][0], 64)
x, _ := strconv.ParseFloat(matches[idx+5][0], 64)
y, _ := strconv.ParseFloat(matches[idx+6][0], 64)
for t := 0.2; t <= 1.0; t += 0.2 {
invT := 1 - t
tx := invT*invT*invT*lastX + 3*invT*invT*t*x1 + 3*invT*t*t*x2 + t*t*t*x
ty := invT*invT*invT*lastY + 3*invT*invT*t*y1 + 3*invT*t*t*y2 + t*t*t*y
ty_f := ty
if flipY {
ty_f = 1024 - ty
}
pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty_f*scale})
}
lastX, lastY = x, y
idx += 7
} else {
idx++
}
case "Q":
if idx+4 < len(matches) {
x1, _ := strconv.ParseFloat(matches[idx+1][0], 64)
y1, _ := strconv.ParseFloat(matches[idx+2][0], 64)
x, _ := strconv.ParseFloat(matches[idx+3][0], 64)
y, _ := strconv.ParseFloat(matches[idx+4][0], 64)
for t := 0.2; t <= 1.0; t += 0.2 {
invT := 1 - t
tx := invT*invT*lastX + 2*invT*t*x1 + t*t*x
ty := invT*invT*lastY + 2*invT*t*y1 + t*t*y
ty_f := ty
if flipY {
ty_f = 1024 - ty
}
pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty_f*scale})
}
lastX, lastY = x, y
idx += 5
} else {
idx++
}
default:
idx++
}
}
return pts
}