img/text.go

732 lines
18 KiB
Go
Raw Normal View History

2025-07-29 16:59:49 +08:00
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)),
}
}