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