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)
|
|||
|
}
|