Files
file-tool-wails/services/image_service.go
2026-06-30 12:30:49 +08:00

1089 lines
25 KiB
Go
Raw Permalink 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 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}
}