2026-05-12 13:21:50 +08:00
|
|
|
|
package vision
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-13 00:47:14 +08:00
|
|
|
|
"bytes"
|
2026-05-12 13:21:50 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"image"
|
|
|
|
|
|
"image/color"
|
|
|
|
|
|
"image/draw"
|
|
|
|
|
|
"image/jpeg"
|
|
|
|
|
|
_ "image/png"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/file"
|
|
|
|
|
|
"github.com/fogleman/gg"
|
|
|
|
|
|
"golang.org/x/image/font"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Canvas 代表一个绘图画布,封装了图像处理与绘制能力
|
|
|
|
|
|
type Canvas struct {
|
|
|
|
|
|
dc *gg.Context
|
|
|
|
|
|
bgColor string
|
|
|
|
|
|
lastColor string
|
|
|
|
|
|
lastFont font.Face
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// New 创建一个新的画布
|
|
|
|
|
|
func New(width, height int, backgroundColor ...string) *Canvas {
|
|
|
|
|
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
|
|
|
|
dc := gg.NewContextForImage(img)
|
|
|
|
|
|
c := &Canvas{
|
|
|
|
|
|
dc: dc,
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(backgroundColor) > 0 && backgroundColor[0] != "" {
|
|
|
|
|
|
c.bgColor = backgroundColor[0]
|
|
|
|
|
|
c.SetColor(c.bgColor)
|
|
|
|
|
|
c.dc.Clear()
|
|
|
|
|
|
}
|
|
|
|
|
|
return c
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Load 从文件加载图像并创建画布
|
|
|
|
|
|
func Load(path string) (*Canvas, error) {
|
2026-05-13 00:47:14 +08:00
|
|
|
|
if path == "" {
|
|
|
|
|
|
return nil, fmt.Errorf("path is empty")
|
|
|
|
|
|
}
|
2026-05-12 13:21:50 +08:00
|
|
|
|
if !file.Exists(path) {
|
|
|
|
|
|
return nil, fmt.Errorf("file not found: %s", path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 00:47:14 +08:00
|
|
|
|
data, err := file.ReadBytes(path)
|
2026-05-12 13:21:50 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 00:47:14 +08:00
|
|
|
|
img, _, err := image.Decode(bytes.NewReader(data))
|
2026-05-12 13:21:50 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("decode image failed: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return &Canvas{
|
|
|
|
|
|
dc: gg.NewContextForImage(img),
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Save 将画布保存到文件
|
|
|
|
|
|
func Save(c *Canvas, path string, quality ...int) error {
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
|
|
|
|
if strings.HasSuffix(strings.ToLower(path), ".jpg") || strings.HasSuffix(strings.ToLower(path), ".jpeg") {
|
|
|
|
|
|
q := 85
|
|
|
|
|
|
if len(quality) > 0 {
|
|
|
|
|
|
q = quality[0]
|
|
|
|
|
|
}
|
|
|
|
|
|
// gg 没有内置 SaveJPG 到 context,我们需要手动编码
|
2026-05-13 00:47:14 +08:00
|
|
|
|
f, createErr := file.Create(path)
|
2026-05-12 13:21:50 +08:00
|
|
|
|
if createErr != nil {
|
|
|
|
|
|
return createErr
|
|
|
|
|
|
}
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
err = jpeg.Encode(f, c.dc.Image(), &jpeg.Options{Quality: q})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
err = c.dc.SavePNG(path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return fmt.Errorf("save image failed: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Image 返回底层图像
|
|
|
|
|
|
func (c *Canvas) Image() image.Image {
|
|
|
|
|
|
return c.dc.Image()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Width 返回画布宽度
|
|
|
|
|
|
func (c *Canvas) Width() int {
|
|
|
|
|
|
return c.dc.Width()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Height 返回画布高度
|
|
|
|
|
|
func (c *Canvas) Height() int {
|
|
|
|
|
|
return c.dc.Height()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SetColor 设置当前绘图颜色 (支持 hex 格式)
|
|
|
|
|
|
func (c *Canvas) SetColor(hex string) {
|
|
|
|
|
|
c.lastColor = hex
|
|
|
|
|
|
c.dc.SetColor(ParseColor(hex))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clear 清除指定区域,如果设置了背景色则填充背景色,否则填充透明
|
|
|
|
|
|
func (c *Canvas) Clear(x, y, w, h int) {
|
|
|
|
|
|
if c.bgColor != "" {
|
|
|
|
|
|
c.dc.Push()
|
|
|
|
|
|
c.dc.DrawRectangle(float64(x), float64(y), float64(w), float64(h))
|
|
|
|
|
|
c.dc.SetColor(ParseColor(c.bgColor))
|
|
|
|
|
|
c.dc.Fill()
|
|
|
|
|
|
c.dc.Pop()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if img, ok := c.dc.Image().(*image.RGBA); ok {
|
|
|
|
|
|
transparent := image.NewUniform(color.RGBA{0, 0, 0, 0})
|
|
|
|
|
|
draw.Draw(img, image.Rect(x, y, x+w, y+h), transparent, image.Point{}, draw.Src)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sub 提取子区域并返回新画布
|
|
|
|
|
|
func (c *Canvas) Sub(x, y, w, h int) *Canvas {
|
|
|
|
|
|
newImg := image.NewRGBA(image.Rect(0, 0, w, h))
|
|
|
|
|
|
draw.Draw(newImg, newImg.Bounds(), c.dc.Image(), image.Pt(x, y), draw.Src)
|
|
|
|
|
|
newDC := gg.NewContextForImage(newImg)
|
|
|
|
|
|
return &Canvas{
|
|
|
|
|
|
dc: newDC,
|
|
|
|
|
|
bgColor: c.bgColor,
|
|
|
|
|
|
lastColor: c.lastColor,
|
|
|
|
|
|
lastFont: c.lastFont,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clone 克隆当前画布
|
|
|
|
|
|
func (c *Canvas) Clone() *Canvas {
|
|
|
|
|
|
bounds := c.dc.Image().Bounds()
|
|
|
|
|
|
newImg := image.NewRGBA(bounds)
|
|
|
|
|
|
draw.Draw(newImg, bounds, c.dc.Image(), bounds.Min, draw.Src)
|
|
|
|
|
|
return &Canvas{
|
|
|
|
|
|
dc: gg.NewContextForImage(newImg),
|
|
|
|
|
|
bgColor: c.bgColor,
|
|
|
|
|
|
lastColor: c.lastColor,
|
|
|
|
|
|
lastFont: c.lastFont,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|