package text2img 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 := ` ` 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) } } }