diff --git a/go.mod b/go.mod index 864c3a18..8af28bc2 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,8 @@ require ( github.com/hajimehoshi/oto v0.7.1 // indirect github.com/pkg/errors v0.9.1 // indirect golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect - golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect + golang.org/x/image v0.45.0 // indirect golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect ) diff --git a/go.sum b/go.sum index 39d08d1e..7a00420e 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= +golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -51,7 +53,11 @@ golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/cjkfont/cjkfont.go b/internal/cjkfont/cjkfont.go new file mode 100644 index 00000000..9817d152 --- /dev/null +++ b/internal/cjkfont/cjkfont.go @@ -0,0 +1,76 @@ +// Package cjkfont 中文字体渲染:加载系统 CJK 字体(微软雅黑)光栅化文本(UI还原.md §4 中文回退)。 +// +// 素材包位图字体只含 ASCII;中文(如「年糕历险记」)用系统字体渲染, +// 保持像素风(最近邻采样、整数缩放),字形不追求与原版一致。 +package cjkfont + +import ( + "image" + "image/color" + "os" + + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" + "golang.org/x/image/math/fixed" +) + +// Renderer CJK 文本渲染器。 +type Renderer struct { + face font.Face +} + +// Load 加载字体文件(如 C:\Windows\Fonts\msyh.ttc)。 +func Load(path string, sizePx float64) (*Renderer, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + // 兼容 .ttc 字体集合(如 msyh.ttc)与单字体 .ttf/.otf + var f *opentype.Font + if col, err := opentype.ParseCollection(data); err == nil && col != nil && col.NumFonts() > 0 { + f, err = col.Font(0) + if err != nil { + return nil, err + } + } else { + f, err = opentype.Parse(data) + if err != nil { + return nil, err + } + } + face, err := opentype.NewFace(f, &opentype.FaceOptions{ + Size: sizePx, + DPI: 72, + Hinting: font.HintingNone, + }) + if err != nil { + return nil, err + } + return &Renderer{face: face}, nil +} + +// Width 文本像素宽度。 +func (r *Renderer) Width(s string) int { + d := &font.Drawer{Face: r.face} + return d.MeasureString(s).Ceil() +} + +// Draw 渲染文本为 RGBA 图像(含 1px 阴影,UI还原.md §4)。 +func (r *Renderer) Draw(s string, c color.RGBA) image.Image { + w := r.Width(s) + 1 + h := int(r.face.Metrics().Height.Ceil()) + 1 + img := image.NewRGBA(image.Rect(0, 0, w, h)) + d := &font.Drawer{ + Dst: img, + Src: image.NewUniform(c), + Face: r.face, + } + // 阴影 + sh := &font.Drawer{Dst: img, Src: image.NewUniform(color.RGBA{10, 10, 10, 220}), Face: r.face} + sh.Dot = fixed.P(1, int(r.face.Metrics().Ascent.Ceil())+1) + sh.DrawString(s) + // 正文 + d.Dot = fixed.P(0, int(r.face.Metrics().Ascent.Ceil())) + d.DrawString(s) + return img +} diff --git a/internal/cjkfont/cjkfont_test.go b/internal/cjkfont/cjkfont_test.go new file mode 100644 index 00000000..15b5ff57 --- /dev/null +++ b/internal/cjkfont/cjkfont_test.go @@ -0,0 +1,52 @@ +package cjkfont + +import ( + "image/color" + "os" + "path/filepath" + "testing" +) + +// msyhCandidates 微软雅黑常见路径。 +var msyhCandidates = []string{ + filepath.Join(os.Getenv("WINDIR"), "Fonts", "msyh.ttc"), + filepath.Join(os.Getenv("WINDIR"), "Fonts", "msyh.ttf"), + "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", +} + +// TestLoadDraw 加载系统 CJK 字体并渲染中文(无字体时跳过)。 +func TestLoadDraw(t *testing.T) { + path := "" + for _, p := range msyhCandidates { + if _, err := os.Stat(p); err == nil { + path = p + break + } + } + if path == "" { + t.Skip("本机无 CJK 字体,跳过") + } + r, err := Load(path, 16) + if err != nil { + t.Fatalf("加载字体失败: %v", err) + } + img := r.Draw("年糕历险记", color.RGBA{255, 255, 255, 255}) + b := img.Bounds() + if b.Dx() < 16 || b.Dy() < 8 { + t.Fatalf("渲染尺寸异常: %dx%d", b.Dx(), b.Dy()) + } + opaque := 0 + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + if _, _, _, a := img.At(x, y).RGBA(); a > 0 { + opaque++ + } + } + } + if opaque == 0 { + t.Fatal("渲染结果全透明") + } + if w := r.Width("年糕历险记"); w < 16 { + t.Fatalf("宽度异常: %d", w) + } +} diff --git a/tools/zig.zip b/tools/zig.zip index c60ff3e4..0a8cc3f4 100644 Binary files a/tools/zig.zip and b/tools/zig.zip differ