732 lines
18 KiB
Go
732 lines
18 KiB
Go
![]() |
package img
|
|||
|
|
|||
|
import (
|
|||
|
"image"
|
|||
|
"image/color"
|
|||
|
"math"
|
|||
|
"math/rand"
|
|||
|
"path/filepath"
|
|||
|
"runtime"
|
|||
|
"strings"
|
|||
|
"sync"
|
|||
|
|
|||
|
"github.com/flopp/go-findfont"
|
|||
|
"github.com/ssgo/u"
|
|||
|
"golang.org/x/image/font"
|
|||
|
"golang.org/x/image/font/opentype"
|
|||
|
"golang.org/x/image/font/sfnt"
|
|||
|
"golang.org/x/image/math/fixed"
|
|||
|
)
|
|||
|
|
|||
|
// 各操作系统默认字体文件列表
|
|||
|
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"},
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
var defaultFontNames = map[string]map[string][]string{
|
|||
|
"windows": {
|
|||
|
"serif": {"SimSun", "Times New Roman"},
|
|||
|
"sans-serif": {"Microsoft YaHei", "Arial"},
|
|||
|
"monospace": {"Consolas", "SimSun"},
|
|||
|
},
|
|||
|
"darwin": {
|
|||
|
"serif": {"Songti SC", "Times New Roman"},
|
|||
|
"sans-serif": {"Hiragino Sans GB", "PingFang SC", "Helvetica"},
|
|||
|
"monospace": {"Menlo", "Courier New", "Hiragino Sans GB"},
|
|||
|
},
|
|||
|
"linux": {
|
|||
|
"serif": {"DejaVu Serif", "WenQuanYi Micro Hei", "Noto Serif CJK SC"},
|
|||
|
"sans-serif": {"DejaVu Sans", "WenQuanYi Micro Hei", "Noto Sans CJK SC"},
|
|||
|
"monospace": {"DejaVu Sans Mono", "WenQuanYi Micro Hei Mono", "Droid Sans Mono"},
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
var defaultFontLoaded = map[string]bool{}
|
|||
|
var fontLoaded = map[string]bool{}
|
|||
|
var fontCache = map[string]*sfnt.Font{}
|
|||
|
var fontLock = sync.RWMutex{}
|
|||
|
|
|||
|
type CompositeFace struct {
|
|||
|
Names []string
|
|||
|
Faces []font.Face
|
|||
|
}
|
|||
|
|
|||
|
// 获取字体 - 支持TTF, OTF, TTC格式
|
|||
|
func saveFontCache(buf *sfnt.Buffer, f *sfnt.Font) {
|
|||
|
fullName, _ := f.Name(buf, sfnt.NameIDFull)
|
|||
|
enFamily, _ := f.Name(buf, sfnt.NameIDFamily)
|
|||
|
enStyle, _ := f.Name(buf, sfnt.NameIDSubfamily)
|
|||
|
cnFamily, _ := f.Name(buf, sfnt.NameIDTypographicFamily)
|
|||
|
cnStyle, _ := f.Name(buf, sfnt.NameIDTypographicSubfamily)
|
|||
|
fullName = strings.TrimSpace(fullName)
|
|||
|
enFamily = strings.TrimSpace(enFamily)
|
|||
|
cnFamily = strings.TrimSpace(cnFamily)
|
|||
|
enStyle = strings.TrimSpace(enStyle)
|
|||
|
cnStyle = strings.TrimSpace(cnStyle)
|
|||
|
enName := enFamily + " " + enStyle
|
|||
|
cnName := cnFamily + " " + cnStyle
|
|||
|
enName = strings.TrimSpace(enName)
|
|||
|
cnName = strings.TrimSpace(cnName)
|
|||
|
|
|||
|
fontLock.Lock()
|
|||
|
if enName != "" && fontCache[enName] == nil {
|
|||
|
fontCache[enName] = f
|
|||
|
}
|
|||
|
if cnName != "" && fontCache[cnName] == nil {
|
|||
|
fontCache[cnName] = f
|
|||
|
}
|
|||
|
if fullName != "" && fullName != enName && fontCache[fullName] == nil {
|
|||
|
fontCache[fullName] = f
|
|||
|
}
|
|||
|
if enFamily != "" && fontCache[enFamily] == nil {
|
|||
|
fontCache[enFamily] = f
|
|||
|
}
|
|||
|
if cnFamily != "" && fontCache[cnFamily] == nil {
|
|||
|
fontCache[cnFamily] = f
|
|||
|
}
|
|||
|
fontLock.Unlock()
|
|||
|
}
|
|||
|
|
|||
|
func ListFont() []string {
|
|||
|
fonts := []string{}
|
|||
|
fontLock.RLock()
|
|||
|
defer fontLock.RUnlock()
|
|||
|
for k := range fontCache {
|
|||
|
fonts = append(fonts, k)
|
|||
|
}
|
|||
|
return fonts
|
|||
|
}
|
|||
|
|
|||
|
var hasFontLoaded = false
|
|||
|
|
|||
|
func LoadFont(fontFiles ...string) {
|
|||
|
fontLock.Lock()
|
|||
|
if !hasFontLoaded {
|
|||
|
hasFontLoaded = true
|
|||
|
}
|
|||
|
fontLock.Unlock()
|
|||
|
if len(fontFiles) == 0 {
|
|||
|
// fontFiles = findfont.List()
|
|||
|
if osFFs, ok := defaultFontFiles[runtime.GOOS]; ok {
|
|||
|
if ffs, ok := osFFs["serif"]; ok {
|
|||
|
fontFiles = append(fontFiles, ffs...)
|
|||
|
}
|
|||
|
if ffs, ok := osFFs["sans-serif"]; ok {
|
|||
|
fontFiles = append(fontFiles, ffs...)
|
|||
|
}
|
|||
|
if ffs, ok := osFFs["monospace"]; ok {
|
|||
|
fontFiles = append(fontFiles, ffs...)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
buf := &sfnt.Buffer{}
|
|||
|
for _, fontFile := range fontFiles {
|
|||
|
fontLock.RLock()
|
|||
|
loaded := fontLoaded[fontFile]
|
|||
|
fontLock.RUnlock()
|
|||
|
if loaded {
|
|||
|
continue
|
|||
|
}
|
|||
|
if !filepath.IsAbs(fontFile) {
|
|||
|
if f, err := findfont.Find(fontFile); err == nil {
|
|||
|
fontFile = f
|
|||
|
}
|
|||
|
}
|
|||
|
if fontData, err := u.ReadFileBytes(fontFile); err == nil {
|
|||
|
if strings.HasSuffix(fontFile, ".ttc") {
|
|||
|
if c, err := sfnt.ParseCollection(fontData); err == nil {
|
|||
|
for i := c.NumFonts() - 1; i >= 0; i-- {
|
|||
|
if f, err := c.Font(i); err == nil {
|
|||
|
saveFontCache(buf, f)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
if f, err := sfnt.Parse(fontData); err == nil {
|
|||
|
saveFontCache(buf, f)
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
fontLock.Lock()
|
|||
|
fontLoaded[fontFile] = true
|
|||
|
fontLock.Unlock()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 获取字体 - 支持TTF, OTF, TTC格式
|
|||
|
func makeFont(size float64, fontNames ...string) *CompositeFace {
|
|||
|
fontLock.RLock()
|
|||
|
hasFontLoaded1 := hasFontLoaded
|
|||
|
fontLock.RUnlock()
|
|||
|
|
|||
|
if !hasFontLoaded1 {
|
|||
|
LoadFont()
|
|||
|
}
|
|||
|
|
|||
|
cf := &CompositeFace{Faces: []font.Face{}, Names: fontNames}
|
|||
|
for _, name := range fontNames {
|
|||
|
fontLock.RLock()
|
|||
|
f := fontCache[name]
|
|||
|
fontLock.RUnlock()
|
|||
|
if f != nil {
|
|||
|
if fc, err := opentype.NewFace(f, &opentype.FaceOptions{Size: size, DPI: 72}); err == nil {
|
|||
|
cf.Faces = append(cf.Faces, fc)
|
|||
|
// fmt.Println(u.BGreen("font"), name)
|
|||
|
// return fc
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return cf
|
|||
|
// // fmt.Println(u.BRed("no font"))
|
|||
|
// return nil
|
|||
|
}
|
|||
|
|
|||
|
func loadDefaultFonts(typ string) {
|
|||
|
fontLock.RLock()
|
|||
|
loaded := defaultFontLoaded[runtime.GOOS+typ]
|
|||
|
fontLock.RUnlock()
|
|||
|
if loaded {
|
|||
|
return
|
|||
|
}
|
|||
|
if osFFs, ok := defaultFontFiles[runtime.GOOS]; ok {
|
|||
|
if ffs, ok := osFFs[typ]; ok {
|
|||
|
LoadFont(ffs...)
|
|||
|
}
|
|||
|
}
|
|||
|
fontLock.Lock()
|
|||
|
defaultFontLoaded[runtime.GOOS+typ] = true
|
|||
|
fontLock.Unlock()
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
func getDefaultFonts(typ string) []string {
|
|||
|
if osFNs, ok := defaultFontNames[runtime.GOOS]; ok {
|
|||
|
if fns, ok := osFNs[typ]; ok {
|
|||
|
return fns
|
|||
|
}
|
|||
|
}
|
|||
|
return []string{}
|
|||
|
}
|
|||
|
|
|||
|
// 设置预设字体
|
|||
|
func (g *Graphics) SetSerifFont(size float64) {
|
|||
|
loadDefaultFonts("serif")
|
|||
|
g.SetFont(size, getDefaultFonts("serif")...)
|
|||
|
}
|
|||
|
func (g *Graphics) SetSansSerifFont(size float64) {
|
|||
|
loadDefaultFonts("sans-serif")
|
|||
|
g.SetFont(size, getDefaultFonts("sans-serif")...)
|
|||
|
}
|
|||
|
func (g *Graphics) SetMonospaceFont(size float64) {
|
|||
|
loadDefaultFonts("monospace")
|
|||
|
g.SetFont(size, getDefaultFonts("monospace")...)
|
|||
|
}
|
|||
|
|
|||
|
func (g *Graphics) SetFont(size float64, fontNames ...string) {
|
|||
|
if len(fontNames) == 0 && g.lastFont != nil {
|
|||
|
if ff, ok := g.lastFont.(*CompositeFace); ok {
|
|||
|
fontNames = ff.Names
|
|||
|
}
|
|||
|
}
|
|||
|
g.lastFont = makeFont(size, fontNames...)
|
|||
|
g.dc.SetFontFace(g.lastFont)
|
|||
|
}
|
|||
|
|
|||
|
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) {
|
|||
|
|
|||
|
// fmt.Println(u.BCyan("Glyph"), len(c.Faces))
|
|||
|
for _, face := range c.Faces {
|
|||
|
dr, mask, maskp, advance, ok = face.Glyph(dot, r)
|
|||
|
// fmt.Println(u.BCyan("Glyph >>> "), i, ok)
|
|||
|
if ok {
|
|||
|
return
|
|||
|
}
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
func (c *CompositeFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
|
|||
|
for _, face := range c.Faces {
|
|||
|
bounds, advance, ok = face.GlyphBounds(r)
|
|||
|
if ok {
|
|||
|
return
|
|||
|
}
|
|||
|
}
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
func (c *CompositeFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
|
|||
|
for _, face := range c.Faces {
|
|||
|
advance, ok := face.GlyphAdvance(r)
|
|||
|
if ok {
|
|||
|
// fmt.Printf("字符 '%c' 使用字体%d 宽度=%d\n", r, i, advance)
|
|||
|
return advance, true
|
|||
|
}
|
|||
|
}
|
|||
|
// 找不到支持的字体
|
|||
|
return fixed.I(1000), true // 返回默认宽度防止错误
|
|||
|
}
|
|||
|
|
|||
|
func (c *CompositeFace) Kern(r0, r1 rune) fixed.Int26_6 {
|
|||
|
for _, face := range c.Faces {
|
|||
|
if kern := face.Kern(r0, r1); kern != 0 {
|
|||
|
return kern
|
|||
|
}
|
|||
|
}
|
|||
|
return 0
|
|||
|
}
|
|||
|
|
|||
|
func (c *CompositeFace) Metrics() font.Metrics {
|
|||
|
if len(c.Faces) == 0 {
|
|||
|
return font.Metrics{}
|
|||
|
}
|
|||
|
return c.Faces[0].Metrics()
|
|||
|
}
|
|||
|
|
|||
|
// 获取实际字符渲染用的字体的 Metrics
|
|||
|
// func (c *CompositeFace) ActualMetrics(r rune) font.Metrics {
|
|||
|
// // 查找实际渲染该字符的字体
|
|||
|
// for _, f := range c.Faces {
|
|||
|
// if _, _, ok := f.GlyphBounds(r); ok {
|
|||
|
// return f.Metrics()
|
|||
|
// }
|
|||
|
// }
|
|||
|
// // 找不到支持字符的字体时返回第一个的 Metrics
|
|||
|
// return c.Faces[0].Metrics()
|
|||
|
// }
|
|||
|
|
|||
|
func (c *CompositeFace) Close() error {
|
|||
|
var err error
|
|||
|
// for _, face := range c.Faces {
|
|||
|
// if closer, ok := face.(io.Closer); ok {
|
|||
|
// if e := closer.Close(); e != nil {
|
|||
|
// err = e
|
|||
|
// }
|
|||
|
// }
|
|||
|
// }
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
type TextOption struct {
|
|||
|
Width float64
|
|||
|
Height float64
|
|||
|
LineHeight float64
|
|||
|
PaddingX float64
|
|||
|
PaddingY float64
|
|||
|
NoWrap bool
|
|||
|
Color string
|
|||
|
BgColor string
|
|||
|
BorderColor string
|
|||
|
BorderWidth float64
|
|||
|
BorderRadius float64
|
|||
|
Align string // left center right
|
|||
|
VAlign string // top middle bottom
|
|||
|
ShadowColor string // default #333333
|
|||
|
ShadowOffset int
|
|||
|
}
|
|||
|
|
|||
|
// 绘制文本
|
|||
|
func (g *Graphics) Text(x, y float64, text string, opt *TextOption) {
|
|||
|
if opt == nil {
|
|||
|
opt = &TextOption{}
|
|||
|
}
|
|||
|
if opt.LineHeight <= 0.01 {
|
|||
|
opt.LineHeight = 1.0
|
|||
|
}
|
|||
|
|
|||
|
// 设置文本颜色
|
|||
|
if opt.Color != "" {
|
|||
|
g.dc.SetColor(hexToRGBA(opt.Color))
|
|||
|
}
|
|||
|
|
|||
|
if g.lastFont == nil {
|
|||
|
g.SetSansSerifFont(14)
|
|||
|
}
|
|||
|
|
|||
|
// 处理换行符
|
|||
|
paragraphs := strings.Split(text, "\n")
|
|||
|
if strings.Contains(text, "\r\n") {
|
|||
|
paragraphs = strings.Split(text, "\r\n")
|
|||
|
}
|
|||
|
|
|||
|
// 准备文本行
|
|||
|
var lines []string
|
|||
|
hasWrap := !opt.NoWrap && opt.Width > 0
|
|||
|
for _, para := range paragraphs {
|
|||
|
if para == "" {
|
|||
|
lines = append(lines, "") // 保留空行
|
|||
|
} else if hasWrap {
|
|||
|
// 使用drawWidth计算换行
|
|||
|
lines = append(lines, g.wrapParagraph(para, opt.Width-2*opt.PaddingX)...)
|
|||
|
} else if opt.Width > 0 {
|
|||
|
// NoWrap模式但指定了宽度:使用相同的换行逻辑但只取第一行
|
|||
|
wrappedLines := g.wrapParagraph(para, opt.Width-2*opt.PaddingX)
|
|||
|
if len(wrappedLines) > 0 {
|
|||
|
lines = append(lines, wrappedLines[0])
|
|||
|
} else {
|
|||
|
lines = append(lines, "")
|
|||
|
}
|
|||
|
} else {
|
|||
|
lines = append(lines, para)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 计算文本总高度
|
|||
|
lineHeight := g.calcLineHeight(opt.LineHeight)
|
|||
|
totalHeight := float64(len(lines)) * lineHeight
|
|||
|
|
|||
|
// 计算字体的上升和下降部分
|
|||
|
ascent := 0.0
|
|||
|
descent := 0.0
|
|||
|
if g.lastFont != nil {
|
|||
|
metrics := g.lastFont.Metrics()
|
|||
|
ascent = float64(metrics.Ascent) / 64
|
|||
|
descent = float64(metrics.Descent) / 64
|
|||
|
} else {
|
|||
|
// 默认估算
|
|||
|
ascent = g.dc.FontHeight() * 0.7
|
|||
|
descent = g.dc.FontHeight() * 0.3
|
|||
|
}
|
|||
|
|
|||
|
// 计算文本内容尺寸
|
|||
|
textWidth := opt.Width
|
|||
|
textHeight := opt.Height
|
|||
|
|
|||
|
// 计算最大行宽
|
|||
|
maxWidth := 0.0
|
|||
|
for _, line := range lines {
|
|||
|
w, _ := g.dc.MeasureString(line)
|
|||
|
if w > maxWidth {
|
|||
|
maxWidth = w
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 如果未指定宽度,计算所需宽度
|
|||
|
if textWidth == 0 {
|
|||
|
textWidth = maxWidth + 2*opt.PaddingX
|
|||
|
}
|
|||
|
|
|||
|
// 如果未指定高度,计算所需高度
|
|||
|
if textHeight == 0 {
|
|||
|
textHeight = totalHeight + 2*opt.PaddingY
|
|||
|
}
|
|||
|
|
|||
|
var halfOffset float64
|
|||
|
if opt.ShadowOffset != 0 {
|
|||
|
if opt.ShadowColor == "" {
|
|||
|
opt.ShadowColor = "#333333"
|
|||
|
}
|
|||
|
halfOffset = float64(opt.ShadowOffset) / 2
|
|||
|
}
|
|||
|
|
|||
|
// 绘制背景和边框(在文本之前)
|
|||
|
if opt.BgColor != "" || opt.BorderColor != "" {
|
|||
|
|
|||
|
if opt.ShadowOffset != 0 {
|
|||
|
g.dc.SetColor(hexToRGBA(opt.ShadowColor))
|
|||
|
if opt.BorderRadius > 0 {
|
|||
|
g.dc.DrawRoundedRectangle(x+halfOffset*2, y+halfOffset*2, textWidth, textHeight, opt.BorderRadius)
|
|||
|
} else {
|
|||
|
g.dc.DrawRectangle(x+halfOffset*2, y+halfOffset*2, textWidth, textHeight)
|
|||
|
}
|
|||
|
if opt.BgColor != "" {
|
|||
|
g.dc.Fill()
|
|||
|
} else {
|
|||
|
g.dc.Stroke()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 绘制背景
|
|||
|
if opt.BgColor != "" {
|
|||
|
g.dc.SetColor(hexToRGBA(opt.BgColor))
|
|||
|
if opt.BorderRadius > 0 {
|
|||
|
g.dc.DrawRoundedRectangle(x, y, textWidth, textHeight, opt.BorderRadius)
|
|||
|
} else {
|
|||
|
g.dc.DrawRectangle(x, y, textWidth, textHeight)
|
|||
|
}
|
|||
|
g.dc.Fill()
|
|||
|
}
|
|||
|
|
|||
|
// 绘制边框
|
|||
|
if opt.BorderWidth < 0.01 {
|
|||
|
opt.BorderWidth = 1
|
|||
|
}
|
|||
|
if opt.BorderColor != "" {
|
|||
|
g.dc.SetColor(hexToRGBA(opt.BorderColor))
|
|||
|
g.dc.SetLineWidth(opt.BorderWidth)
|
|||
|
if opt.BorderRadius > 0 {
|
|||
|
g.dc.DrawRoundedRectangle(x, y, textWidth, textHeight, opt.BorderRadius)
|
|||
|
} else {
|
|||
|
g.dc.DrawRectangle(x, y, textWidth, textHeight)
|
|||
|
}
|
|||
|
g.dc.Stroke()
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 计算实际内容和绘制区域
|
|||
|
contentX := x + opt.PaddingX
|
|||
|
contentY := y + opt.PaddingY
|
|||
|
drawWidth := textWidth - 2*opt.PaddingX
|
|||
|
drawHeight := textHeight - 2*opt.PaddingY
|
|||
|
|
|||
|
// 修正垂直对齐计算
|
|||
|
// startY := contentY + ascent + descent // 基线位置
|
|||
|
startY := contentY + ascent + (lineHeight-ascent-descent)/2 // 基线位置
|
|||
|
if drawHeight > 0 {
|
|||
|
// 计算文本块的实际高度
|
|||
|
textBlockHeight := totalHeight + descent
|
|||
|
|
|||
|
switch opt.VAlign {
|
|||
|
case "top":
|
|||
|
// 顶部对齐:从顶部开始绘制
|
|||
|
case "middle":
|
|||
|
// 中间对齐:居中位置减去文本块高度的一半
|
|||
|
startY += (drawHeight - textBlockHeight) / 2
|
|||
|
case "bottom":
|
|||
|
// 底部对齐:从底部减去文本块高度
|
|||
|
startY += drawHeight - textBlockHeight
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 创建阴影效果(如果有)
|
|||
|
if opt.ShadowOffset != 0 {
|
|||
|
g.dc.SetColor(hexToRGBA(opt.ShadowColor))
|
|||
|
for i, line := range lines {
|
|||
|
// 计算行位置
|
|||
|
lineY := startY + float64(i)*lineHeight
|
|||
|
// 检查行是否在可见区域内
|
|||
|
if drawHeight > 0 && (lineY < contentY || lineY > contentY+drawHeight) {
|
|||
|
continue
|
|||
|
}
|
|||
|
lineX := g.calcLineX(contentX, drawWidth, line, opt.Align)
|
|||
|
g.dc.DrawString(line, lineX+halfOffset, lineY+halfOffset)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 设置文本颜色
|
|||
|
if opt.Color != "" {
|
|||
|
g.dc.SetColor(hexToRGBA(opt.Color))
|
|||
|
}
|
|||
|
|
|||
|
// 绘制文本
|
|||
|
for i, line := range lines {
|
|||
|
if line == "" {
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
// 计算行位置
|
|||
|
lineY := startY + float64(i)*lineHeight
|
|||
|
|
|||
|
// 检查行是否在可见区域内
|
|||
|
if drawHeight > 0 && (lineY < contentY || lineY > contentY+drawHeight) {
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
lineX := g.calcLineX(contentX, drawWidth, line, opt.Align)
|
|||
|
g.dc.DrawString(line, lineX-halfOffset, lineY-halfOffset)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 计算行水平位置
|
|||
|
func (g *Graphics) calcLineX(x, w float64, text, align string) float64 {
|
|||
|
if w <= 0 {
|
|||
|
return x
|
|||
|
}
|
|||
|
|
|||
|
switch align {
|
|||
|
case "center":
|
|||
|
textW, _ := g.dc.MeasureString(text)
|
|||
|
return x + (w-textW)/2
|
|||
|
case "right":
|
|||
|
textW, _ := g.dc.MeasureString(text)
|
|||
|
return x + w - textW
|
|||
|
default: // left
|
|||
|
return x
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 段落换行算法
|
|||
|
func (g *Graphics) wrapParagraph(para string, maxWidth float64) []string {
|
|||
|
var lines []string
|
|||
|
currentLine := ""
|
|||
|
currentWidth := 0.0
|
|||
|
|
|||
|
for _, r := range para {
|
|||
|
char := string(r)
|
|||
|
|
|||
|
// 快速估算字符宽度
|
|||
|
charWidth := g.estimateCharWidth(r)
|
|||
|
if currentWidth+charWidth <= maxWidth {
|
|||
|
currentLine += char
|
|||
|
currentWidth += charWidth
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
// 精确测量(必要时)
|
|||
|
if measuredWidth, _ := g.dc.MeasureString(currentLine + char); measuredWidth <= maxWidth {
|
|||
|
currentLine += char
|
|||
|
currentWidth = measuredWidth
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
// 需要换行处理
|
|||
|
if currentLine != "" {
|
|||
|
lines = append(lines, currentLine)
|
|||
|
}
|
|||
|
|
|||
|
currentLine = char
|
|||
|
currentWidth = g.estimateCharWidth(r)
|
|||
|
}
|
|||
|
|
|||
|
if currentLine != "" {
|
|||
|
lines = append(lines, currentLine)
|
|||
|
}
|
|||
|
|
|||
|
return lines
|
|||
|
}
|
|||
|
|
|||
|
// 字符宽度估算(比实际测量高效)
|
|||
|
func (g *Graphics) estimateCharWidth(r rune) float64 {
|
|||
|
if g.lastFont != nil {
|
|||
|
advance, ok := g.lastFont.GlyphAdvance(r)
|
|||
|
if ok {
|
|||
|
return float64(advance) / 64 // fixed.Int26_6 转像素值
|
|||
|
}
|
|||
|
}
|
|||
|
return g.dc.FontHeight() * 0.6 // 默认比例估算
|
|||
|
}
|
|||
|
|
|||
|
// 行高计算
|
|||
|
func (g *Graphics) calcLineHeight(multiplier float64) float64 {
|
|||
|
if multiplier <= 0 {
|
|||
|
multiplier = 1.0 // 默认行高
|
|||
|
}
|
|||
|
|
|||
|
if g.lastFont != nil {
|
|||
|
metrics := g.lastFont.Metrics()
|
|||
|
return float64(metrics.Height) / 64 * multiplier
|
|||
|
}
|
|||
|
|
|||
|
return g.dc.FontHeight() * multiplier
|
|||
|
}
|
|||
|
|
|||
|
// MeasureText 测量文本尺寸
|
|||
|
func (g *Graphics) MeasureText(text string) (width, height float64) {
|
|||
|
return g.dc.MeasureString(text)
|
|||
|
}
|
|||
|
|
|||
|
// RandText 绘制随机文本并返回每个字符的位置信息
|
|||
|
// 返回: 每个字符的位置数组 [x, y, width, height]
|
|||
|
func (g *Graphics) RandText(text string) [][4]float64 {
|
|||
|
if text == "" {
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
bounds := g.dc.Image().Bounds()
|
|||
|
w, h := bounds.Dx(), bounds.Dy()
|
|||
|
|
|||
|
// 智能字体大小计算:确保字体足够大
|
|||
|
fontSize := math.Max(32, float64(h)*0.6) // 最小32px,高度60%
|
|||
|
if len(text) > 4 {
|
|||
|
fontSize = math.Max(28, fontSize*0.9) // 长文本适当减小
|
|||
|
}
|
|||
|
g.SetFont(fontSize)
|
|||
|
|
|||
|
metrics := g.lastFont.Metrics()
|
|||
|
ascent := float64(metrics.Ascent) / 64
|
|||
|
descent := float64(metrics.Descent) / 64
|
|||
|
charHeight := ascent + descent
|
|||
|
// height := float64(metrics.Height) / 64
|
|||
|
fullWidth, _ := g.dc.MeasureString(text)
|
|||
|
|
|||
|
// 计算初始位置(水平居中,但留出随机偏移空间)
|
|||
|
startX := (float64(w) - fullWidth) / 2
|
|||
|
startY := float64(h)/2 + ascent - charHeight/2 // 基于文本高度的垂直居中
|
|||
|
|
|||
|
// 保存初始状态
|
|||
|
g.dc.Push()
|
|||
|
defer g.dc.Pop()
|
|||
|
|
|||
|
// 存储每个字符的位置信息
|
|||
|
charPositions := make([][4]float64, 0, len(text))
|
|||
|
|
|||
|
// 字符处理
|
|||
|
x := startX
|
|||
|
for _, char := range text {
|
|||
|
charStr := string(char)
|
|||
|
|
|||
|
// 准确测量中文字符
|
|||
|
charWidth, _ := g.dc.MeasureString(charStr)
|
|||
|
// charWidth, charHeight := g.dc.MeasureString(charStr)
|
|||
|
|
|||
|
// 随机Y偏移(不超过字体高度的20%)
|
|||
|
maxYOffset := fontSize * 0.2
|
|||
|
yOffset := rand.Float64()*(2*maxYOffset) - maxYOffset
|
|||
|
|
|||
|
// 随机旋转(-15°到15°)
|
|||
|
angle := rand.Float64()*0.52 - 0.26 // ±15°弧度值
|
|||
|
|
|||
|
// 记录字符位置(旋转前的原始位置)
|
|||
|
charPositions = append(charPositions, [4]float64{
|
|||
|
x, // X位置
|
|||
|
startY + yOffset - charHeight + descent, // Y位置
|
|||
|
charWidth, // 字符宽度
|
|||
|
charHeight, // 字符高度
|
|||
|
})
|
|||
|
|
|||
|
g.dc.Push()
|
|||
|
|
|||
|
// 应用变换
|
|||
|
g.dc.RotateAbout(angle, x, startY+yOffset)
|
|||
|
g.dc.SetColor(color.Gray{Y: 50})
|
|||
|
g.dc.DrawString(charStr, x+1, startY+yOffset+1)
|
|||
|
g.dc.SetColor(generateLightTextColor())
|
|||
|
g.dc.DrawString(charStr, x, startY+yOffset)
|
|||
|
g.dc.Pop()
|
|||
|
|
|||
|
// 优化间距:确保足够的字符间距
|
|||
|
// 基础间距为字符宽度的80%
|
|||
|
baseSpace := charWidth * 0.8
|
|||
|
|
|||
|
// 添加随机间距(0-20%)
|
|||
|
randomSpace := rand.Float64() * charWidth * 0.2
|
|||
|
|
|||
|
// 长文本适当减少间距
|
|||
|
if len(text) > 6 {
|
|||
|
baseSpace *= 0.9
|
|||
|
}
|
|||
|
|
|||
|
// 根据角度调整实际水平移动
|
|||
|
effectiveSpace := (baseSpace + randomSpace) * math.Cos(math.Abs(angle))
|
|||
|
x += effectiveSpace * 1.2
|
|||
|
}
|
|||
|
|
|||
|
return charPositions
|
|||
|
}
|
|||
|
|
|||
|
// 生成浅色系文字颜色
|
|||
|
func generateLightTextColor() color.RGBA {
|
|||
|
return color.RGBA{
|
|||
|
R: 150 + uint8(rand.Intn(106)),
|
|||
|
G: 150 + uint8(rand.Intn(106)),
|
|||
|
B: 150 + uint8(rand.Intn(106)),
|
|||
|
A: 190 + uint8(rand.Intn(66)),
|
|||
|
}
|
|||
|
}
|