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