feat: 添加图像处理功能,包含精灵管理、文本渲染和SVG字体支持,更新README文档以描述新功能,添加.gitignore文件以排除不必要的文件。

This commit is contained in:
lixiangwuxian 2025-05-10 05:15:32 +08:00
parent 1346911f86
commit 2c2f855198
8 changed files with 14266 additions and 1 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
test.png
face.png
.vscode/launch.json

188
README.md
View File

@ -1,3 +1,189 @@
# imagedd
golang绘图库
golang绘图库
## 功能概述
imagedd是一个Go语言绘图库提供了精灵图(Sprite)管理、2D图形绘制、图像渲染等功能。
## 主要组件
### 精灵图 (Sprite)
精灵图是基本的渲染单元,包含图像数据和位置信息。
```go
type Sprite struct {
Name string // 精灵名称
Position image.Point // 精灵位置
Image image.Image // 精灵图像
Index int // 用于排序的索引
}
```
#### 精灵图操作
```go
// 移动精灵
sprite.Move(x, y int)
// 旋转精灵
sprite.Rotate(angle float64)
// 投影变换
sprite.Project(projectMatrix *ProjectMatrix)
// 在精灵上绘制线条
sprite.DrawLine(line *Line)
```
### 精灵板 (NamedSpriteBoard)
精灵板用于管理多个精灵,提供高效的添加、查找、排序功能。
```go
// 创建新的精灵板
board := sprite.NewNamedSpriteBoard()
// 添加精灵
board.AddSprite(sprite)
// 按名称查找精灵
foundSprite := board.GetSpriteByName("精灵名称")
// 获取特定索引的所有精灵
sprites := board.GetSpritesByIndex(1)
// 按名称删除精灵
board.RemoveSpriteByName("精灵名称")
// 更新精灵的索引
board.UpdateSpriteIndex("精灵名称", 2)
// 更新精灵的名称
board.UpdateSpriteName("旧名称", "新名称")
// 获取所有精灵按Index主排序Name次排序
allSprites := board.GetAllSprites()
// 渲染精灵板为图像
img := board.RenderToImage()
// 保存为PNG文件
board.SaveToPng("output.png")
```
### 2D图形组件
#### 线条 (Line)
```go
// 创建线条
line := &sprite.Line{
Start: image.Point{X: 10, Y: 10},
End: image.Point{X: 100, Y: 100},
Width: 2,
Color: color.RGBA{255, 0, 0, 255}, // 红色
}
// 添加到精灵
line.AddToSprite(sprite)
```
#### 圆形 (Circle)
```go
// 创建圆形
circle := &sprite.Circle{
Center: image.Point{X: 50, Y: 50},
Radius: 30,
Color: color.RGBA{0, 0, 255, 255}, // 蓝色
}
// 添加到精灵
circle.AddToSprite(sprite)
```
#### 投影矩阵 (ProjectMatrix)
```go
// 创建投影矩阵
matrix := &sprite.ProjectMatrix{
Matrix: [2][2]float64{
{1.0, 0.5},
{0.0, 1.0},
},
}
// 投影点
newPoint := matrix.ProjectPoint(image.Point{X: 10, Y: 20})
// 投影线条
newLine := matrix.ProjectLine(line)
```
## 渲染特性
- 支持精灵的层级渲染通过Index控制
- 支持精灵的名称索引O(1)时间复杂度查找)
- 支持负坐标位置的精灵
- 自动计算画布大小以适应所有精灵
- 处理超出边界的精灵(裁剪)
## 使用示例
```go
package main
import (
"image"
"image/color"
"git.lxtend.com/lixiangwuxian/imagedd/sprite"
)
func main() {
// 创建精灵板
board := sprite.NewNamedSpriteBoard()
// 创建精灵
sprite1 := &sprite.Sprite{
Name: "背景",
Position: image.Point{0, 0},
Index: 0,
}
// 创建圆形并添加到精灵
circle := &sprite.Circle{
Center: image.Point{50, 50},
Radius: 30,
Color: color.RGBA{0, 0, 255, 255},
}
circle.AddToSprite(sprite1)
// 添加到精灵板
board.AddSprite(sprite1)
// 创建第二个精灵
sprite2 := &sprite.Sprite{
Name: "线条",
Position: image.Point{20, 20},
Index: 1,
}
// 创建线条并添加到精灵
line := &sprite.Line{
Start: image.Point{0, 0},
End: image.Point{100, 100},
Width: 2,
Color: color.RGBA{255, 0, 0, 255},
}
line.AddToSprite(sprite2)
// 添加到精灵板
board.AddSprite(sprite2)
// 渲染并保存
board.SaveToPng("output.png")
}
```

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

