img/img.go
2025-07-29 16:59:49 +08:00

333 lines
8.2 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 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)
}