package services import ( "bytes" "encoding/base64" "fmt" "image" "image/color" "image/jpeg" "image/png" "math" "os" "path/filepath" "strconv" "strings" "github.com/disintegration/imaging" "github.com/signintech/gopdf" _ "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) } outExt := strings.ToLower(filepath.Ext(outputPath)) switch { case outExt == ".jpg" || outExt == ".jpeg" || format == "jpeg" || format == "jpg": if quality <= 0 || quality > 100 { quality = 85 } err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality)) case outExt == ".png" || format == "png": err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression)) case outExt == ".gif" || format == "gif": err = imaging.Save(img, outputPath, imaging.GIFNumColors(256)) case outExt == ".bmp" || format == "bmp": err = imaging.Save(img, outputPath) case outExt == ".tiff" || outExt == ".tif" || format == "tiff" || format == "tif": err = imaging.Save(img, outputPath) case format == "ico" || outExt == ".ico": err = s.ConvertToICO(inputPath, outputPath, []int{16, 32, 48, 64, 128, 256}) default: if quality <= 0 || quality > 100 { quality = 85 } err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality)) } 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") } // 记录原始文件大小 originalInfo, err := os.Stat(inputPath) if err != nil { return fmt.Errorf("获取原文件信息失败: %v", err) } originalSize := originalInfo.Size() img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("打开图片失败: %v", err) } // 智能降采样:超大图片自动缩小到合理尺寸 bounds := img.Bounds() width := bounds.Dx() height := bounds.Dy() // 如果未指定尺寸,对超大图片自动降采样 if maxWidth == 0 && maxHeight == 0 { maxDim := 4000 // 超过4000px视为超大 if width > maxDim || height > maxDim { ratio := float64(maxDim) / float64(max(width, height)) maxWidth = int(float64(width) * ratio) maxHeight = int(float64(height) * ratio) } } if maxWidth > 0 || maxHeight > 0 { newWidth, newHeight := calculateResize(width, height, maxWidth, maxHeight) if newWidth < width || newHeight < height { img = imaging.Resize(img, newWidth, newHeight, imaging.Lanczos) } } ext := strings.ToLower(filepath.Ext(outputPath)) switch ext { case ".jpg", ".jpeg": err = s.saveOptimizedJPEG(img, outputPath, quality) case ".png": err = s.saveOptimizedPNG(img, outputPath, quality) case ".gif": err = imaging.Save(img, outputPath, imaging.GIFNumColors(256)) case ".bmp": err = imaging.Save(img, outputPath) case ".tiff", ".tif": err = imaging.Save(img, outputPath) default: err = s.saveOptimizedJPEG(img, outputPath, quality) } if err != nil { return fmt.Errorf("保存压缩图片失败: %v", err) } // 检查压缩后的文件大小 compressedInfo, err := os.Stat(outputPath) if err != nil { return fmt.Errorf("获取压缩文件信息失败: %v", err) } compressedSize := compressedInfo.Size() // 如果压缩后文件变大,删除压缩文件并返回提示 if compressedSize >= originalSize { os.Remove(outputPath) return fmt.Errorf("压缩后文件变大(%s → %s),建议降低质量参数或选择JPEG格式", formatSize(originalSize), formatSize(compressedSize)) } 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 } // calculateResize 计算保持宽高比的resize尺寸 func calculateResize(origW, origH, maxW, maxH int) (int, int) { if maxW == 0 && maxH == 0 { return origW, origH } ratioW := float64(origW) / float64(maxW) ratioH := float64(origH) / float64(maxH) var ratio float64 if maxW == 0 { ratio = ratioH } else if maxH == 0 { ratio = ratioW } else { ratio = math.Max(ratioW, ratioH) } if ratio <= 1 { return origW, origH // 无需缩小 } newW := int(float64(origW) / ratio) newH := int(float64(origH) / ratio) return newW, newH } // saveOptimizedJPEG 使用优化的JPEG编码 func (s *ImageService) saveOptimizedJPEG(img image.Image, path string, quality int) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() // 使用标准JPEG编码器,imaging库已做优化 opts := &jpeg.Options{ Quality: quality, } return jpeg.Encode(f, img, opts) } // saveOptimizedPNG 根据质量选择最优PNG压缩级别 func (s *ImageService) saveOptimizedPNG(img image.Image, path string, quality int) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() // PNG压缩级别映射:quality越高,压缩越强(但速度慢) var level png.CompressionLevel if quality >= 90 { level = png.BestCompression // 最大压缩 } else if quality >= 70 { level = png.DefaultCompression } else { level = png.BestSpeed // 快速压缩 } encoder := &png.Encoder{ CompressionLevel: level, } return encoder.Encode(f, img) } func (s *ImageService) ImageToPDF(inputPath, outputPath string) error { if err := s.checkFormat(inputPath); err != nil { return err } if outputPath == "" { outputPath = ChangeExtension(inputPath, ".pdf") } img, err := imaging.Open(inputPath) if err != nil { return fmt.Errorf("open image failed: %v", err) } bounds := img.Bounds() imgW := float64(bounds.Dx()) imgH := float64(bounds.Dy()) pdf := gopdf.GoPdf{} pageW := 595.0 pageH := 842.0 scale := pageW / imgW if imgH*scale > pageH { scale = pageH / imgH } drawW := imgW * scale drawH := imgH * scale pdf.Start(gopdf.Config{PageSize: gopdf.Rect{W: pageW, H: pageH}}) pdf.AddPage() if err := pdf.Image(inputPath, (pageW-drawW)/2, (pageH-drawH)/2, &gopdf.Rect{W: drawW, H: drawH}); err != nil { return fmt.Errorf("embed image: %v", err) } return pdf.WritePdf(outputPath) } // StitchRequest 拼接参数 type StitchRequest struct { InputPaths []string `json:"inputPaths"` Direction string `json:"direction"` // "vertical" or "horizontal" Gap int `json:"gap"` // 图片间距(px) BgColor string `json:"bgColor"` // 背景色 hex, e.g. "#ffffff" OutputPath string `json:"outputPath"` } func (s *ImageService) StitchImages(req StitchRequest) (string, error) { if len(req.InputPaths) < 2 { return "", fmt.Errorf("至少需要 2 张图片才能拼接") } direction := req.Direction if direction == "" { direction = "vertical" } bgColor := parseHexColor(req.BgColor, color.White) // 加载所有图片 images := make([]image.Image, 0, len(req.InputPaths)) for _, p := range req.InputPaths { img, err := imaging.Open(p) if err != nil { return "", fmt.Errorf("打开图片失败 %s: %v", p, err) } images = append(images, img) } // 计算拼接后尺寸 var totalW, totalH int if direction == "vertical" { for _, img := range images { b := img.Bounds() if b.Dx() > totalW { totalW = b.Dx() } totalH += b.Dy() } if len(images) > 1 { totalH += req.Gap * (len(images) - 1) } } else { for _, img := range images { b := img.Bounds() totalW += b.Dx() if b.Dy() > totalH { totalH = b.Dy() } } if len(images) > 1 { totalW += req.Gap * (len(images) - 1) } } // 创建画布 canvas := image.NewRGBA(image.Rect(0, 0, totalW, totalH)) // 填充背景色 for y := 0; y < totalH; y++ { for x := 0; x < totalW; x++ { canvas.Set(x, y, bgColor) } } // 逐张绘制 offsetX, offsetY := 0, 0 for _, img := range images { b := img.Bounds() var dx, dy int if direction == "vertical" { dx = (totalW - b.Dx()) / 2 dy = offsetY offsetY += b.Dy() + req.Gap } else { dx = offsetX dy = (totalH - b.Dy()) / 2 offsetX += b.Dx() + req.Gap } // 绘制子图到画布 for y := 0; y < b.Dy(); y++ { for x := 0; x < b.Dx(); x++ { canvas.Set(dx+x, dy+y, img.At(b.Min.X+x, b.Min.Y+y)) } } } outputPath := req.OutputPath if outputPath == "" { outputPath = filepath.Join(os.TempDir(), "xk_stitch_output.png") } f, err := os.Create(outputPath) if err != nil { return "", fmt.Errorf("创建输出文件失败: %v", err) } defer f.Close() encoder := &png.Encoder{CompressionLevel: png.DefaultCompression} if err := encoder.Encode(f, canvas); err != nil { return "", fmt.Errorf("保存拼接图片失败: %v", err) } return outputPath, nil } func parseHexColor(hex string, fallback color.Color) color.Color { hex = strings.TrimPrefix(hex, "#") if len(hex) != 6 { return fallback } r, err1 := strconv.ParseUint(hex[0:2], 16, 8) g, err2 := strconv.ParseUint(hex[2:4], 16, 8) b, err3 := strconv.ParseUint(hex[4:6], 16, 8) if err1 != nil || err2 != nil || err3 != nil { return fallback } return color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255} }