img/text.go
2025-07-29 16:59:49 +08:00

732 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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