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 }