Initial commit: Modular personal toolbox with high-fidelity Chinese stroke order tool and CI/CD
Build and Push Docker Image / build (push) Successful in 2m45s
Build and Push Docker Image / build (push) Successful in 2m45s
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Tool 定义了工具箱中每个子工具必须实现的接口
|
||||
type Tool interface {
|
||||
ID() string // 工具的唯一标识,用于路由前缀,如 "zitie"
|
||||
Name() string // 工具的显示名称
|
||||
Description() string // 工具的描述
|
||||
Init() error // 初始化逻辑,如加载 embed 的数据
|
||||
RegisterRoutes(r *gin.RouterGroup) // 注册该工具的 API 路由
|
||||
}
|
||||
|
||||
// Registry 存储所有已注册的工具
|
||||
var Registry = make(map[string]Tool)
|
||||
|
||||
// Register 用于工具在 init() 函数中注册自己
|
||||
func Register(t Tool) {
|
||||
Registry[t.ID()] = t
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# 汉字字帖生成器 (Zitie Tool)
|
||||
|
||||
这是 `Own-Tools` 的核心插件之一,专注于生成高质量的书法练习帖。
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
### 1. 2x3 教学方格
|
||||
* **特性**:每页 6 个字,每个汉字包含笔顺序号、行进箭头和红色骨架线。
|
||||
* **特色**:序号采用“圆圈相切”定位算法,美观且精准。
|
||||
* **用途**:适合硬笔书法初学者学习笔画顺序。
|
||||
|
||||
### 2. 步进式笔顺分解
|
||||
* **特性**:9列无缝米字格排版。首格为黑体全字,后续格子展示逐笔增加的灰色过程。
|
||||
* **特色**:智能换行逻辑(第二行首格留空),保持视觉连贯。
|
||||
* **用途**:详细解析汉字的间架结构。
|
||||
|
||||
### 3. 古风竖排信纸
|
||||
* **特性**:模拟传统“乌丝栏”笺纸,从右向左、从上到下竖向排版。
|
||||
* **特色**:
|
||||
* 内嵌 4 款高品质书法字体(楷体、行草、隶变、书宋)。
|
||||
* **智能缺字降级**:当选择字体缺字时,自动切换至宋体/楷体并以 1/2 大小在右上角标注。
|
||||
* 支持换行符 `
|
||||
` 实现自由分列。
|
||||
* **用途**:生成极具艺术感的诗词临摹帖。
|
||||
|
||||
## 🎨 技术细节
|
||||
* **PDF 生成**:基于 `gopdf` 的纯矢量绘图,打印效果极其锐利。
|
||||
* **字体处理**:采用“启动同步至磁盘”技术,绕过了内存加载大型 CJK 字体时的兼容性 Bug。
|
||||
* **坐标计算**:所有元素均基于格子大小动态缩放,完美适配 A4 和 Letter 纸张。
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,253 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package logic
|
||||
|
||||
type HanziData struct {
|
||||
Character string `json:"character"`
|
||||
Strokes []string `json:"strokes"`
|
||||
Medians [][]Point `json:"medians"`
|
||||
}
|
||||
|
||||
type Point [2]float64
|
||||
@@ -0,0 +1,153 @@
|
||||
package zitie
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"toolbox/pkg/base"
|
||||
"toolbox/pkg/zitie/logic"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/image/font/sfnt"
|
||||
)
|
||||
|
||||
//go:embed data/all.json
|
||||
var allDataContent []byte
|
||||
|
||||
//go:embed data/*.ttf
|
||||
var fontFS embed.FS
|
||||
|
||||
type zitieTool struct {
|
||||
allChars map[string]logic.HanziData
|
||||
}
|
||||
|
||||
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) Init() error {
|
||||
fmt.Println("Initializing Zitie tool with font check...")
|
||||
t.allChars = make(map[string]logic.HanziData)
|
||||
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",
|
||||
}
|
||||
|
||||
for id, src := range fonts {
|
||||
bytes, err := fontFS.ReadFile(src)
|
||||
if err != nil { continue }
|
||||
|
||||
// 1. 同步到磁盘用于 gopdf
|
||||
tmpPath := filepath.Join(os.TempDir(), fmt.Sprintf("own_tools_%s.ttf", id))
|
||||
_ = os.WriteFile(tmpPath, bytes, 0644)
|
||||
logic.RegisterFontPath(id, tmpPath)
|
||||
|
||||
// 2. 解析 Cmap 用于缺字检查
|
||||
f, err := sfnt.Parse(bytes)
|
||||
if err == nil {
|
||||
logic.RegisterFontSFNT(id, f)
|
||||
fmt.Printf("Font [%s] analyzed and registered\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *zitieTool) RegisterRoutes(r *gin.RouterGroup) {
|
||||
r.POST("/teaching", t.handleTeaching)
|
||||
r.POST("/step", t.handleStep)
|
||||
r.POST("/manuscript", t.handleManuscript)
|
||||
}
|
||||
|
||||
type ZitieRequest struct {
|
||||
Chars string `json:"chars" binding:"required"`
|
||||
PaperSize string `json:"paper_size"`
|
||||
FontType string `json:"font_type"`
|
||||
}
|
||||
|
||||
func (t *zitieTool) handleTeaching(c *gin.Context) {
|
||||
var req ZitieRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
data := t.filterChars(req.Chars, false)
|
||||
t.generateAndResponse(c, data, "teaching", req.PaperSize, "kaiti")
|
||||
}
|
||||
|
||||
func (t *zitieTool) handleStep(c *gin.Context) {
|
||||
var req ZitieRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
data := t.filterChars(req.Chars, false)
|
||||
t.generateAndResponse(c, data, "step", req.PaperSize, "kaiti")
|
||||
}
|
||||
|
||||
func (t *zitieTool) handleManuscript(c *gin.Context) {
|
||||
var req ZitieRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.FontType == "" { req.FontType = "kaiti" }
|
||||
data := t.filterChars(req.Chars, true)
|
||||
t.generateAndResponse(c, data, "manuscript", req.PaperSize, req.FontType)
|
||||
}
|
||||
|
||||
func (t *zitieTool) filterChars(input string, keepNewline bool) []logic.HanziData {
|
||||
var res []logic.HanziData
|
||||
// 扩充标点符号列表
|
||||
puncs := ",。!?;:、“”()《》〈〉…·.?!,:;\"'()<> 「」【】『』〔〕"
|
||||
|
||||
for _, r := range input {
|
||||
charStr := string(r)
|
||||
if r == '\n' {
|
||||
if keepNewline { res = append(res, logic.HanziData{Character: "\n"}) }
|
||||
continue
|
||||
}
|
||||
if r == ' ' || r == '\r' || r == '\t' { continue }
|
||||
|
||||
isPunc := false
|
||||
for _, p := range puncs { if r == p { isPunc = true; break } }
|
||||
if isPunc { continue }
|
||||
|
||||
if hd, ok := t.allChars[charStr]; ok {
|
||||
hd.Character = charStr
|
||||
res = append(res, hd)
|
||||
} else {
|
||||
res = append(res, logic.HanziData{Character: charStr})
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (t *zitieTool) generateAndResponse(c *gin.Context, data []logic.HanziData, mode, paper, font string) {
|
||||
if len(data) == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "no valid characters found"})
|
||||
return
|
||||
}
|
||||
pdfBytes, err := logic.GeneratePDFExtended(data, mode, paper, font)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "application/pdf", pdfBytes)
|
||||
}
|
||||
Reference in New Issue
Block a user