887 lines
22 KiB
Go
887 lines
22 KiB
Go
package font2img
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/xml"
|
||
"errors"
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"os"
|
||
"strings"
|
||
"sync"
|
||
"unicode"
|
||
|
||
"github.com/golang/freetype"
|
||
"github.com/golang/freetype/truetype"
|
||
"github.com/srwiley/oksvg"
|
||
"github.com/srwiley/rasterx"
|
||
"golang.org/x/image/font"
|
||
"golang.org/x/image/math/fixed"
|
||
|
||
_ "embed"
|
||
)
|
||
|
||
//go:embed NotoEmoji-VariableFont_wght.svg
|
||
var svgEmojiBytes []byte
|
||
|
||
//go:embed fusion-pixel-12px-monospaced.ttf
|
||
var ttfBytes []byte
|
||
|
||
// 全局emoji字体和相关变量
|
||
var (
|
||
svgEmojiPath string
|
||
svgEmojiLock sync.RWMutex
|
||
|
||
// 添加SVG缓存,避免重复解析相同的SVG文件
|
||
svgCache map[rune][]byte
|
||
svgCacheLock sync.RWMutex
|
||
|
||
// 保存已加载的SVG字体文件
|
||
svgFontData []byte
|
||
svgFontLock sync.RWMutex
|
||
svgFontCache *SVG
|
||
)
|
||
|
||
func init() {
|
||
// 初始化SVG缓存
|
||
svgCache = make(map[rune][]byte)
|
||
|
||
// 初始化嵌入的SVG字体
|
||
var svg SVG
|
||
if err := xml.Unmarshal(svgEmojiBytes, &svg); err == nil {
|
||
svgFontLock.Lock()
|
||
svgFontData = svgEmojiBytes
|
||
svgFontCache = &svg
|
||
svgFontLock.Unlock()
|
||
}
|
||
}
|
||
|
||
// SVG表示一个简单的SVG解析结构
|
||
type SVG struct {
|
||
Width int `xml:"width,attr"`
|
||
Height int `xml:"height,attr"`
|
||
ViewBox string `xml:"viewBox,attr"`
|
||
Defs struct {
|
||
Font struct {
|
||
Glyphs []struct {
|
||
GlyphName string `xml:"glyph-name,attr"`
|
||
Unicode string `xml:"unicode,attr"`
|
||
VertAdvY string `xml:"vert-adv-y,attr"`
|
||
Path string `xml:"d,attr"`
|
||
} `xml:"glyph"`
|
||
} `xml:"font"`
|
||
} `xml:"defs"`
|
||
}
|
||
|
||
// SetSVGEmojiPath 设置SVG格式emoji的字体文件路径
|
||
func SetSVGEmojiPath(path string) error {
|
||
// 检查文件是否存在
|
||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||
return errors.New("SVG emoji字体文件不存在")
|
||
}
|
||
|
||
svgEmojiLock.Lock()
|
||
defer svgEmojiLock.Unlock()
|
||
|
||
// 清除之前的缓存
|
||
svgFontLock.Lock()
|
||
svgFontData = nil
|
||
svgFontCache = nil
|
||
svgFontLock.Unlock()
|
||
|
||
svgCacheLock.Lock()
|
||
svgCache = make(map[rune][]byte)
|
||
svgCacheLock.Unlock()
|
||
|
||
svgEmojiPath = path
|
||
return nil
|
||
}
|
||
|
||
// loadSVGEmoji 加载特定码点的SVG Emoji
|
||
func loadSVGEmoji(codePoint rune) ([]byte, error) {
|
||
// 先从缓存中查找
|
||
svgCacheLock.RLock()
|
||
if data, ok := svgCache[codePoint]; ok {
|
||
svgCacheLock.RUnlock()
|
||
return data, nil
|
||
}
|
||
svgCacheLock.RUnlock()
|
||
|
||
// 缓存中没有,从SVG字体文件加载
|
||
svgFontLock.RLock()
|
||
fontData := svgFontData
|
||
fontCache := svgFontCache
|
||
svgFontLock.RUnlock()
|
||
|
||
// 如果没有字体数据,且有指定路径,则从文件加载
|
||
if len(fontData) == 0 {
|
||
svgEmojiLock.RLock()
|
||
path := svgEmojiPath
|
||
svgEmojiLock.RUnlock()
|
||
|
||
if path != "" {
|
||
// 读取SVG字体文件
|
||
var err error
|
||
fontData, err = os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 解析SVG字体
|
||
var svg SVG
|
||
if err := xml.Unmarshal(fontData, &svg); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 保存到缓存
|
||
svgFontLock.Lock()
|
||
svgFontData = fontData
|
||
svgFontCache = &svg
|
||
fontCache = &svg
|
||
svgFontLock.Unlock()
|
||
} else {
|
||
// 没有外部路径,直接使用嵌入的字体数据
|
||
fontData = svgEmojiBytes
|
||
|
||
// 如果未初始化,则解析嵌入的字体
|
||
if fontCache == nil {
|
||
var svg SVG
|
||
if err := xml.Unmarshal(fontData, &svg); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
svgFontLock.Lock()
|
||
svgFontCache = &svg
|
||
fontCache = &svg
|
||
svgFontLock.Unlock()
|
||
}
|
||
}
|
||
}
|
||
|
||
// 在字体中查找匹配的字形
|
||
targetChar := string(codePoint)
|
||
|
||
if fontCache != nil {
|
||
for _, glyph := range fontCache.Defs.Font.Glyphs {
|
||
if glyph.Unicode == targetChar {
|
||
// 创建包含该字形的SVG数据
|
||
glyphSVG := createGlyphSVG(glyph, fontCache)
|
||
|
||
// 添加到缓存
|
||
svgCacheLock.Lock()
|
||
svgCache[codePoint] = []byte(glyphSVG)
|
||
svgCacheLock.Unlock()
|
||
|
||
return []byte(glyphSVG), nil
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil, errors.New("未找到匹配的SVG字形")
|
||
}
|
||
|
||
// createGlyphSVG 从字形数据创建独立的SVG
|
||
func createGlyphSVG(glyph struct {
|
||
GlyphName string `xml:"glyph-name,attr"`
|
||
Unicode string `xml:"unicode,attr"`
|
||
VertAdvY string `xml:"vert-adv-y,attr"`
|
||
Path string `xml:"d,attr"`
|
||
}, fontInfo *SVG) string {
|
||
// 获取视口尺寸
|
||
viewBox := fontInfo.ViewBox
|
||
if viewBox == "" {
|
||
viewBox = "0 0 1000 1000" // 默认视口
|
||
}
|
||
|
||
// 创建独立的SVG标记
|
||
svgTemplate := `<svg xmlns="http://www.w3.org/2000/svg" viewBox="%s">
|
||
<path d="%s" fill="currentColor"/>
|
||
</svg>`
|
||
|
||
return fmt.Sprintf(svgTemplate, viewBox, glyph.Path)
|
||
}
|
||
|
||
// renderSVGEmojiToImage 将SVG转换为图像
|
||
func renderSVGEmojiToImage(svgData []byte, size float64, col color.Color) (*image.RGBA, error) {
|
||
// 解析SVG以获取尺寸信息
|
||
var svg SVG
|
||
if err := xml.Unmarshal(svgData, &svg); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 准备输入数据
|
||
r, g, b, _ := col.RGBA()
|
||
hexColor := rgbaToHex(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||
|
||
// 替换currentColor为指定的颜色
|
||
svgStr := string(svgData)
|
||
svgStr = strings.ReplaceAll(svgStr, "currentColor", hexColor)
|
||
|
||
// 创建输出图像
|
||
imgSize := int(size)
|
||
if imgSize < 10 {
|
||
imgSize = 10
|
||
}
|
||
|
||
// 创建一个透明背景的RGBA图像
|
||
rgba := image.NewRGBA(image.Rect(0, 0, imgSize, imgSize))
|
||
draw.Draw(rgba, rgba.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||
|
||
// 直接渲染SVG数据到图像
|
||
err := renderSVGDataToImage([]byte(svgStr), rgba)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return rgba, nil
|
||
}
|
||
|
||
// renderSVGDataToImage 使用oksvg库渲染SVG数据到图像
|
||
func renderSVGDataToImage(svgData []byte, dst *image.RGBA) error {
|
||
// 从字节数据创建SVG图标
|
||
icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData), oksvg.StrictErrorMode)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 设置图标大小适应目标图像
|
||
bounds := dst.Bounds()
|
||
width, height := float64(bounds.Dx()), float64(bounds.Dy())
|
||
|
||
// 创建转换后的图像(先渲染到这里,然后再处理翻转)
|
||
tempImg := image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy()))
|
||
|
||
// 适应目标大小
|
||
icon.SetTarget(0, width/8, width/2.7, height/2.7)
|
||
|
||
// 创建光栅化器
|
||
scanner := rasterx.NewScannerGV(bounds.Dx(), bounds.Dy(), tempImg, tempImg.Bounds())
|
||
raster := rasterx.NewDasher(bounds.Dx(), bounds.Dy(), scanner)
|
||
|
||
// 渲染图标
|
||
icon.Draw(raster, 1.0)
|
||
|
||
// 只垂直翻转图像(处理上下颠倒问题)
|
||
for y := 0; y < bounds.Dy(); y++ {
|
||
for x := 0; x < bounds.Dx(); x++ {
|
||
// 计算垂直翻转后的坐标
|
||
flippedY := bounds.Dy() - y - 1
|
||
|
||
// 将翻转后的像素复制到目标图像
|
||
dst.Set(x, y, tempImg.At(x, flippedY))
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// rgbaToHex 将RGB颜色分量转换为十六进制字符串
|
||
func rgbaToHex(r, g, b uint8) string {
|
||
return "#" + colorToHex(r) + colorToHex(g) + colorToHex(b)
|
||
}
|
||
|
||
// colorToHex 将单个颜色分量转换为十六进制
|
||
func colorToHex(c uint8) string {
|
||
hex := "0123456789ABCDEF"
|
||
return string(hex[c>>4]) + string(hex[c&0x0F])
|
||
}
|
||
|
||
// IsEmoji 检测一个字符是否是emoji
|
||
func IsEmoji(r rune) bool {
|
||
// 简单检测一些常见的emoji范围
|
||
return unicode.Is(unicode.So, r) || // Symbol, Other
|
||
// 表情符号范围
|
||
(r >= 0x1F600 && r <= 0x1F64F) || // Emoticons
|
||
(r >= 0x1F300 && r <= 0x1F5FF) || // Misc Symbols and Pictographs
|
||
(r >= 0x1F680 && r <= 0x1F6FF) || // Transport and Map
|
||
(r >= 0x1F700 && r <= 0x1F77F) || // Alchemical Symbols
|
||
(r >= 0x1F780 && r <= 0x1F7FF) || // Geometric Shapes
|
||
(r >= 0x1F800 && r <= 0x1F8FF) || // Supplemental Arrows-C
|
||
(r >= 0x1F900 && r <= 0x1F9FF) || // Supplemental Symbols and Pictographs
|
||
(r >= 0x1FA00 && r <= 0x1FA6F) || // Chess Symbols
|
||
(r >= 0x1FA70 && r <= 0x1FAFF) || // Symbols and Pictographs Extended-A
|
||
// 国旗符号
|
||
(r >= 0x1F1E6 && r <= 0x1F1FF)
|
||
}
|
||
|
||
// HasGlyph 检查字体是否包含该字符的字形
|
||
func HasGlyph(fontContent *truetype.Font, char rune) bool {
|
||
if fontContent == nil {
|
||
return false
|
||
}
|
||
// 尝试获取字符索引
|
||
index := fontContent.Index(char)
|
||
return index != 0 // 索引0通常是notdef字形
|
||
}
|
||
|
||
// SafeGlyphAdvance 安全获取字符宽度,避免崩溃
|
||
func SafeGlyphAdvance(face font.Face, char rune) (fixed.Int26_6, bool) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
// 发生panic,忽略
|
||
}
|
||
}()
|
||
|
||
return face.GlyphAdvance(char)
|
||
}
|
||
|
||
// SafeGlyphBounds 安全获取字符边界,避免崩溃
|
||
func SafeGlyphBounds(face font.Face, char rune) (fixed.Rectangle26_6, fixed.Int26_6, bool) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
// 发生panic,忽略
|
||
}
|
||
}()
|
||
|
||
return face.GlyphBounds(char)
|
||
}
|
||
|
||
// SafeDrawString 安全绘制字符串,避免崩溃
|
||
func SafeDrawString(ctx *freetype.Context, text string, pt fixed.Point26_6) (fixed.Point26_6, error) {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
// 发生panic,忽略
|
||
}
|
||
}()
|
||
|
||
return ctx.DrawString(text, pt)
|
||
}
|
||
|
||
// ParseFont 解析字体文件或使用内嵌字体
|
||
func ParseFont(fontPath string) (*truetype.Font, error) {
|
||
// 如果提供了路径,尝试从文件加载
|
||
if fontPath != "" {
|
||
fontBytes, err := os.ReadFile(fontPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
fontCache, err := truetype.Parse(fontBytes)
|
||
return fontCache, err
|
||
}
|
||
|
||
// 没有提供路径,使用嵌入的TTF字体
|
||
fontCache, err := truetype.Parse(ttfBytes)
|
||
return fontCache, err
|
||
}
|
||
|
||
// RenderCharToImage 将单个字符渲染为图像
|
||
// fontSize: 字体大小(点数)
|
||
// textColor: 字体颜色
|
||
// 返回字符的图像
|
||
func RenderCharToImage(fontContent *truetype.Font, char rune, fontSize float64, textColor color.Color) (*image.RGBA, error) {
|
||
// 确保字体已加载
|
||
if fontContent == nil {
|
||
// 尝试使用嵌入的字体
|
||
var err error
|
||
fontContent, err = truetype.Parse(ttfBytes)
|
||
if err != nil {
|
||
return nil, ErrFontNotLoaded
|
||
}
|
||
}
|
||
|
||
// 检查是否是emoji,且应该使用SVG格式
|
||
if IsEmoji(char) {
|
||
// 尝试加载SVG emoji
|
||
svgData, err := loadSVGEmoji(char)
|
||
if err == nil {
|
||
// 成功加载SVG,渲染为图像
|
||
return renderSVGEmojiToImage(svgData, fontSize, textColor)
|
||
}
|
||
// 如果加载失败,回退到使用普通emoji字体
|
||
return createPlaceholderImage(fontSize, textColor), nil
|
||
}
|
||
|
||
// 创建字体上下文
|
||
c := freetype.NewContext()
|
||
c.SetDPI(72) // 标准DPI
|
||
c.SetFont(fontContent)
|
||
c.SetFontSize(fontSize)
|
||
c.SetHinting(font.HintingFull)
|
||
|
||
// 计算字符尺寸
|
||
opts := truetype.Options{
|
||
Size: fontSize,
|
||
DPI: 72,
|
||
Hinting: font.HintingFull,
|
||
}
|
||
face := truetype.NewFace(fontContent, &opts)
|
||
|
||
// 测量单个字符宽度
|
||
charWidth, ok := SafeGlyphAdvance(face, char)
|
||
if !ok {
|
||
// 无法获取字符宽度,使用替代图像
|
||
return createPlaceholderImage(fontSize, textColor), nil
|
||
}
|
||
widthInPixels := charWidth.Ceil()
|
||
if widthInPixels <= 0 {
|
||
widthInPixels = int(fontSize / 2)
|
||
}
|
||
|
||
// 计算字体高度和基线位置
|
||
bounds, _, ok := SafeGlyphBounds(face, char)
|
||
if !ok {
|
||
// 无法获取字符边界,使用替代图像
|
||
return createPlaceholderImage(fontSize, textColor), nil
|
||
}
|
||
height := (bounds.Max.Y - bounds.Min.Y).Ceil()
|
||
if height <= 0 {
|
||
height = int(fontSize)
|
||
}
|
||
|
||
// 添加少量额外空间确保字符完全显示
|
||
heightInPixels := height + int(fontSize/8)
|
||
|
||
// 创建图像,确保有足够空间
|
||
rgba := image.NewRGBA(image.Rect(0, 0, widthInPixels, heightInPixels))
|
||
draw.Draw(rgba, rgba.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||
|
||
// 设置绘图属性
|
||
c.SetDst(rgba)
|
||
c.SetClip(rgba.Bounds())
|
||
c.SetSrc(image.NewUniform(textColor))
|
||
|
||
// 计算正确的基线位置
|
||
// 基线应该在字符下降部分的上方
|
||
baseline := (-bounds.Min.Y).Ceil()
|
||
|
||
// 绘制字符,Y坐标需要考虑一个偏移以确保完整显示
|
||
pt := fixed.Point26_6{
|
||
X: fixed.Int26_6(0 << 6),
|
||
Y: fixed.Int26_6(baseline << 6),
|
||
}
|
||
_, err := SafeDrawString(c, string(char), pt)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 裁剪图像,去除多余的空白
|
||
trimmedRGBA := trimImage(rgba)
|
||
return trimmedRGBA, nil
|
||
}
|
||
|
||
// trimImage 裁剪图像,去除底部、顶部、左侧和右侧的空白
|
||
func trimImage(img *image.RGBA) *image.RGBA {
|
||
bounds := img.Bounds()
|
||
min, max := bounds.Min, bounds.Max
|
||
|
||
// 找到字符的实际底部(最后一个非透明像素的行)
|
||
bottom := min.Y
|
||
for y := max.Y - 1; y >= min.Y; y-- {
|
||
isEmpty := true
|
||
for x := min.X; x < max.X; x++ {
|
||
_, _, _, a := img.At(x, y).RGBA()
|
||
if a > 0 {
|
||
isEmpty = false
|
||
break
|
||
}
|
||
}
|
||
if !isEmpty {
|
||
bottom = y + 1 // 找到最后一个非空行
|
||
break
|
||
}
|
||
}
|
||
|
||
// 找到字符的实际顶部(第一个非透明像素的行)
|
||
top := max.Y - 1
|
||
for y := min.Y; y < max.Y; y++ {
|
||
isEmpty := true
|
||
for x := min.X; x < max.X; x++ {
|
||
_, _, _, a := img.At(x, y).RGBA()
|
||
if a > 0 {
|
||
isEmpty = false
|
||
break
|
||
}
|
||
}
|
||
if !isEmpty {
|
||
top = y // 找到第一个非空行
|
||
break
|
||
}
|
||
}
|
||
|
||
// 找到字符的实际左侧边缘(第一个非透明像素的列)
|
||
left := max.X - 1
|
||
for x := min.X; x < max.X; x++ {
|
||
isEmpty := true
|
||
for y := min.Y; y < max.Y; y++ {
|
||
_, _, _, a := img.At(x, y).RGBA()
|
||
if a > 0 {
|
||
isEmpty = false
|
||
break
|
||
}
|
||
}
|
||
if !isEmpty {
|
||
left = x // 找到第一个非空列
|
||
break
|
||
}
|
||
}
|
||
|
||
// 找到字符的实际右侧边缘(最后一个非透明像素的列)
|
||
right := min.X
|
||
for x := max.X - 1; x >= min.X; x-- {
|
||
isEmpty := true
|
||
for y := min.Y; y < max.Y; y++ {
|
||
_, _, _, a := img.At(x, y).RGBA()
|
||
if a > 0 {
|
||
isEmpty = false
|
||
break
|
||
}
|
||
}
|
||
if !isEmpty {
|
||
right = x + 1 // 找到最后一个非空列
|
||
break
|
||
}
|
||
}
|
||
|
||
// 防止空图像情况
|
||
if top >= bottom || left >= right {
|
||
return img // 返回原图
|
||
}
|
||
|
||
// 创建新图像并复制内容
|
||
newWidth := right - left
|
||
newHeight := bottom - top
|
||
newImg := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
||
for y := 0; y < newHeight; y++ {
|
||
for x := 0; x < newWidth; x++ {
|
||
newImg.Set(x, y, img.At(x+left, y+top))
|
||
}
|
||
}
|
||
|
||
return newImg
|
||
}
|
||
|
||
// RenderTextToImage 将一段文本渲染为图像
|
||
// text: 要渲染的文本,支持换行符
|
||
// fontSize: 字体大小(点数)
|
||
// textColor: 字体颜色
|
||
// charSpacing: 字符间距(像素)
|
||
// lineSpacing: 行间距(像素)
|
||
// 返回渲染后的图像
|
||
func RenderTextToImage(fontContent *truetype.Font, text string, fontSize float64, textColor color.Color, charSpacing, lineSpacing int) (*image.RGBA, error) {
|
||
// 确保字体已加载
|
||
if fontContent == nil {
|
||
// 尝试使用嵌入的字体
|
||
var err error
|
||
fontContent, err = truetype.Parse(ttfBytes)
|
||
if err != nil {
|
||
return nil, ErrFontNotLoaded
|
||
}
|
||
}
|
||
|
||
// 检查是否有SVG emoji数据
|
||
hasSVGData := false
|
||
svgFontLock.RLock()
|
||
hasSVGData = svgFontCache != nil
|
||
svgFontLock.RUnlock()
|
||
|
||
// 首先需要计算整个文本的尺寸
|
||
lines := splitTextIntoLines(text)
|
||
if len(lines) == 0 {
|
||
return image.NewRGBA(image.Rect(0, 0, 1, 1)), nil // 返回1x1空图像
|
||
}
|
||
|
||
// 创建字体配置
|
||
opts := truetype.Options{
|
||
Size: fontSize,
|
||
DPI: 72,
|
||
Hinting: font.HintingFull,
|
||
}
|
||
face := truetype.NewFace(fontContent, &opts)
|
||
|
||
// 计算每行的宽度和整体高度
|
||
var maxWidth int
|
||
lineHeights := make([]int, len(lines))
|
||
lineBounds := make([]fixed.Rectangle26_6, len(lines))
|
||
|
||
for i, line := range lines {
|
||
var lineWidth int
|
||
// 计算一行的宽度
|
||
for _, char := range line {
|
||
// 决定使用哪个字体
|
||
currentFace := face
|
||
|
||
// 处理SVG emoji
|
||
if hasSVGData && IsEmoji(char) {
|
||
// 对于SVG emoji,使用固定宽度
|
||
lineWidth += int(fontSize) + charSpacing
|
||
continue
|
||
}
|
||
|
||
advance, ok := SafeGlyphAdvance(currentFace, char)
|
||
if !ok {
|
||
// 如果无法获取宽度,使用默认宽度
|
||
lineWidth += int(fontSize/2) + charSpacing
|
||
continue
|
||
}
|
||
lineWidth += advance.Ceil() + charSpacing
|
||
}
|
||
|
||
// 如果一行有字符,减去最后一个字符后多加的间距
|
||
if len(line) > 0 {
|
||
lineWidth -= charSpacing
|
||
}
|
||
|
||
// 计算行的边界,用于高度计算
|
||
lineBound := fixed.Rectangle26_6{
|
||
Min: fixed.Point26_6{X: 0, Y: fixed.Int26_6(-int(fontSize) << 6)},
|
||
Max: fixed.Point26_6{X: 0, Y: fixed.Int26_6(int(fontSize) << 6)},
|
||
}
|
||
|
||
for _, char := range line {
|
||
// 如果是SVG emoji,跳过边界计算
|
||
if hasSVGData && IsEmoji(char) {
|
||
continue
|
||
}
|
||
|
||
// 决定使用哪个字体
|
||
currentFace := face
|
||
|
||
bound, _, ok := SafeGlyphBounds(currentFace, char)
|
||
if !ok {
|
||
continue
|
||
}
|
||
if bound.Min.Y < lineBound.Min.Y {
|
||
lineBound.Min.Y = bound.Min.Y
|
||
}
|
||
if bound.Max.Y > lineBound.Max.Y {
|
||
lineBound.Max.Y = bound.Max.Y
|
||
}
|
||
}
|
||
|
||
lineBounds[i] = lineBound
|
||
lineHeight := int(fontSize)
|
||
lineHeights[i] = lineHeight
|
||
|
||
if lineWidth > maxWidth {
|
||
maxWidth = lineWidth
|
||
}
|
||
}
|
||
|
||
// 防止创建太小的图像
|
||
if maxWidth <= 0 {
|
||
maxWidth = int(fontSize)
|
||
}
|
||
|
||
// 计算总高度(每行高度+行间距)
|
||
totalHeight := 0
|
||
for _, height := range lineHeights {
|
||
totalHeight += height + lineSpacing + 1
|
||
}
|
||
// 减去最后一行多加的行间距
|
||
if len(lineHeights) > 0 {
|
||
totalHeight -= lineSpacing
|
||
}
|
||
|
||
// 防止创建太小的图像
|
||
if totalHeight <= 0 {
|
||
totalHeight = int(fontSize)
|
||
}
|
||
|
||
// 创建图像
|
||
rgba := image.NewRGBA(image.Rect(0, 0, maxWidth, totalHeight))
|
||
draw.Draw(rgba, rgba.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||
|
||
// 设置字体上下文
|
||
c := freetype.NewContext()
|
||
c.SetDPI(72)
|
||
c.SetFontSize(fontSize)
|
||
c.SetHinting(font.HintingFull)
|
||
c.SetDst(rgba)
|
||
c.SetClip(rgba.Bounds())
|
||
c.SetSrc(image.NewUniform(textColor))
|
||
|
||
// 渲染每一行文本
|
||
y := 0
|
||
for i, line := range lines {
|
||
if len(line) == 0 {
|
||
y += lineHeights[i] + lineSpacing
|
||
continue
|
||
}
|
||
|
||
// 计算基线位置
|
||
baseline := y + (-lineBounds[i].Min.Y).Ceil()
|
||
|
||
// 绘制每个字符
|
||
x := 0
|
||
for _, char := range line {
|
||
// 如果是SVG格式的emoji,单独处理
|
||
if hasSVGData && IsEmoji(char) {
|
||
svgData, err := loadSVGEmoji(char)
|
||
if err == nil {
|
||
// 渲染SVG emoji
|
||
emojiImg, err := renderSVGEmojiToImage(svgData, fontSize, textColor)
|
||
if err == nil {
|
||
emojiBounds := emojiImg.Bounds()
|
||
draw.Draw(rgba,
|
||
image.Rect(x, baseline-int(fontSize), x+emojiBounds.Dx(), baseline),
|
||
emojiImg,
|
||
image.Point{},
|
||
draw.Over)
|
||
x += emojiBounds.Dx() + charSpacing
|
||
continue
|
||
}
|
||
}
|
||
// 如果加载或渲染SVG失败,回退到使用普通方式
|
||
}
|
||
|
||
// 决定使用哪个字体
|
||
currentFont := fontContent
|
||
currentFace := face
|
||
useCurrentFont := true
|
||
|
||
if useCurrentFont {
|
||
// 设置当前字符的字体
|
||
c.SetFont(currentFont)
|
||
|
||
// 绘制字符
|
||
pt := fixed.Point26_6{
|
||
X: fixed.Int26_6(x << 6),
|
||
Y: fixed.Int26_6(baseline << 6),
|
||
}
|
||
|
||
width, ok := SafeGlyphAdvance(currentFace, char)
|
||
if !ok {
|
||
// 无法获取宽度,使用默认宽度
|
||
x += int(fontSize/2) + charSpacing
|
||
continue
|
||
}
|
||
|
||
_, err := SafeDrawString(c, string(char), pt)
|
||
if err != nil {
|
||
// 绘制失败,使用替代方法
|
||
drawPlaceholder(rgba, x, baseline, int(fontSize), textColor)
|
||
x += int(fontSize/2) + charSpacing
|
||
continue
|
||
}
|
||
|
||
// 移动到下一个字符位置
|
||
x += width.Ceil() + charSpacing
|
||
} else {
|
||
// 不能使用当前字体,绘制占位符
|
||
drawPlaceholder(rgba, x, baseline, int(fontSize/2), textColor)
|
||
x += int(fontSize/2) + charSpacing
|
||
}
|
||
}
|
||
|
||
// 移动到下一行
|
||
y += lineHeights[i] + lineSpacing
|
||
}
|
||
|
||
return rgba, nil
|
||
}
|
||
|
||
// RenderTextToTrimmedImage 将一段文本渲染为图像并裁剪多余空白
|
||
// 与RenderTextToImage功能相同,但会自动裁剪图像边缘的空白
|
||
func RenderTextToTrimmedImage(fontContent *truetype.Font, text string, fontSize float64, textColor color.Color, charSpacing, lineSpacing int) (*image.RGBA, error) {
|
||
// 如果字体为nil,尝试使用嵌入的字体
|
||
if fontContent == nil {
|
||
var err error
|
||
fontContent, err = truetype.Parse(ttfBytes)
|
||
if err != nil {
|
||
return nil, ErrFontNotLoaded
|
||
}
|
||
}
|
||
|
||
img, err := RenderTextToImage(fontContent, text, fontSize, textColor, charSpacing, lineSpacing)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 裁剪图像四周的空白
|
||
return trimImage(img), nil
|
||
}
|
||
|
||
// splitTextIntoLines 将文本按换行符分割成行
|
||
func splitTextIntoLines(text string) [][]rune {
|
||
lines := [][]rune{}
|
||
currentLine := []rune{}
|
||
|
||
for _, char := range text {
|
||
if char == '\n' {
|
||
lines = append(lines, currentLine)
|
||
currentLine = []rune{}
|
||
} else {
|
||
currentLine = append(currentLine, char)
|
||
}
|
||
}
|
||
|
||
// 添加最后一行
|
||
if len(currentLine) > 0 {
|
||
lines = append(lines, currentLine)
|
||
}
|
||
|
||
return lines
|
||
}
|
||
|
||
// ErrFontNotLoaded 表示字体未加载错误
|
||
var ErrFontNotLoaded = ErrorMessage("字体未加载,请先调用ParseFont加载字体")
|
||
|
||
// ErrorMessage 是一个自定义错误类型
|
||
type ErrorMessage string
|
||
|
||
func (e ErrorMessage) Error() string {
|
||
return string(e)
|
||
}
|
||
|
||
// createPlaceholderImage 创建替代图像,用于不支持的字符
|
||
func createPlaceholderImage(fontSize float64, textColor color.Color) *image.RGBA {
|
||
size := int(fontSize)
|
||
if size < 10 {
|
||
size = 10
|
||
}
|
||
|
||
// 创建正方形图像
|
||
img := image.NewRGBA(image.Rect(0, 0, size, size))
|
||
draw.Draw(img, img.Bounds(), image.Transparent, image.Point{}, draw.Src)
|
||
|
||
// 绘制边框
|
||
for x := 0; x < size; x++ {
|
||
img.Set(x, 0, textColor)
|
||
img.Set(x, size-1, textColor)
|
||
}
|
||
for y := 0; y < size; y++ {
|
||
img.Set(0, y, textColor)
|
||
img.Set(size-1, y, textColor)
|
||
}
|
||
|
||
// 绘制对角线
|
||
for i := 0; i < size; i++ {
|
||
if i < size {
|
||
img.Set(i, i, textColor)
|
||
img.Set(i, size-i-1, textColor)
|
||
}
|
||
}
|
||
|
||
return img
|
||
}
|
||
|
||
// drawPlaceholder 在图像上绘制占位符
|
||
func drawPlaceholder(img *image.RGBA, x, y, size int, c color.Color) {
|
||
halfSize := size / 2
|
||
|
||
// 绘制一个简单的方框
|
||
for i := 0; i < size; i++ {
|
||
img.Set(x+i, y-halfSize, c) // 顶边
|
||
img.Set(x+i, y+halfSize, c) // 底边
|
||
}
|
||
|
||
for i := -halfSize; i <= halfSize; i++ {
|
||
img.Set(x, y+i, c) // 左边
|
||
img.Set(x+size-1, y+i, c) // 右边
|
||
}
|
||
|
||
// 绘制对角线
|
||
for i := 0; i < size; i++ {
|
||
y1 := y - halfSize + i
|
||
y2 := y + halfSize - i
|
||
|
||
if y1 >= 0 && y1 < img.Bounds().Max.Y &&
|
||
y2 >= 0 && y2 < img.Bounds().Max.Y {
|
||
img.Set(x+i, y1, c)
|
||
img.Set(x+i, y2, c)
|
||
}
|
||
}
|
||
}
|