diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4aa640c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+test.png
+face.png
+.vscode/launch.json
diff --git a/README.md b/README.md
index 99a5124..f80c81a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,189 @@
# imagedd
-golang绘图库
\ No newline at end of file
+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")
+}
+```
\ No newline at end of file
diff --git a/font2img/NotoEmoji-VariableFont_wght.svg b/font2img/NotoEmoji-VariableFont_wght.svg
new file mode 100644
index 0000000..4d2e969
--- /dev/null
+++ b/font2img/NotoEmoji-VariableFont_wght.svg
@@ -0,0 +1,13046 @@
+
+
+
diff --git a/font2img/fusion-pixel-12px-monospaced.ttf b/font2img/fusion-pixel-12px-monospaced.ttf
new file mode 100644
index 0000000..ea0c426
Binary files /dev/null and b/font2img/fusion-pixel-12px-monospaced.ttf differ
diff --git a/font2img/parse_font.go b/font2img/parse_font.go
new file mode 100644
index 0000000..d6efba2
--- /dev/null
+++ b/font2img/parse_font.go
@@ -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 := ``
+
+ 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)
+ }
+ }
+}
diff --git a/go.mod b/go.mod
index 259920e..c5c3a9b 100644
--- a/go.mod
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..57d4894
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..37b131a
--- /dev/null
+++ b/main.go
@@ -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")
+}