feat: 添加图像处理功能,包含精灵管理、文本渲染和SVG字体支持,更新README文档以描述新功能,添加.gitignore文件以排除不必要的文件。
This commit is contained in:
parent
1346911f86
commit
2c2f855198
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
test.png
|
||||||
|
face.png
|
||||||
|
.vscode/launch.json
|
188
README.md
188
README.md
@ -1,3 +1,189 @@
|
|||||||
# imagedd
|
# 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")
|
||||||
|
}
|
||||||
|
```
|
13046
font2img/NotoEmoji-VariableFont_wght.svg
Normal file
13046
font2img/NotoEmoji-VariableFont_wght.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 2.6 MiB |
BIN
font2img/fusion-pixel-12px-monospaced.ttf
Normal file
BIN
font2img/fusion-pixel-12px-monospaced.ttf
Normal file
Binary file not shown.
886
font2img/parse_font.go
Normal file
886
font2img/parse_font.go
Normal 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
11
go.mod
@ -1,3 +1,14 @@
|
|||||||
module git.lxtend.com/lixiangwuxian/imagedd
|
module git.lxtend.com/lixiangwuxian/imagedd
|
||||||
|
|
||||||
go 1.24.2
|
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
12
go.sum
Normal 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
121
main.go
Normal 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")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user