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