imagedd/text2img/parse_font.go

887 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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