package services import ( "bytes" "encoding/base64" "fmt" "image" "image/color" "image/png" "math" "os" "path/filepath" "strings" "github.com/disintegration/imaging" _ "golang.org/x/image/webp" ) // supportedFormats lists formats that imaging.Open() can handle var supportedFormats = map[string]bool{ ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".bmp": true, ".tiff": true, ".tif": true, ".webp": true, } func (s *ImageService) checkFormat(filePath string) error { ext := strings.ToLower(filepath.Ext(filePath)) if ext == ".ico" { return fmt.Errorf("ICO 格式暂不支持,请先转换为 PNG 或 JPG 格式") } if !supportedFormats[ext] { return fmt.Errorf("不支持的图片格式: %s", ext) } return nil } type ImageService struct{} func NewImageService() *ImageService { return &ImageService{} } type ImageInfo struct { Width int `json:"width"` Height int `json:"height"` Format string `json:"format"` FilePath string `json:"filePath"` Size int64 `json:"size"` } func (s *ImageService) GetImageInfo(filePath string) (*ImageInfo, error) { file, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("打开图片文件失败: %v", err) } defer file.Close() config, format, err := image.DecodeConfig(file) if err != nil { return nil, fmt.Errorf("解码图片配置失败: %v", err) } fileInfo, err := os.Stat(filePath) if err != nil { return nil, fmt.Errorf("获取文件信息失败: %v", err) } return &ImageInfo{ Width: config.Width, Height: config.Height, Format: format, FilePath: filePath, Size: fileInfo.Size(), }, nil } func (s *ImageService) GetImageBase64(filePath string) (string, error) { data, err := os.ReadFile(filePath) if err != nil { return "", err } ext := strings.ToLower(filepath.Ext(filePath)) mimeType := "image/png" switch ext { case ".jpg", ".jpeg": mimeType = "image/jpeg" case ".gif": mimeType = "image/gif" case ".bmp": mimeType = "image/bmp" case ".webp": mimeType = "image/webp" case ".ico": mimeType = "image/x-icon" } return "data:" + mimeType + ";base64," + base64.StdEncoding.EncodeToString(data), nil } func (s *ImageService) ConvertFormat(inputPath, outputPath, format string, quality int) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = ChangeExtension(inputPath, "."+format) } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } switch strings.ToLower(format) { case "jpeg", "jpg": if quality <= 0 || quality > 100 { quality = 85 } err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality)) case "png": err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression)) case "gif": err = imaging.Save(img, outputPath, imaging.GIFNumColors(256)) case "bmp": err = imaging.Save(img, outputPath) case "tiff": err = imaging.Save(img, outputPath) case "ico": err = s.ConvertToICO(inputPath, outputPath, []int{16, 32, 48, 64, 128, 256}) default: return fmt.Errorf("不支持的图片格式: %s", format) } if err != nil { return fmt.Errorf("保存图片失败: %v", err) } return nil } func (s *ImageService) ConvertToICO(inputPath, outputPath string, sizes []int) error { if outputPath == "" { outputPath = ChangeExtension(inputPath, ".ico") } srcImg, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } var images []image.Image for _, size := range sizes { resized := imaging.Resize(srcImg, size, size, imaging.Lanczos) images = append(images, resized) } return encodeICO(outputPath, images) } func encodeICO(outputPath string, images []image.Image) error { f, err := os.Create(outputPath) if err != nil { return err } defer f.Close() var pngDataBuffers [][]byte for _, img := range images { buf := new(bytes.Buffer) if err := png.Encode(buf, img); err != nil { return err } pngDataBuffers = append(pngDataBuffers, buf.Bytes()) } header := make([]byte, 6) header[0] = 0 header[1] = 0 header[2] = 1 header[3] = 0 count := uint16(len(images)) header[4] = byte(count) header[5] = byte(count >> 8) if _, err := f.Write(header); err != nil { return err } offset := uint32(6 + len(images)*16) for i, img := range images { bounds := img.Bounds() w := bounds.Dx() h := bounds.Dy() entry := make([]byte, 16) if w > 255 { w = 0 } if h > 255 { h = 0 } entry[0] = byte(w) entry[1] = byte(h) entry[2] = 0 entry[3] = 0 entry[4] = 1 entry[5] = 0 entry[6] = 32 entry[7] = 0 dataSize := uint32(len(pngDataBuffers[i])) entry[8] = byte(dataSize) entry[9] = byte(dataSize >> 8) entry[10] = byte(dataSize >> 16) entry[11] = byte(dataSize >> 24) entry[12] = byte(offset) entry[13] = byte(offset >> 8) entry[14] = byte(offset >> 16) entry[15] = byte(offset >> 24) if _, err := f.Write(entry); err != nil { return err } offset += uint32(len(pngDataBuffers[i])) _ = i } for _, data := range pngDataBuffers { if _, err := f.Write(data); err != nil { return err } } return nil } func (s *ImageService) CompressImage(inputPath, outputPath string, quality int, maxWidth, maxHeight int) error { if err := s.checkFormat(inputPath); err != nil { return err } if quality < 1 { quality = 1 } if quality > 100 { quality = 100 } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_compressed") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } if maxWidth > 0 || maxHeight > 0 { bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() newWidth := width newHeight := height if maxWidth > 0 && width > maxWidth { newWidth = maxWidth newHeight = height * maxWidth / width } if maxHeight > 0 && newHeight > maxHeight { newWidth = newWidth * maxHeight / newHeight newHeight = maxHeight } img = imaging.Resize(img, newWidth, newHeight, imaging.Lanczos) } ext := strings.ToLower(filepath.Ext(outputPath)) switch ext { case ".jpg", ".jpeg": err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality)) case ".png": err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression)) case ".gif": err = imaging.Save(img, outputPath, imaging.GIFNumColors(256)) default: err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality)) } if err != nil { return fmt.Errorf("保存压缩图片失败: %v", err) } return nil } func (s *ImageService) ResizeImage(inputPath, outputPath string, width, height int, maintainRatio bool) error { if err := s.checkFormat(inputPath); err != nil { return err } if width <= 0 || height <= 0 { return fmt.Errorf("宽度和高度必须大于 0") } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_resized") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } if maintainRatio { img = imaging.Fit(img, width, height, imaging.Lanczos) } else { img = imaging.Resize(img, width, height, imaging.Lanczos) } err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存调整大小后的图片失败: %v", err) } return nil } func (s *ImageService) RotateImage(inputPath, outputPath string, angle float64, bgColor string) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_rotated") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } var fillColor color.Color switch bgColor { case "white": fillColor = color.White case "black": fillColor = color.Black default: fillColor = color.Transparent } img = imaging.Rotate(img, angle, fillColor) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存旋转后的图片失败: %v", err) } return nil } func (s *ImageService) AddGrayscale(inputPath, outputPath string) error { return s.AdjustGrayscale(inputPath, outputPath, 100) } func (s *ImageService) AdjustGrayscale(inputPath, outputPath string, intensity int) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_grayscale") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } if intensity >= 100 { img = imaging.Grayscale(img) } else if intensity > 0 { gray := imaging.Grayscale(img) img = blendImages(img, gray, float64(intensity)/100.0) } err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存灰度图片失败: %v", err) } return nil } func blendImages(src, gray image.Image, opacity float64) image.Image { if opacity < 0 { opacity = 0 } if opacity > 1 { opacity = 1 } bounds := src.Bounds() dst := image.NewRGBA(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { r1, g1, b1, a1 := src.At(x, y).RGBA() r2, g2, b2, _ := gray.At(x, y).RGBA() r := uint8((float64(r1>>8)*(1-opacity) + float64(r2>>8)*opacity)) g := uint8((float64(g1>>8)*(1-opacity) + float64(g2>>8)*opacity)) b := uint8((float64(b1>>8)*(1-opacity) + float64(b2>>8)*opacity)) a := uint8(a1 >> 8) dst.Set(x, y, color.RGBA{r, g, b, a}) } } return dst } func (s *ImageService) AdjustBrightness(inputPath, outputPath string, factor float64) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_bright") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.AdjustBrightness(img, factor) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存调整亮度后的图片失败: %v", err) } return nil } func (s *ImageService) AdjustContrast(inputPath, outputPath string, factor float64) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_contrast") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.AdjustContrast(img, factor) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存调整对比度后的图片失败: %v", err) } return nil } func (s *ImageService) AdjustSaturation(inputPath, outputPath string, factor float64) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_saturation") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.AdjustSaturation(img, factor) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存调整饱和度后的图片失败: %v", err) } return nil } func (s *ImageService) FlipH(inputPath, outputPath string) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_fliph") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.FlipH(img) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存水平翻转图片失败: %v", err) } return nil } func (s *ImageService) FlipV(inputPath, outputPath string) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_flipv") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.FlipV(img) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存垂直翻转图片失败: %v", err) } return nil } func (s *ImageService) SharpenImage(inputPath, outputPath string, amount float64) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_sharpen") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.Sharpen(img, amount) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存锐化图片失败: %v", err) } return nil } func (s *ImageService) BlurImage(inputPath, outputPath string, radius float64) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_blur") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.Blur(img, radius) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存模糊图片失败: %v", err) } return nil } func (s *ImageService) InvertImage(inputPath, outputPath string) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_invert") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } img = imaging.Invert(img) err = imaging.Save(img, outputPath) if err != nil { return fmt.Errorf("保存反色图片失败: %v", err) } return nil } func (s *ImageService) RemoveBackground(inputPath, outputPath string, threshold int, bgColor *ColorRGB) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_nobg") } if threshold <= 0 { threshold = 30 } srcImg, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } bounds := srcImg.Bounds() dst := image.NewRGBA(bounds) maxDist := float64(threshold) / 100.0 * 441.67 for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { r, g, b, _ := srcImg.At(x, y).RGBA() r8 := uint8(r >> 8) g8 := uint8(g >> 8) b8 := uint8(b >> 8) var isBg bool if bgColor != nil { dr := float64(int(r8) - bgColor.R) dg := float64(int(g8) - bgColor.G) db := float64(int(b8) - bgColor.B) dist := math.Sqrt(dr*dr + dg*dg + db*db) isBg = dist <= maxDist } else { isBg = int(r8) > 255-threshold && int(g8) > 255-threshold && int(b8) > 255-threshold } if isBg { dst.Set(x, y, color.RGBA{0, 0, 0, 0}) } else { dst.Set(x, y, srcImg.At(x, y)) } } } err = imaging.Save(dst, outputPath, imaging.PNGCompressionLevel(png.BestCompression)) if err != nil { return fmt.Errorf("保存去背景图片失败: %v", err) } return nil } type CropShape struct { Type string `json:"type"` X int `json:"x"` Y int `json:"y"` Width int `json:"width"` Height int `json:"height"` Radius int `json:"radius"` Points []Point `json:"points"` } type Point struct { X int `json:"x"` Y int `json:"y"` } type ColorRGB struct { R int `json:"r"` G int `json:"g"` B int `json:"b"` } func (s *ImageService) CropImageShape(inputPath, outputPath string, shape CropShape) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = GetOutputPath(inputPath, "_cropped") } srcImg, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } switch shape.Type { case "rectangle": rect := image.Rect(shape.X, shape.Y, shape.X+shape.Width, shape.Y+shape.Height) cropped := imaging.Crop(srcImg, rect) return imaging.Save(cropped, outputPath) case "circle": cx := shape.X + shape.Width/2 cy := shape.Y + shape.Height/2 radius := shape.Width / 2 if shape.Height/2 < radius { radius = shape.Height / 2 } size := radius * 2 result := image.NewRGBA(image.Rect(0, 0, size, size)) for y := 0; y < size; y++ { for x := 0; x < size; x++ { if math.Sqrt(float64((x-radius)*(x-radius)+((y-radius)*(y-radius)))) <= float64(radius) { result.Set(x, y, srcImg.At(cx-radius+x, cy-radius+y)) } } } return imaging.Save(result, outputPath) case "rounded": rect := image.Rect(shape.X, shape.Y, shape.X+shape.Width, shape.Y+shape.Height) cropped := imaging.Crop(srcImg, rect) radius := shape.Radius if radius <= 0 { radius = 20 } size := cropped.Bounds().Size() result := image.NewRGBA(image.Rect(0, 0, size.X, size.Y)) for y := 0; y < size.Y; y++ { for x := 0; x < size.X; x++ { inCorner := false if x < radius && y < radius { dx := float64(radius - x) dy := float64(radius - y) if math.Sqrt(dx*dx+dy*dy) > float64(radius) { inCorner = true } } else if x >= size.X-radius && y < radius { dx := float64(x - (size.X - radius - 1)) dy := float64(radius - y) if math.Sqrt(dx*dx+dy*dy) > float64(radius) { inCorner = true } } else if x < radius && y >= size.Y-radius { dx := float64(radius - x) dy := float64(y - (size.Y - radius - 1)) if math.Sqrt(dx*dx+dy*dy) > float64(radius) { inCorner = true } } else if x >= size.X-radius && y >= size.Y-radius { dx := float64(x - (size.X - radius - 1)) dy := float64(y - (size.Y - radius - 1)) if math.Sqrt(dx*dx+dy*dy) > float64(radius) { inCorner = true } } if !inCorner { result.Set(x, y, cropped.At(cropped.Bounds().Min.X+x, cropped.Bounds().Min.Y+y)) } } } return imaging.Save(result, outputPath) case "polygon": if len(shape.Points) < 3 { return fmt.Errorf("多边形至少需要3个点") } minX, minY := shape.Points[0].X, shape.Points[0].Y maxX, maxY := shape.Points[0].X, shape.Points[0].Y for _, p := range shape.Points { if p.X < minX { minX = p.X } if p.Y < minY { minY = p.Y } if p.X > maxX { maxX = p.X } if p.Y > maxY { maxY = p.Y } } cropW := maxX - minX cropH := maxY - minY rect := image.Rect(minX, minY, maxX, maxY) cropped := imaging.Crop(srcImg, rect) result := image.NewRGBA(image.Rect(0, 0, cropW, cropH)) for y := 0; y < cropH; y++ { for x := 0; x < cropW; x++ { if pointInPolygon(x, y, shape.Points, minX, minY) { result.Set(x, y, cropped.At(cropped.Bounds().Min.X+x, cropped.Bounds().Min.Y+y)) } } } return imaging.Save(result, outputPath) default: rect := image.Rect(shape.X, shape.Y, shape.X+shape.Width, shape.Y+shape.Height) cropped := imaging.Crop(srcImg, rect) return imaging.Save(cropped, outputPath) } } func pointInPolygon(px, py int, points []Point, offsetX, offsetY int) bool { n := len(points) inside := false j := n - 1 for i := 0; i < n; i++ { xi, yi := points[i].X-offsetX, points[i].Y-offsetY xj, yj := points[j].X-offsetX, points[j].Y-offsetY if yi == yj { j = i continue } if ((yi > py) != (yj > py)) && (px < (xj-xi)*(py-yi)/(yj-yi)+xi) { inside = !inside } j = i } return inside }