img/img.go

333 lines
8.2 KiB
Go
Raw Permalink Normal View History

2025-07-29 16:59:49 +08:00
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)
}