feat(ui): 中文字体渲染(系统 CJK 字体光栅化,微软雅黑验证通过)

This commit is contained in:
NianGao Dev
2026-08-15 23:22:40 +08:00
parent 236f7a4b06
commit 093b3df8ec
5 changed files with 137 additions and 2 deletions

5
go.mod
View File

@@ -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
)

6
go.sum
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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)
}
}

Binary file not shown.