254 lines
10 KiB
Go
254 lines
10 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]
|
|
if item == "M" || item == "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++ }
|
|
} else if item == "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 {
|
|
tx := math.Pow(1-t, 3)*lastX + 3*math.Pow(1-t, 2)*t*x1 + 3*(1-t)*math.Pow(t, 2)*x2 + math.Pow(t, 3)*x
|
|
ty := math.Pow(1-t, 3)*lastY + 3*math.Pow(1-t, 2)*t*y1 + 3*(1-t)*math.Pow(t, 2)*y2 + math.Pow(t, 3)*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++ }
|
|
} else if item == "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 {
|
|
tx := math.Pow(1-t, 2)*lastX + 2*(1-t)*t*x1 + math.Pow(t, 2)*x
|
|
ty := math.Pow(1-t, 2)*lastY + 2*(1-t)*t*y1 + math.Pow(t, 2)*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++ }
|
|
} else { idx++ }
|
|
}
|
|
return pts
|
|
}
|