From 5c185da0a9e1a1d3f21e07c1c15181e882a8fa3a Mon Sep 17 00:00:00 2001 From: haopengzhan Date: Sun, 1 Mar 2026 09:54:04 -0800 Subject: [PATCH] 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 --- cmd/toolbox/main.go | 52 ++++- cmd/toolbox/web/layout.html | 51 ++++- pkg/learnnumber/data/icons.go | 2 +- pkg/learnnumber/logic/counting.go | 257 ++++++++++++++++------ pkg/learnnumber/logic/writing.go | 107 +++++++--- pkg/learnnumber/tool.go | 10 +- pkg/zitie/logic/pdf.go | 344 ++++++++++++++++++++++-------- pkg/zitie/logic/types.go | 6 +- pkg/zitie/tool.go | 57 +++-- 9 files changed, 657 insertions(+), 229 deletions(-) diff --git a/cmd/toolbox/main.go b/cmd/toolbox/main.go index ee08f33..8ace6ee 100644 --- a/cmd/toolbox/main.go +++ b/cmd/toolbox/main.go @@ -4,6 +4,7 @@ import ( "bytes" "embed" "flag" + "fmt" "html/template" "io/fs" "log" @@ -56,6 +57,23 @@ func main() { subFS, _ := fs.Sub(webFS, "web") r.StaticFS("/static", http.FS(subFS)) + // 2.5 SEO 静态文件映射 + r.GET("/robots.txt", func(c *gin.Context) { + c.String(http.StatusOK, "User-agent: *\nAllow: /\nSitemap: https://toolbox.pengzhan.dev/sitemap.xml\n") + }) + r.GET("/sitemap.xml", func(c *gin.Context) { + var buf bytes.Buffer + buf.WriteString("\n") + buf.WriteString("\n") + buf.WriteString(" https://toolbox.pengzhan.dev/1.0\n") + + // 动态根据注册的工具生成路由 + for id := range base.Registry { + fmt.Fprintf(&buf, " https://toolbox.pengzhan.dev/%s0.8\n", id) + } + buf.WriteString("") + c.Data(http.StatusOK, "application/xml; charset=utf-8", buf.Bytes()) + }) // 3. 模板引擎初始化 // 递归加载 web 目录下所有的 .html 文件 tmpl, err := template.ParseFS(subFS, "layout.html", "index.html", "tools/*.html") @@ -65,11 +83,32 @@ func main() { // 通用页面渲染函数 serveIndex := func(c *gin.Context) { - data := gin.H{ - "Title": "Own-Tools", - "GA_ID": gaID, - } + path := c.Request.URL.Path + title := "探索工具箱" + desc := "基于 Go 语言构建的模块化个人工具箱,提供汉字字帖生成、数字学习等多种实用生产力工具。" + keywords := "个人工具箱, 汉字字帖生成, 书法练习, 数字学习, Own-Tools" + baseURL := "https://toolbox.pengzhan.dev" + canonical := baseURL + path + // 针对不同路径设置 SEO 标签 + switch path { + case "/zitie": + title = "汉字字帖生成器 - 教学方格与步进式分解" + desc = "在线生成 2x3 教学方格字帖和 9 列步进式笔顺分解字帖,支持多种书法字体和古风排版。" + keywords = "字帖生成, 笔顺分解, 汉字教学, 书法字帖, 练字" + case "/learn-number": + title = "趣味数字学习工具" + desc = "为儿童设计的数字学习与计数练习工具,生动活泼,寓教于乐。" + keywords = "数字学习, 儿童计数, 幼小衔接" + } + data := gin.H{ + "Title": title, + "Description": desc, + "Keywords": keywords, + "CanonicalURL": canonical, + "GA_ID": gaID, + } + var buf bytes.Buffer // 因为已经 Sub 了,模板名就是文件名 if err := tmpl.ExecuteTemplate(&buf, "layout.html", data); err != nil { @@ -78,12 +117,13 @@ func main() { } c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes()) } - r.GET("/", serveIndex) r.GET("/api/tools", func(c *gin.Context) { var list []map[string]string ids := make([]string, 0, len(base.Registry)) - for id := range base.Registry { ids = append(ids, id) } + for id := range base.Registry { + ids = append(ids, id) + } sort.Strings(ids) for _, id := range ids { diff --git a/cmd/toolbox/web/layout.html b/cmd/toolbox/web/layout.html index 9eb3c4d..cf56bf4 100644 --- a/cmd/toolbox/web/layout.html +++ b/cmd/toolbox/web/layout.html @@ -4,6 +4,24 @@ {{.Title}} | Own-Tools + + + + + + + + + + + + + + + + + + {{if .GA_ID}} @@ -126,10 +144,41 @@ renderCurrentPath(); } + const metaConfig = { + 'welcome': { + title: '探索工具箱', + desc: '基于 Go 语言构建的模块化个人工具箱,提供汉字字帖生成、数字学习等多种实用生产力工具。', + keywords: '个人工具箱, 汉字字帖生成, 书法练习, 数字学习, Own-Tools' + }, + 'zitie': { + title: '汉字字帖生成器 - 教学方格与步进式分解', + desc: '在线生成 2x3 教学方格字帖和 9 列步进式笔顺分解字帖,支持多种书法字体和古风排版。', + keywords: '字帖生成, 笔顺分解, 汉字教学, 书法字帖, 练字' + }, + 'learn-number': { + title: '趣味数字学习工具', + desc: '为儿童设计的数字学习与计数练习工具,生动活泼,寓教于乐。', + keywords: '数字学习, 儿童计数, 幼小衔接' + } + }; + function renderCurrentPath() { const path = window.location.pathname.replace('/', '') || 'welcome'; - // 隐藏所有面板 + // 1. 更新标题与元数据 + const meta = metaConfig[path] || metaConfig['welcome']; + document.title = `${meta.title} | Own-Tools`; + document.querySelector('meta[name="description"]').setAttribute('content', meta.desc); + document.querySelector('meta[name="keywords"]').setAttribute('content', meta.keywords); + + // 更新 Open Graph 标签 (可选,社交分享主要看服务端渲染) + const canonical = `https://toolbox.pengzhan.dev/${path === 'welcome' ? '' : path}`; + document.querySelector('link[rel="canonical"]').setAttribute('href', canonical); + document.querySelector('meta[property="og:title"]').setAttribute('content', `${meta.title} | Own-Tools`); + document.querySelector('meta[property="og:description"]').setAttribute('content', meta.desc); + document.querySelector('meta[property="og:url"]').setAttribute('content', canonical); + + // 2. 隐藏所有面板 document.querySelectorAll('.tool-panel').forEach(p => p.classList.remove('active')); // 显示目标面板 diff --git a/pkg/learnnumber/data/icons.go b/pkg/learnnumber/data/icons.go index 8fbc153..dbab1c3 100644 --- a/pkg/learnnumber/data/icons.go +++ b/pkg/learnnumber/data/icons.go @@ -31,7 +31,7 @@ var IconCategories = map[string][]Icon{ {Name: "Trapezoid", Paths: []string{"M 300 200 L 724 200 L 924 800 L 100 800 Z"}}, {Name: "Hexagon", Paths: []string{"M 512 100 L 858 300 L 858 724 L 512 924 L 166 724 L 166 300 Z"}}, }, - "fruits": {}, + "fruits": {}, } type SVG struct { diff --git a/pkg/learnnumber/logic/counting.go b/pkg/learnnumber/logic/counting.go index 02bcc8e..2a5fd43 100644 --- a/pkg/learnnumber/logic/counting.go +++ b/pkg/learnnumber/logic/counting.go @@ -7,7 +7,6 @@ import ( "regexp" "strconv" "strings" - "time" "toolbox/pkg/learnnumber/data" "github.com/signintech/gopdf" @@ -33,15 +32,20 @@ type PathResult struct { var reSVGToken = regexp.MustCompile(`[a-zA-Z]|-?\d+\.?\d*`) func GenerateCountingPDF(req CountingRequest) ([]byte, error) { - rand.Seed(time.Now().UnixNano()) pdf := &gopdf.GoPdf{} rect := gopdf.Rect{W: 595.28, H: 841.89} - if req.PaperSize == "Letter" { rect = gopdf.Rect{W: 612, H: 792} } + if req.PaperSize == "Letter" { + rect = gopdf.Rect{W: 612, H: 792} + } pdf.Start(gopdf.Config{PageSize: rect}) // 硬性限制:页数 1-10 页,防止资源耗尽 - if req.PageCount < 1 { req.PageCount = 1 } - if req.PageCount > 10 { req.PageCount = 10 } + if req.PageCount < 1 { + req.PageCount = 1 + } + if req.PageCount > 10 { + req.PageCount = 10 + } for i := 0; i < req.PageCount; i++ { pdf.AddPage() @@ -57,25 +61,40 @@ func GenerateCountingPDF(req CountingRequest) ([]byte, error) { func drawProblemV4(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64) { margin := 40.0 boxW, boxH := (pW-2*margin)*0.7, pH-60.0 - xBase, yBase := margin, startY + 30.0 + xBase, yBase := margin, startY+30.0 - pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.5) + pdf.SetStrokeColor(0, 0, 0) + pdf.SetLineWidth(2.5) pdf.RectFromUpperLeft(xBase, yBase, boxW, boxH) catIcons := data.IconCategories[req.Category] - if len(catIcons) == 0 { catIcons = data.IconCategories["shapes"] } - + if len(catIcons) == 0 { + catIcons = data.IconCategories["shapes"] + } + numTypes := req.IconTypes - if numTypes < 1 { numTypes = 1 }; if numTypes > 6 { numTypes = 6 } - if numTypes > len(catIcons) { numTypes = len(catIcons) } - + if numTypes < 1 { + numTypes = 1 + } + if numTypes > 6 { + numTypes = 6 + } + if numTypes > len(catIcons) { + numTypes = len(catIcons) + } + total := req.TotalCount - if total < numTypes { total = numTypes }; if total > 30 { total = 30 } + if total < numTypes { + total = numTypes + } + if total > 30 { + total = 30 + } allIconsPerm := rand.Perm(len(catIcons)) var selectedIcons []data.Icon counts := make([]int, numTypes) - + // 随机分配逻辑 for i := 0; i < numTypes; i++ { selectedIcons = append(selectedIcons, catIcons[allIconsPerm[i]]) @@ -87,49 +106,80 @@ func drawProblemV4(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64 } avgRadius := math.Sqrt((boxW * boxH * 0.22) / (float64(total) * math.Pi)) - if avgRadius > 35.0 { avgRadius = 35.0 } + if avgRadius > 35.0 { + avgRadius = 35.0 + } var placed []PlacedIcon iconTypeIdx, currentInType := 0, 0 for i := 0; i < total; i++ { - scaleVar := 0.9 + rand.Float64()*0.2; r := avgRadius * scaleVar + scaleVar := 0.9 + rand.Float64()*0.2 + r := avgRadius * scaleVar for retry := 0; retry < 200; retry++ { randX := xBase + r + 5 + rand.Float64()*(boxW-2*r-10) randY := yBase + r + 5 + rand.Float64()*(boxH-2*r-10) collision := false - for _, p := range placed { - if math.Sqrt(math.Pow(randX-p.X, 2)+math.Pow(randY-p.Y, 2)) < (r + p.Radius + 10.0) { - collision = true; break - } - } + for _, p := range placed { + dx := randX - p.X + dy := randY - p.Y + if math.Sqrt(dx*dx+dy*dy) < (r + p.Radius + 10.0) { + collision = true + break + } + } + if !collision { drawSimpleIcon(pdf, selectedIcons[iconTypeIdx], randX, randY, r*2) - placed = append(placed, PlacedIcon{X: randX, Y: randY, Radius: r}); break + placed = append(placed, PlacedIcon{X: randX, Y: randY, Radius: r}) + break } } - currentInType++; if currentInType >= counts[iconTypeIdx] { iconTypeIdx++; currentInType = 0 } + currentInType++ + if currentInType >= counts[iconTypeIdx] { + iconTypeIdx++ + currentInType = 0 + } } legendX, legendStepY := xBase+boxW+20.0, boxH/float64(numTypes+1) for i := 0; i < numTypes; i++ { lY := yBase + float64(i+1)*legendStepY drawSimpleIcon(pdf, selectedIcons[i], legendX+25, lY, 35) - pdf.SetStrokeColor(150, 150, 150); pdf.SetLineWidth(1.0); pdf.RectFromUpperLeft(legendX+60, lY-15, 35, 35) + pdf.SetStrokeColor(150, 150, 150) + pdf.SetLineWidth(1.0) + pdf.RectFromUpperLeft(legendX+60, lY-15, 35, 35) } } func drawSimpleIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) { rawResults := parseMultiPath(icon, 1.0, 0, 0) - if len(rawResults) == 0 { return } - var minX, minY, maxX, maxY float64 = 1e9, 1e9, -1e9, -1e9 + if len(rawResults) == 0 { + return + } + var minX, minY, maxX, maxY = 1e9, 1e9, -1e9, -1e9 for _, res := range rawResults { for _, p := range res.Points { - if p.X < minX { minX = p.X }; if p.X > maxX { maxX = p.X } - if p.Y < minY { minY = p.Y }; if p.Y > maxY { maxY = p.Y } + if p.X < minX { + minX = p.X + } + if p.X > maxX { + maxX = p.X + } + if p.Y < minY { + minY = p.Y + } + if p.Y > maxY { + maxY = p.Y + } } } curW, curH := maxX-minX, maxY-minY - if curW <= 0 { curW = 1 }; if curH <= 0 { curH = 1 } + if curW <= 0 { + curW = 1 + } + if curH <= 0 { + curH = 1 + } scale := size / math.Max(curW, curH) ox, oy := cX-(curW*scale)/2-minX*scale, cY-(curH*scale)/2-minY*scale @@ -141,10 +191,16 @@ func drawSimpleIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) { } for _, res := range rawResults { scaledPts := make([]gopdf.Point, len(res.Points)) - for i, p := range res.Points { scaledPts[i] = gopdf.Point{X: ox + p.X*scale, Y: oy + p.Y*scale} } + for i, p := range res.Points { + scaledPts[i] = gopdf.Point{X: ox + p.X*scale, Y: oy + p.Y*scale} + } if len(scaledPts) > 1 { - if res.Closed { pdf.Polygon(scaledPts, "D") } else { - for j := 0; j < len(scaledPts)-1; j++ { pdf.Line(scaledPts[j].X, scaledPts[j].Y, scaledPts[j+1].X, scaledPts[j+1].Y) } + if res.Closed { + pdf.Polygon(scaledPts, "D") + } else { + for j := 0; j < len(scaledPts)-1; j++ { + pdf.Line(scaledPts[j].X, scaledPts[j].Y, scaledPts[j+1].X, scaledPts[j+1].Y) + } } } } @@ -163,57 +219,122 @@ func parseMultiPath(icon data.Icon, scale, ox, oy float64) []PathResult { if (token[0] >= 'a' && token[0] <= 'z') || (token[0] >= 'A' && token[0] <= 'Z') { cmd := strings.ToUpper(token) if cmd == "M" { - if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: false}); pts = nil } + if len(pts) > 0 { + results = append(results, PathResult{Points: pts, Closed: false}) + pts = nil + } } else if cmd == "Z" { - if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: true}); pts = nil } - lx, ly = startX, startY; i++; continue + if len(pts) > 0 { + results = append(results, PathResult{Points: pts, Closed: true}) + pts = nil + } + lx, ly = startX, startY + i++ + continue } - currentCmd = cmd; isRel = (token[0] >= 'a' && token[0] <= 'z'); i++ + currentCmd = cmd + isRel = (token[0] >= 'a' && token[0] <= 'z') + i++ continue } switch currentCmd { case "M", "L": - if i+1 >= len(tokens) { i = len(tokens); break } - x, _ := strconv.ParseFloat(tokens[i], 64); y, _ := strconv.ParseFloat(tokens[i+1], 64) - if isRel { x += lx; y += ly } - if currentCmd == "M" { startX, startY = x, y } + if i+1 >= len(tokens) { + i = len(tokens) + break + } + x, _ := strconv.ParseFloat(tokens[i], 64) + y, _ := strconv.ParseFloat(tokens[i+1], 64) + if isRel { + x += lx + y += ly + } + if currentCmd == "M" { + startX, startY = x, y + } lx, ly = x, y - pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale}); i += 2 - if currentCmd == "M" { currentCmd = "L" } + pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale}) + i += 2 + if currentCmd == "M" { + currentCmd = "L" + } case "H": - x, _ := strconv.ParseFloat(tokens[i], 64); if isRel { x += lx }; lx = x - pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + ly*scale}); i++ + x, _ := strconv.ParseFloat(tokens[i], 64) + if isRel { + x += lx + } + lx = x + pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + ly*scale}) + i++ case "V": - y, _ := strconv.ParseFloat(tokens[i], 64); if isRel { y += ly }; ly = y - pts = append(pts, gopdf.Point{X: ox + lx*scale, Y: oy + y*scale}); i++ + y, _ := strconv.ParseFloat(tokens[i], 64) + if isRel { + y += ly + } + ly = y + pts = append(pts, gopdf.Point{X: ox + lx*scale, Y: oy + y*scale}) + i++ case "C": - if i+5 >= len(tokens) { i = len(tokens); break } - x1, _ := strconv.ParseFloat(tokens[i], 64); y1, _ := strconv.ParseFloat(tokens[i+1], 64) - x2, _ := strconv.ParseFloat(tokens[i+2], 64); y2, _ := strconv.ParseFloat(tokens[i+3], 64) - x, _ := strconv.ParseFloat(tokens[i+4], 64); y, _ := strconv.ParseFloat(tokens[i+5], 64) - if isRel { x1 += lx; y1 += ly; x2 += lx; y2 += ly; x += lx; y += ly } - for t := 0.2; t <= 1.0; t += 0.2 { - tx := math.Pow(1-t, 3)*lx + 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)*ly + 3*math.Pow(1-t, 2)*t*y1 + 3*(1-t)*math.Pow(t, 2)*y2 + math.Pow(t, 3)*y - pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale}) + if i+5 >= len(tokens) { + i = len(tokens) + break } - lx, ly = x, y; i += 6 + x1, _ := strconv.ParseFloat(tokens[i], 64) + y1, _ := strconv.ParseFloat(tokens[i+1], 64) + x2, _ := strconv.ParseFloat(tokens[i+2], 64) + y2, _ := strconv.ParseFloat(tokens[i+3], 64) + x, _ := strconv.ParseFloat(tokens[i+4], 64) + y, _ := strconv.ParseFloat(tokens[i+5], 64) + if isRel { + x1 += lx + y1 += ly + x2 += lx + y2 += ly + x += lx + y += ly + } + for t := 0.2; t <= 1.0; t += 0.2 { + invT := 1 - t + tx := invT*invT*invT*lx + 3*invT*invT*t*x1 + 3*invT*t*t*x2 + t*t*t*x + ty := invT*invT*invT*ly + 3*invT*invT*t*y1 + 3*invT*t*t*y2 + t*t*t*y + pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale}) + } + + lx, ly = x, y + i += 6 case "Q": - if i+3 >= len(tokens) { i = len(tokens); break } - x1, _ := strconv.ParseFloat(tokens[i], 64); y1, _ := strconv.ParseFloat(tokens[i+1], 64) - x, _ := strconv.ParseFloat(tokens[i+2], 64); y, _ := strconv.ParseFloat(tokens[i+3], 64) - if isRel { x1 += lx; y1 += ly; x += lx; y += ly } - for t := 0.25; t <= 1.0; t += 0.25 { - tx := math.Pow(1-t, 2)*lx + 2*(1-t)*t*x1 + math.Pow(t, 2)*x - ty := math.Pow(1-t, 2)*ly + 2*(1-t)*t*y1 + math.Pow(t, 2)*y - pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale}) + if i+3 >= len(tokens) { + i = len(tokens) + break } - lx, ly = x, y; i += 4 - case "A": i += 7 - default: i++ + x1, _ := strconv.ParseFloat(tokens[i], 64) + y1, _ := strconv.ParseFloat(tokens[i+1], 64) + x, _ := strconv.ParseFloat(tokens[i+2], 64) + y, _ := strconv.ParseFloat(tokens[i+3], 64) + if isRel { + x1 += lx + y1 += ly + x += lx + y += ly + } + for t := 0.25; t <= 1.0; t += 0.25 { + invT := 1 - t + tx := invT*invT*lx + 2*invT*t*x1 + t*t*x + ty := invT*invT*ly + 2*invT*t*y1 + t*t*y + pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale}) + } + + lx, ly = x, y + i += 4 + case "A": + i += 7 + default: + i++ } } - if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: false}) } + if len(pts) > 0 { + results = append(results, PathResult{Points: pts, Closed: false}) + } } return results } diff --git a/pkg/learnnumber/logic/writing.go b/pkg/learnnumber/logic/writing.go index 10fe864..84934d5 100644 --- a/pkg/learnnumber/logic/writing.go +++ b/pkg/learnnumber/logic/writing.go @@ -17,7 +17,7 @@ type WritingRequest struct { } type WritingNode struct { - X, Y, R float64 + X, Y, R float64 AngleA, AngleB float64 BulgeR, Dist float64 Number int @@ -26,11 +26,15 @@ type WritingNode struct { func GenerateWritingPDF(req WritingRequest) ([]byte, error) { pdf := &gopdf.GoPdf{} rect := gopdf.Rect{W: 595.28, H: 841.89} - if req.PaperSize == "Letter" { rect = gopdf.Rect{W: 612, H: 792} } + if req.PaperSize == "Letter" { + rect = gopdf.Rect{W: 612, H: 792} + } pdf.Start(gopdf.Config{PageSize: rect}) err := pdf.AddTTFFontData("basic", data.FontContent) - if err != nil { return nil, err } + if err != nil { + return nil, err + } currentNum := req.StartNum // 只要还没达到 EndNum,就继续生成“整页”内容 @@ -54,7 +58,7 @@ func drawOneFullPage(pdf *gopdf.GoPdf, start int, pW, pH float64) int { nodeR := 28.0 bulgeR := nodeR * 0.32 dist := nodeR + bulgeR + 18.0 - checkR := nodeR * 1.3 + checkR := nodeR * 1.3 var nodes []WritingNode num := start @@ -63,11 +67,13 @@ func drawOneFullPage(pdf *gopdf.GoPdf, start int, pW, pH float64) int { for { placed := false for retry := 0; retry < 1000; retry++ { - rx := xBase + dist + 20 + rand.Float64()*(boxW - 2*dist - 40) - ry := yBase + dist + 20 + rand.Float64()*(boxH - 2*dist - 40) - - if isColliding(rx, ry, checkR, nodes) { continue } - + rx := xBase + dist + 20 + rand.Float64()*(boxW-2*dist-40) + ry := yBase + dist + 20 + rand.Float64()*(boxH-2*dist-40) + + if isColliding(rx, ry, checkR, nodes) { + continue + } + angleA := rand.Float64() * 2 * math.Pi angleB := angleA + math.Pi + (rand.Float64()-0.5)*1.5 nodes = append(nodes, WritingNode{X: rx, Y: ry, R: nodeR, AngleA: angleA, AngleB: angleB, BulgeR: bulgeR, Dist: dist, Number: num}) @@ -98,7 +104,9 @@ func drawOneFullPage(pdf *gopdf.GoPdf, start int, pW, pH float64) int { } func optimizeSpace(nodes []WritingNode, xBase, yBase, boxW, boxH, dist, checkR float64) { - if len(nodes) < 2 { return } + if len(nodes) < 2 { + return + } for iter := 0; iter < 120; iter++ { for i := range nodes { bestX, bestY := nodes[i].X, nodes[i].Y @@ -107,11 +115,16 @@ func optimizeSpace(nodes []WritingNode, xBase, yBase, boxW, boxH, dist, checkR f moveAngle := rand.Float64() * 2 * math.Pi nx := nodes[i].X + math.Cos(moveAngle)*5.0 ny := nodes[i].Y + math.Sin(moveAngle)*5.0 - if nx-dist < xBase || nx+dist > xBase+boxW || ny-dist < yBase || ny+dist > yBase+boxH { continue } - if isColliding(nx, ny, checkR, nodes[:i]) || isColliding(nx, ny, checkR, nodes[i+1:]) { continue } + if nx-dist < xBase || nx+dist > xBase+boxW || ny-dist < yBase || ny+dist > yBase+boxH { + continue + } + if isColliding(nx, ny, checkR, nodes[:i]) || isColliding(nx, ny, checkR, nodes[i+1:]) { + continue + } newMinDist := calcMinDist(nx, ny, i, nodes) if newMinDist > maxMinDist { - maxMinDist = newMinDist; bestX, bestY = nx, ny + maxMinDist = newMinDist + bestX, bestY = nx, ny } } nodes[i].X, nodes[i].Y = bestX, bestY @@ -121,8 +134,12 @@ func optimizeSpace(nodes []WritingNode, xBase, yBase, boxW, boxH, dist, checkR f func isColliding(x, y, r float64, existing []WritingNode) bool { for _, e := range existing { - d := math.Sqrt(math.Pow(x-e.X, 2) + math.Pow(y-e.Y, 2)) - if d < (r + e.R*1.3 + 30.0) { return true } + dx := x - e.X + dy := y - e.Y + d := math.Sqrt(dx*dx + dy*dy) + if d < (r + e.R*1.3 + 30.0) { + return true + } } return false } @@ -130,50 +147,76 @@ func isColliding(x, y, r float64, existing []WritingNode) bool { func calcMinDist(x, y float64, idx int, nodes []WritingNode) float64 { minD := 10000.0 for i, n := range nodes { - if i == idx { continue } - d := math.Sqrt(math.Pow(x-n.X, 2) + math.Pow(y-n.Y, 2)) - if d < minD { minD = d } + if i == idx { + continue + } + dx := x - n.X + dy := y - n.Y + d := math.Sqrt(dx*dx + dy*dy) + if d < minD { + minD = d + } } return minD } func getEdgePoint(x, y, r, angle float64, isCircle bool) (float64, float64) { - gap := 2.0 + gap := 2.0 er := r + gap - if isCircle { return x + math.Cos(angle)*er, y + math.Sin(angle)*er } + if isCircle { + return x + math.Cos(angle)*er, y + math.Sin(angle)*er + } absCos, absSin := math.Abs(math.Cos(angle)), math.Abs(math.Sin(angle)) var d float64 - if absCos > absSin { d = er / absCos } else { d = er / absSin } + if absCos > absSin { + d = er / absCos + } else { + d = er / absSin + } return x + math.Cos(angle)*d, y + math.Sin(angle)*d } func drawCenteredText(pdf *gopdf.GoPdf, cx, cy float64, text string, fontSize float64) { - pdf.SetFont("basic", "", fontSize) + _ = pdf.SetFont("basic", "", fontSize) tw, _ := pdf.MeasureTextWidth(text) - pdf.SetXY(cx - tw/2, cy + fontSize*0.38) - pdf.Text(text) + pdf.SetXY(cx-tw/2, cy+fontSize*0.38) + _ = pdf.Text(text) } func drawFullNode(pdf *gopdf.GoPdf, n WritingNode) { isOdd := n.Number%2 != 0 - pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(1.8) - if isOdd { pdf.Oval(n.X-n.R, n.Y-n.R, n.X+n.R, n.Y+n.R) } else { pdf.RectFromUpperLeft(n.X-n.R, n.Y-n.R, n.R*2, n.R*2) } + pdf.SetStrokeColor(0, 0, 0) + pdf.SetLineWidth(1.8) + if isOdd { + pdf.Oval(n.X-n.R, n.Y-n.R, n.X+n.R, n.Y+n.R) + } else { + pdf.RectFromUpperLeft(n.X-n.R, n.Y-n.R, n.R*2, n.R*2) + } pdf.SetTextColor(220, 220, 220) drawCenteredText(pdf, n.X, n.Y, strconv.Itoa(n.Number), n.R*1.35) - pdf.SetStrokeColor(180, 180, 180); pdf.SetLineWidth(1.0) - ax, ay := n.X + math.Cos(n.AngleA)*n.Dist, n.Y + math.Sin(n.AngleA)*n.Dist + pdf.SetStrokeColor(180, 180, 180) + pdf.SetLineWidth(1.0) + ax, ay := n.X+math.Cos(n.AngleA)*n.Dist, n.Y+math.Sin(n.AngleA)*n.Dist lx1, ly1 := getEdgePoint(n.X, n.Y, n.R, n.AngleA, isOdd) lx2, ly2 := getEdgePoint(ax, ay, n.BulgeR, n.AngleA+math.Pi, !isOdd) pdf.Line(lx1, ly1, lx2, ly2) - if isOdd { pdf.RectFromUpperLeft(ax-n.BulgeR, ay-n.BulgeR, n.BulgeR*2, n.BulgeR*2) } else { pdf.Oval(ax-n.BulgeR, ay-n.BulgeR, ax+n.BulgeR, ay+n.BulgeR) } + if isOdd { + pdf.RectFromUpperLeft(ax-n.BulgeR, ay-n.BulgeR, n.BulgeR*2, n.BulgeR*2) + } else { + pdf.Oval(ax-n.BulgeR, ay-n.BulgeR, ax+n.BulgeR, ay+n.BulgeR) + } - bx, by := n.X + math.Cos(n.AngleB)*n.Dist, n.Y + math.Sin(n.AngleB)*n.Dist + bx, by := n.X+math.Cos(n.AngleB)*n.Dist, n.Y+math.Sin(n.AngleB)*n.Dist lx3, ly3 := getEdgePoint(n.X, n.Y, n.R, n.AngleB, isOdd) lx4, ly4 := getEdgePoint(bx, by, n.BulgeR, n.AngleB+math.Pi, !isOdd) pdf.Line(lx3, ly3, lx4, ly4) - if isOdd { pdf.RectFromUpperLeft(bx-n.BulgeR, by-n.BulgeR, n.BulgeR*2, n.BulgeR*2) } else { pdf.Oval(bx-n.BulgeR, by-n.BulgeR, bx+n.BulgeR, by+n.BulgeR) } - + if isOdd { + pdf.RectFromUpperLeft(bx-n.BulgeR, by-n.BulgeR, n.BulgeR*2, n.BulgeR*2) + } else { + pdf.Oval(bx-n.BulgeR, by-n.BulgeR, bx+n.BulgeR, by+n.BulgeR) + } + pdf.SetTextColor(120, 120, 120) drawCenteredText(pdf, bx, by, strconv.Itoa(n.Number+1), math.Max(7, n.BulgeR*1.2)) } diff --git a/pkg/learnnumber/tool.go b/pkg/learnnumber/tool.go index 3c2506e..d11ecd0 100644 --- a/pkg/learnnumber/tool.go +++ b/pkg/learnnumber/tool.go @@ -16,10 +16,12 @@ func init() { base.Register(&learnNumberTool{}) } -func (t *learnNumberTool) ID() string { return "learn-number" } -func (t *learnNumberTool) Name() string { return "幼儿数学助手" } -func (t *learnNumberTool) Description() string { return "包含数图形、基础加减法等趣味数学练习" } -func (t *learnNumberTool) Emoji() string { return "🔢" } +func (t *learnNumberTool) ID() string { return "learn-number" } +func (t *learnNumberTool) Name() string { return "幼儿数学助手" } +func (t *learnNumberTool) Description() string { + return "包含数图形、基础加减法等趣味数学练习" +} +func (t *learnNumberTool) Emoji() string { return "🔢" } func (t *learnNumberTool) Init() error { fmt.Println("Initializing Learn-Number tool...") diff --git a/pkg/zitie/logic/pdf.go b/pkg/zitie/logic/pdf.go index 91b348a..69e553d 100644 --- a/pkg/zitie/logic/pdf.go +++ b/pkg/zitie/logic/pdf.go @@ -16,14 +16,16 @@ var ( sfntMap = make(map[string]*sfnt.Font) ) -func SetFontData(data []byte) { embeddedFont = data } -func RegisterFontPath(id, path string) { fontMap[id] = path } +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 } + if !ok { + return false + } var buffer sfnt.Buffer idx, err := f.GlyphIndex(&buffer, char) return err == nil && idx != 0 @@ -33,10 +35,14 @@ func HasGlyph(fontId string, char rune) bool { 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} } + if paperSize == "Letter" { + rect = gopdf.Rect{W: 612, H: 792} + } pdf.Start(gopdf.Config{PageSize: rect}) - if len(embeddedFont) > 0 { _ = pdf.AddTTFFontData("font", embeddedFont) } + if len(embeddedFont) > 0 { + _ = pdf.AddTTFFontData("font", embeddedFont) + } // 注册所有可用字体 for id, path := range fontMap { @@ -58,13 +64,17 @@ func GeneratePDFExtended(chars []HanziData, mode, paperSize, fontId string) ([]b } func addTeachingPages(pdf *gopdf.GoPdf, chars []HanziData, flipY bool, pW, pH float64) { - cols := 2; gap, margin := 17.0, 40.0 + 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) } + 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) } @@ -72,58 +82,91 @@ func addTeachingPages(pdf *gopdf.GoPdf, chars []HanziData, flipY bool, pW, pH fl } 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 + 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 + 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) } + 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 + 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) + 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) } + 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) } + 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 + 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 + renderFont = "songti" + isFallback = true } else if HasGlyph("kaiti", charRune) { - renderFont = "kaiti"; isFallback = true + renderFont = "kaiti" + isFallback = true } else { // 全部缺失,保留空白 - rIdx++; continue + rIdx++ + continue } } @@ -133,12 +176,12 @@ func addManuscriptPages(pdf *gopdf.GoPdf, chars []HanziData, pW, pH float64, tar smallSize := fontSize * 0.5 _ = pdf.SetFont(renderFont, "", smallSize) // 计算右上角位置:X靠右,Y靠上 - pdf.SetXY(x + colW - smallSize - 2, y + 2) + 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.SetXY(x+(colW-fontSize*0.9)/2, y+(rowH-fontSize)/2) _ = pdf.Cell(nil, data.Character) } rIdx++ @@ -146,108 +189,221 @@ func addManuscriptPages(pdf *gopdf.GoPdf, chars []HanziData, pW, pH float64, tar } 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) + 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 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) } + 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 } + 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 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) + 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") } + 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) + 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) + 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) } + 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) + 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} } + 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}) + 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++ } - 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 } diff --git a/pkg/zitie/logic/types.go b/pkg/zitie/logic/types.go index a745f71..01ff40b 100644 --- a/pkg/zitie/logic/types.go +++ b/pkg/zitie/logic/types.go @@ -1,9 +1,9 @@ package logic type HanziData struct { - Character string `json:"character"` - Strokes []string `json:"strokes"` - Medians [][]Point `json:"medians"` + Character string `json:"character"` + Strokes []string `json:"strokes"` + Medians [][]Point `json:"medians"` } type Point [2]float64 diff --git a/pkg/zitie/tool.go b/pkg/zitie/tool.go index aa535bc..f68cf6b 100644 --- a/pkg/zitie/tool.go +++ b/pkg/zitie/tool.go @@ -28,10 +28,12 @@ func init() { base.Register(&zitieTool{}) } -func (t *zitieTool) ID() string { return "zitie" } -func (t *zitieTool) Name() string { return "汉字字帖生成" } -func (t *zitieTool) Description() string { return "提供智能缺字处理和古风排版的专业字帖工具" } -func (t *zitieTool) Emoji() string { return "🎨" } +func (t *zitieTool) ID() string { return "zitie" } +func (t *zitieTool) Name() string { return "汉字字帖生成" } +func (t *zitieTool) Description() string { + return "提供智能缺字处理和古风排版的专业字帖工具" +} +func (t *zitieTool) Emoji() string { return "🎨" } func (t *zitieTool) Init() error { fmt.Println("Initializing Zitie tool with font check...") @@ -39,21 +41,23 @@ func (t *zitieTool) Init() error { if err := json.Unmarshal(allDataContent, &t.allChars); err != nil { return fmt.Errorf("failed to unmarshal all.json: %v", err) } - + fontBytes, _ := fontFS.ReadFile("data/font.ttf") logic.SetFontData(fontBytes) fonts := map[string]string{ - "kaiti": "data/kaiti.ttf", - "lishu": "data/lishu.ttf", - "xingshu": "data/xingshu.ttf", - "songti": "data/songti.ttf", + "kaiti": "data/kaiti.ttf", + "lishu": "data/lishu.ttf", + "xingshu": "data/xingshu.ttf", + "songti": "data/songti.ttf", } for id, src := range fonts { bytes, err := fontFS.ReadFile(src) - if err != nil { continue } - + if err != nil { + continue + } + // 1. 同步到磁盘用于 gopdf tmpPath := filepath.Join(os.TempDir(), fmt.Sprintf("own_tools_%s.ttf", id)) _ = os.WriteFile(tmpPath, bytes, 0644) @@ -66,7 +70,7 @@ func (t *zitieTool) Init() error { fmt.Printf("Font [%s] analyzed and registered\n", id) } } - + return nil } @@ -79,7 +83,7 @@ func (t *zitieTool) RegisterRoutes(r *gin.RouterGroup) { type ZitieRequest struct { Chars string `json:"chars" binding:"required"` PaperSize string `json:"paper_size"` - FontType string `json:"font_type"` + FontType string `json:"font_type"` } func (t *zitieTool) handleTeaching(c *gin.Context) { @@ -108,7 +112,9 @@ func (t *zitieTool) handleManuscript(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if req.FontType == "" { req.FontType = "kaiti" } + if req.FontType == "" { + req.FontType = "kaiti" + } data := t.filterChars(req.Chars, true) t.generateAndResponse(c, data, "manuscript", req.PaperSize, req.FontType) } @@ -117,18 +123,29 @@ func (t *zitieTool) filterChars(input string, keepNewline bool) []logic.HanziDat var res []logic.HanziData // 扩充标点符号列表 puncs := ",。!?;:、“”()《》〈〉…·.?!,:;\"'()<> 「」【】『』〔〕" - + for _, r := range input { charStr := string(r) if r == '\n' { - if keepNewline { res = append(res, logic.HanziData{Character: "\n"}) } + if keepNewline { + res = append(res, logic.HanziData{Character: "\n"}) + } continue } - if r == ' ' || r == '\r' || r == '\t' { continue } - + if r == ' ' || r == '\r' || r == '\t' { + continue + } + isPunc := false - for _, p := range puncs { if r == p { isPunc = true; break } } - if isPunc { continue } + for _, p := range puncs { + if r == p { + isPunc = true + break + } + } + if isPunc { + continue + } if hd, ok := t.allChars[charStr]; ok { hd.Character = charStr