333 lines
8.2 KiB
Go
333 lines
8.2 KiB
Go
package img
|
||
|
||
import (
|
||
"fmt"
|
||
"image"
|
||
"image/color"
|
||
"image/draw"
|
||
"image/jpeg"
|
||
"image/png"
|
||
"math"
|
||
"math/rand"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"apigo.cc/gojs"
|
||
"github.com/disintegration/imaging"
|
||
"github.com/fogleman/gg"
|
||
"golang.org/x/image/font"
|
||
)
|
||
|
||
type Graphics struct {
|
||
// img image.Image
|
||
bgColor string
|
||
lastColor string
|
||
lastFont font.Face
|
||
dc *gg.Context // 高级绘图上下文
|
||
}
|
||
|
||
func init() {
|
||
obj := map[string]any{
|
||
"createImage": CreateImage,
|
||
"loadImage": LoadImage,
|
||
"loadFont": LoadFont,
|
||
"listFont": ListFont,
|
||
"randColor": RandColor,
|
||
"lineCap": lineCap,
|
||
"lineJoin": lineJoin,
|
||
"fillRule": fillRule,
|
||
}
|
||
gojs.Register("apigo.cc/gojs/img", gojs.Module{
|
||
Object: gojs.ToMap(obj),
|
||
TsCode: gojs.MakeTSCode(obj),
|
||
})
|
||
}
|
||
|
||
// CreateImage 创建一个新的图像
|
||
func CreateImage(width, height int, c *string) *Graphics {
|
||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
dc := gg.NewContextForImage(img)
|
||
g := &Graphics{
|
||
// img: dc.Image(),
|
||
dc: dc,
|
||
}
|
||
if c != nil {
|
||
g.bgColor = *c
|
||
g.SetColor(*c)
|
||
g.dc.Clear()
|
||
}
|
||
return g
|
||
}
|
||
|
||
// LoadImage 从文件加载图像
|
||
func LoadImage(filePath string) (*Graphics, error) {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer file.Close()
|
||
|
||
img, _, err := image.Decode(file)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &Graphics{
|
||
// img: img,
|
||
dc: gg.NewContextForImage(img),
|
||
}, nil
|
||
}
|
||
|
||
func (g *Graphics) Clear(x, y, width, height int) {
|
||
if g.bgColor != "" {
|
||
g.dc.DrawRectangle(float64(x), float64(y), float64(width), float64(height))
|
||
g.dc.SetColor(hexToRGBA(g.bgColor))
|
||
g.dc.Fill()
|
||
return
|
||
}
|
||
if img, ok := g.dc.Image().(*image.RGBA); ok {
|
||
transparent := image.NewUniform(color.RGBA{0, 0, 0, 0})
|
||
draw.Draw(img, image.Rect(x, y, x+width, y+height), transparent, image.Point{}, draw.Src)
|
||
}
|
||
}
|
||
|
||
// SubImage 提取指定区域的子图
|
||
func (g *Graphics) Sub(x, y, width, height int) *Graphics {
|
||
// bounds := image.Rect(x, y, x+width, y+height)
|
||
newImg := image.NewRGBA(image.Rect(0, 0, width, height))
|
||
draw.Draw(newImg, newImg.Bounds(), g.dc.Image(), image.Pt(x, y), draw.Src)
|
||
newDC := gg.NewContextForImage(newImg)
|
||
return &Graphics{
|
||
// img: newDC.Image(),
|
||
dc: newDC,
|
||
bgColor: g.bgColor,
|
||
lastColor: g.lastColor,
|
||
lastFont: g.lastFont,
|
||
}
|
||
}
|
||
|
||
// Clone 创建图像的深拷贝
|
||
func (g *Graphics) Clone() *Graphics {
|
||
bounds := g.dc.Image().Bounds()
|
||
newImg := image.NewRGBA(bounds)
|
||
draw.Draw(newImg, bounds, g.dc.Image(), bounds.Min, draw.Src)
|
||
|
||
return &Graphics{
|
||
// img: newImg,
|
||
dc: gg.NewContextForImage(newImg),
|
||
}
|
||
}
|
||
|
||
func (g *Graphics) SetColor(c string) {
|
||
g.lastColor = c
|
||
g.dc.SetColor(hexToRGBA(c))
|
||
}
|
||
|
||
// // Image 绘制图片(支持多种布局模式)
|
||
// func (g *Graphics) Image(src *Graphics, x, y, width, height int, mode string) {
|
||
// if src == nil {
|
||
// return
|
||
// }
|
||
|
||
// srcBounds := src.img.Bounds()
|
||
// srcW := srcBounds.Dx()
|
||
// srcH := srcBounds.Dy()
|
||
|
||
// // 计算目标尺寸
|
||
// targetW := width
|
||
// targetH := height
|
||
|
||
// // 根据不同模式计算实际绘制区域
|
||
// var drawW, drawH int
|
||
// switch mode {
|
||
// case scaleMode.Fill:
|
||
// drawW = targetW
|
||
// drawH = targetH
|
||
// case scaleMode.Contain:
|
||
// ratio := math.Min(float64(targetW)/float64(srcW), float64(targetH)/float64(srcH))
|
||
// drawW = int(float64(srcW) * ratio)
|
||
// drawH = int(float64(srcH) * ratio)
|
||
// case scaleMode.Cover:
|
||
// ratio := math.Max(float64(targetW)/float64(srcW), float64(targetH)/float64(srcH))
|
||
// drawW = int(float64(srcW) * ratio)
|
||
// drawH = int(float64(srcH) * ratio)
|
||
// }
|
||
|
||
// // 计算居中位置
|
||
// offsetX := (targetW - drawW) / 2
|
||
// offsetY := (targetH - drawH) / 2
|
||
|
||
// // 缩放图片
|
||
// scaled := imaging.Resize(src.img, drawW, drawH, imaging.Lanczos)
|
||
|
||
// // 绘制图片
|
||
// g.dc.DrawImage(scaled, x+offsetX, y+offsetY)
|
||
// g.img = g.dc.Image()
|
||
// }
|
||
|
||
// Put 简单的把图原样贴上去
|
||
func (g *Graphics) Put(src *Graphics, x, y int) {
|
||
if src == nil || src.dc == nil {
|
||
return
|
||
}
|
||
// 直接绘制源图像,不进行缩放
|
||
g.dc.DrawImage(src.dc.Image(), x, y)
|
||
// g.img = g.dc.Image() // 更新主图像
|
||
}
|
||
|
||
// PutTo 将原图整张贴进去,根据mode处理尺寸
|
||
func (g *Graphics) PutTo(src *Graphics, dx, dy, dw, dh int, sizeMode string) {
|
||
// 调用完整功能函数,源区域是整个源图像
|
||
srcBounds := src.dc.Image().Bounds()
|
||
g.PutBy(src, srcBounds.Min.X, srcBounds.Min.Y, srcBounds.Dx(), srcBounds.Dy(), dx, dy, dw, dh, sizeMode)
|
||
}
|
||
|
||
// PutBy 完整的贴图功能
|
||
func (g *Graphics) PutBy(src *Graphics, sx, sy, sw, sh, dx, dy, dw, dh int, sizeMode string) {
|
||
// 基础检查
|
||
if src == nil || src.dc == nil || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 {
|
||
return
|
||
}
|
||
|
||
// 1. 裁剪源图像到指定区域
|
||
srcBounds := src.dc.Image().Bounds()
|
||
cropX := max(sx, srcBounds.Min.X)
|
||
cropY := max(sy, srcBounds.Min.Y)
|
||
cropW := min(sw, srcBounds.Dx())
|
||
cropH := min(sh, srcBounds.Dy())
|
||
|
||
cropped := imaging.Crop(src.dc.Image(), image.Rect(cropX, cropY, cropX+cropW, cropY+cropH))
|
||
|
||
// 2. 根据模式计算缩放尺寸
|
||
var (
|
||
drawW, drawH int
|
||
ratio float64
|
||
)
|
||
|
||
switch sizeMode {
|
||
case "contain":
|
||
ratio = math.Min(float64(dw)/float64(cropW), float64(dh)/float64(cropH))
|
||
drawW = int(float64(cropW) * ratio)
|
||
drawH = int(float64(cropH) * ratio)
|
||
case "cover":
|
||
ratio = math.Max(float64(dw)/float64(cropW), float64(dh)/float64(cropH))
|
||
drawW = int(float64(cropW) * ratio)
|
||
drawH = int(float64(cropH) * ratio)
|
||
case "fill":
|
||
drawW = dw
|
||
drawH = dh
|
||
}
|
||
|
||
// 3. 居中偏移计算
|
||
offsetX := (dw - drawW) / 2
|
||
offsetY := (dh - drawH) / 2
|
||
|
||
// 4. 缩放图像
|
||
scaled := imaging.Resize(cropped, drawW, drawH, imaging.Lanczos)
|
||
|
||
// 5. 绘制到目标位置
|
||
g.dc.DrawImage(scaled, dx+offsetX, dy+offsetY)
|
||
// g.img = g.dc.Image()
|
||
}
|
||
|
||
// FillAreaWithImage 用图像填充目标区域
|
||
func (g *Graphics) FillAreaWithImage(src *Graphics, x, y, width, height int) {
|
||
if src == nil {
|
||
return
|
||
}
|
||
|
||
srcBounds := src.dc.Image().Bounds()
|
||
srcW := srcBounds.Dx()
|
||
srcH := srcBounds.Dy()
|
||
|
||
// 计算需要复制的次数
|
||
cols := (width + srcW - 1) / srcW
|
||
rows := (height + srcH - 1) / srcH
|
||
|
||
for row := 0; row < rows; row++ {
|
||
for col := 0; col < cols; col++ {
|
||
px := x + col*srcW
|
||
py := y + row*srcH
|
||
g.dc.DrawImage(src.dc.Image(), px, py)
|
||
}
|
||
}
|
||
// g.img = g.dc.Image()
|
||
}
|
||
|
||
// ExportImage 导出图像到文件
|
||
func (g *Graphics) ExportImage(filePath string, quality *int) error {
|
||
file, err := os.Create(filePath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer file.Close()
|
||
|
||
switch {
|
||
case strings.HasSuffix(strings.ToLower(filePath), ".png"):
|
||
return png.Encode(file, g.dc.Image())
|
||
case strings.HasSuffix(strings.ToLower(filePath), ".jpg"),
|
||
strings.HasSuffix(strings.ToLower(filePath), ".jpeg"):
|
||
q := 85 // 默认质量
|
||
if quality != nil {
|
||
q = *quality
|
||
if q < 0 {
|
||
q = 0
|
||
} else if q > 100 {
|
||
q = 100
|
||
}
|
||
}
|
||
return jpeg.Encode(file, g.dc.Image(), &jpeg.Options{Quality: q})
|
||
default:
|
||
return fmt.Errorf("unsupported file format: %s", filePath)
|
||
}
|
||
}
|
||
|
||
// GetImageData 获取图像原始数据
|
||
// func (g *Graphics) GetImageData() image.Image {
|
||
// return g.img
|
||
// }
|
||
|
||
// hexToRGBA 将多种格式的十六进制颜色字符串转换为 color.RGBA
|
||
// 支持格式: #RRGGBB, #RRGGBBAA, #RGB, #RGBA
|
||
func hexToRGBA(hex string) color.Color {
|
||
// 去除开头的 # 号并转为大写
|
||
hex = strings.ToUpper(strings.TrimPrefix(hex, "#"))
|
||
|
||
// 验证合法字符
|
||
for _, ch := range hex {
|
||
if !((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'F')) {
|
||
return color.RGBA{}
|
||
}
|
||
}
|
||
|
||
switch len(hex) {
|
||
case 3:
|
||
hex = fmt.Sprintf("%c%c%c%c%c%c", hex[0], hex[0], hex[1], hex[1], hex[2], hex[2])
|
||
case 4:
|
||
hex = fmt.Sprintf("%c%c%c%c%c%c%c%c", hex[0], hex[0], hex[1], hex[1], hex[2], hex[2], hex[3], hex[3])
|
||
}
|
||
|
||
switch len(hex) {
|
||
case 6: // #RRGGBB
|
||
return color.RGBA{R: parseHex(hex[0:2]), G: parseHex(hex[2:4]), B: parseHex(hex[4:6]), A: 255}
|
||
case 8: // #RRGGBBAA
|
||
return color.RGBA{R: parseHex(hex[0:2]), G: parseHex(hex[2:4]), B: parseHex(hex[4:6]), A: parseHex(hex[6:8])}
|
||
}
|
||
return color.RGBA{}
|
||
}
|
||
|
||
func parseHex(s string) uint8 {
|
||
val, _ := strconv.ParseUint(s, 16, 8)
|
||
return uint8(val)
|
||
}
|
||
|
||
// 辅助函数:生成对比色
|
||
func RandColor() string {
|
||
r := uint8(rand.Intn(150) + 50)
|
||
gr := uint8(rand.Intn(150) + 50)
|
||
b := uint8(rand.Intn(150) + 50)
|
||
a := uint8(rand.Intn(150) + 105)
|
||
return fmt.Sprintf("#%02X%02X%02X%02X", r, gr, b, a)
|
||
}
|