886
font2img/parse_font.go Normal file
View File

@ -0,0 +1,886 @@
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)
}
}
}

11
go.mod
View File

@ -1,3 +1,14 @@
module git.lxtend.com/lixiangwuxian/imagedd
go 1.24.2
require github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
require golang.org/x/image v0.27.0
require (
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
golang.org/x/text v0.25.0 // indirect
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=

121
main.go Normal file
View File

@ -0,0 +1,121 @@
package main
import (
"image"
"image/color"
"log"
"git.lxtend.com/lixiangwuxian/imagedd/font2img"
"git.lxtend.com/lixiangwuxian/imagedd/sprite"
)
func main() {
board := sprite.NewNamedSpriteBoard()
{
whiteBackground := image.NewRGBA(image.Rect(0, 0, 200, 200))
for i := range whiteBackground.Bounds().Dx() {
for j := range whiteBackground.Bounds().Dy() {
whiteBackground.Set(i, j, color.White)
}
}
board.AddSprite(&sprite.Sprite{
Name: "background",
Image: whiteBackground,
Index: 0,
Position: image.Point{X: 0, Y: 0},
})
}
// {
// wordSprite := &sprite.Sprite{
// Name: "word",
// Index: 5,
// Position: image.Point{X: 0, Y: 0},
// }
// img, err := font2img.RenderCharToImage(font, '🤣', 90, color.Black)
// if err != nil {
// log.Fatal(err)
// }
// wordSprite.Image = img
// board.AddSprite(wordSprite)
// }
{
textSprite := &sprite.Sprite{
Name: "text",
Index: 5,
Position: image.Point{X: 10, Y: 12},
}
img, err := font2img.RenderTextToTrimmedImage(nil, "usss测试\n测试📧测✌试测\n试🥳🧁🍰\n🎁🎂🎈🎺🎉🎊\n📧🧿🌶无焊无缝🔋😂❤😍🤣😊🥺\n🙏💕😭😘👍\n😅👏测试测试", 12, color.Black, 0, 0)
if err != nil {
log.Fatal(err)
}
textSprite.Image = img
board.AddSprite(textSprite)
}
// {
// faceBytes, err := os.ReadFile("face.png")
// if err != nil {
// log.Fatal(err)
// }
// faceImage, err := png.Decode(bytes.NewReader(faceBytes))
// if err != nil {
// log.Fatal(err)
// }
// faceSprite := &sprite.Sprite{
// Name: "face",
// Image: faceImage,
// Index: 1,
// Position: image.Point{X: -100, Y: -100},
// }
// faceSprite.Rotate(math.Pi / 2)
// board.AddSprite(faceSprite)
// }
// {
// rect := image.NewRGBA(image.Rect(0, 0, 101, 101))
// rectSprite := &sprite.Sprite{
// Name: "rect",
// Image: rect,
// Index: 2,
// Position: image.Point{X: 20, Y: 20},
// }
// lineL := &sprite.Line{
// Start: image.Point{X: 0, Y: 100},
// End: image.Point{X: 50, Y: 0},
// Width: 1,
// Color: color.RGBA{0, 0, 0, 255},
// }
// lineL.AddToSprite(rectSprite)
// lineR := &sprite.Line{
// Start: image.Point{X: 100, Y: 100},
// End: image.Point{X: 50, Y: 0},
// Width: 1,
// Color: color.RGBA{0, 0, 0, 255},
// }
// lineR.AddToSprite(rectSprite)
// lineB := &sprite.Line{
// Start: image.Point{X: 0, Y: 100},
// End: image.Point{X: 100, Y: 100},
// Width: 1,
// Color: color.RGBA{0, 0, 0, 255},
// }
// lineB.AddToSprite(rectSprite)
// board.AddSprite(rectSprite)
// }
// {
// circleSprite := &sprite.Sprite{
// Name: "circle",
// Image: image.NewRGBA(image.Rect(0, 0, 101, 101)),
// Index: 3,
// Position: image.Point{X: 30, Y: 20},
// }
// for r := 15; r < 45; r++ {
// circle := &sprite.Circle{
// Center: image.Point{X: 50, Y: 50},
// Radius: r,
// Color: color.RGBA{0, 0, 255, 255},
// }
// circle.AddToSprite(circleSprite)
// }
// board.AddSprite(circleSprite)
// }
board.SaveToPng("test.png")
}