211 lines
4.7 KiB
Go
211 lines
4.7 KiB
Go
package vision
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
|
|
"apigo.cc/go/file"
|
|
"github.com/flopp/go-findfont"
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/font/opentype"
|
|
"golang.org/x/image/font/sfnt"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
var (
|
|
fontCache = make(map[string]*sfnt.Font)
|
|
fontLock sync.RWMutex
|
|
loaded = make(map[string]bool)
|
|
)
|
|
|
|
// 各操作系统默认字体文件列表
|
|
var defaultFontFiles = map[string]map[string][]string{
|
|
"windows": {
|
|
"serif": {"simsun.ttc", "times.ttf"},
|
|
"sans-serif": {"msyh.ttc", "arial.ttf"},
|
|
"monospace": {"consola.ttf", "simsun.ttc"},
|
|
},
|
|
"darwin": {
|
|
"serif": {"Songti.ttc", "Times New Roman.ttf"},
|
|
"sans-serif": {"Hiragino Sans GB.ttc", "PingFang.ttc", "Helvetica.ttf"},
|
|
"monospace": {"Menlo.ttc", "Courier New.ttf", "Hiragino Sans GB.ttc"},
|
|
},
|
|
"linux": {
|
|
"serif": {"dejavu/DejaVuSerif.ttf", "wqy-microhei.ttc", "noto/NotoSerifCJK-Regular.ttc"},
|
|
"sans-serif": {"dejavu/DejaVuSans.ttf", "wqy-microhei.ttc", "noto/NotoSansCJK-Regular.ttc"},
|
|
"monospace": {"dejavu/DejaVuSansMono.ttf", "wqy-microhei_mono.ttc", "droid/DroidSansMono.ttf"},
|
|
},
|
|
}
|
|
|
|
// LoadFonts 加载指定路径的字体文件
|
|
func LoadFonts(paths ...string) {
|
|
if len(paths) == 0 {
|
|
// 加载系统默认字体
|
|
if ffs, ok := defaultFontFiles[runtime.GOOS]; ok {
|
|
for _, list := range ffs {
|
|
paths = append(paths, list...)
|
|
}
|
|
}
|
|
}
|
|
|
|
buf := &sfnt.Buffer{}
|
|
for _, p := range paths {
|
|
fontLock.RLock()
|
|
isLoaded := loaded[p]
|
|
fontLock.RUnlock()
|
|
if isLoaded {
|
|
continue
|
|
}
|
|
|
|
fullPath := p
|
|
if !filepath.IsAbs(p) {
|
|
if f, err := findfont.Find(p); err == nil {
|
|
fullPath = f
|
|
}
|
|
}
|
|
|
|
data, err := file.ReadBytes(fullPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
fontLock.Lock()
|
|
if strings.HasSuffix(strings.ToLower(fullPath), ".ttc") {
|
|
if collection, err := sfnt.ParseCollection(data); err == nil {
|
|
for i := 0; i < collection.NumFonts(); i++ {
|
|
if f, err := collection.Font(i); err == nil {
|
|
cacheFont(buf, f)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if f, err := sfnt.Parse(data); err == nil {
|
|
cacheFont(buf, f)
|
|
}
|
|
}
|
|
loaded[p] = true
|
|
fontLock.Unlock()
|
|
}
|
|
}
|
|
|
|
func cacheFont(buf *sfnt.Buffer, f *sfnt.Font) {
|
|
names := []sfnt.NameID{sfnt.NameIDFull, sfnt.NameIDFamily, sfnt.NameIDTypographicFamily}
|
|
for _, id := range names {
|
|
if name, err := f.Name(buf, id); err == nil && name != "" {
|
|
fontCache[strings.TrimSpace(name)] = f
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetFont 设置画布字体
|
|
func (c *Canvas) SetFont(size float64, names ...string) error {
|
|
if len(names) == 0 {
|
|
LoadFonts()
|
|
}
|
|
|
|
cf := &CompositeFace{Faces: make([]font.Face, 0), Names: names}
|
|
fontLock.RLock()
|
|
for _, name := range names {
|
|
if f, ok := fontCache[name]; ok {
|
|
if face, err := opentype.NewFace(f, &opentype.FaceOptions{Size: size, DPI: 72}); err == nil {
|
|
cf.Faces = append(cf.Faces, face)
|
|
}
|
|
}
|
|
}
|
|
fontLock.RUnlock()
|
|
|
|
if len(cf.Faces) > 0 {
|
|
c.lastFont = cf
|
|
c.dc.SetFontFace(cf)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("no font found for: %v", names)
|
|
}
|
|
|
|
// TextOption 定义文本绘制选项
|
|
type TextOption struct {
|
|
Width float64
|
|
Height float64
|
|
LineHeight float64
|
|
Align string // left, center, right
|
|
VAlign string // top, middle, bottom
|
|
Color string
|
|
BgColor string
|
|
BorderColor string
|
|
BorderWidth float64
|
|
Padding [4]float64 // top, right, bottom, left
|
|
}
|
|
|
|
// DrawText 在画布上绘制文本
|
|
func (c *Canvas) DrawText(x, y float64, text string, opt *TextOption) {
|
|
if opt == nil {
|
|
opt = &TextOption{}
|
|
}
|
|
if opt.Color != "" {
|
|
c.dc.SetColor(ParseColor(opt.Color))
|
|
}
|
|
|
|
// 基础绘制逻辑
|
|
c.dc.DrawStringAnchored(text, x, y, 0, 0)
|
|
}
|
|
|
|
// CompositeFace 支持多字体回退的字体接口实现
|
|
type CompositeFace struct {
|
|
Names []string
|
|
Faces []font.Face
|
|
}
|
|
|
|
func (c *CompositeFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
|
|
for _, f := range c.Faces {
|
|
if dr, mask, maskp, advance, ok = f.Glyph(dot, r); ok {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *CompositeFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
|
|
for _, f := range c.Faces {
|
|
if bounds, advance, ok = f.GlyphBounds(r); ok {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *CompositeFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
|
|
for _, f := range c.Faces {
|
|
if advance, ok = f.GlyphAdvance(r); ok {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (c *CompositeFace) Kern(r0, r1 rune) fixed.Int26_6 {
|
|
for _, f := range c.Faces {
|
|
if k := f.Kern(r0, r1); k != 0 {
|
|
return k
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (c *CompositeFace) Metrics() font.Metrics {
|
|
if len(c.Faces) == 0 {
|
|
return font.Metrics{}
|
|
}
|
|
return c.Faces[0].Metrics()
|
|
}
|
|
|
|
func (c *CompositeFace) Close() error {
|
|
for _, f := range c.Faces {
|
|
f.Close()
|
|
}
|
|
return nil
|
|
}
|