Files
file-tool-wails/services/image_service.go

1089 lines
25 KiB
Go
Raw Permalink Normal View History

2026-06-17 08:05:20 +08:00
package services
import (
"bytes"
"encoding/base64"
"fmt"
"image"
"image/color"
2026-06-30 12:30:49 +08:00
"image/jpeg"
2026-06-17 08:05:20 +08:00
"image/png"
"math"
"os"
"path/filepath"
2026-06-30 12:30:49 +08:00
"strconv"
2026-06-17 08:05:20 +08:00
"strings"
"github.com/disintegration/imaging"
2026-06-23 15:33:22 +08:00
"github.com/signintech/gopdf"
2026-06-17 08:05:20 +08:00
_ "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)
}
2026-06-23 15:33:22 +08:00
outExt := strings.ToLower(filepath.Ext(outputPath))
switch {
case outExt == ".jpg" || outExt == ".jpeg" || format == "jpeg" || format == "jpg":
2026-06-17 08:05:20 +08:00
if quality <= 0 || quality > 100 {
quality = 85
}
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
2026-06-23 15:33:22 +08:00
case outExt == ".png" || format == "png":
2026-06-17 08:05:20 +08:00
err = imaging.Save(img, outputPath, imaging.PNGCompressionLevel(png.BestCompression))
2026-06-23 15:33:22 +08:00
case outExt == ".gif" || format == "gif":
2026-06-17 08:05:20 +08:00
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
2026-06-23 15:33:22 +08:00
case outExt == ".bmp" || format == "bmp":
2026-06-17 08:05:20 +08:00
err = imaging.Save(img, outputPath)
2026-06-23 15:33:22 +08:00
case outExt == ".tiff" || outExt == ".tif" || format == "tiff" || format == "tif":
2026-06-17 08:05:20 +08:00
err = imaging.Save(img, outputPath)
2026-06-23 15:33:22 +08:00
case format == "ico" || outExt == ".ico":
2026-06-17 08:05:20 +08:00
err = s.ConvertToICO(inputPath, outputPath, []int{16, 32, 48, 64, 128, 256})
default:
2026-06-23 15:33:22 +08:00
if quality <= 0 || quality > 100 {
quality = 85
}
err = imaging.Save(img, outputPath, imaging.JPEGQuality(quality))
2026-06-17 08:05:20 +08:00
}
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")
}
2026-06-30 12:30:49 +08:00
// 记录原始文件大小
originalInfo, err := os.Stat(inputPath)
if err != nil {
return fmt.Errorf("获取原文件信息失败: %v", err)
}
originalSize := originalInfo.Size()
2026-06-17 08:05:20 +08:00
img, err := imaging.Open(inputPath)
if err != nil {
return fmt.Errorf("打开图片失败: %v", err)
}
2026-06-30 12:30:49 +08:00
// 智能降采样:超大图片自动缩小到合理尺寸
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)
2026-06-17 08:05:20 +08:00
}
2026-06-30 12:30:49 +08:00
}
2026-06-17 08:05:20 +08:00
2026-06-30 12:30:49 +08:00
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)
2026-06-17 08:05:20 +08:00
}
}
ext := strings.ToLower(filepath.Ext(outputPath))
switch ext {
case ".jpg", ".jpeg":
2026-06-30 12:30:49 +08:00
err = s.saveOptimizedJPEG(img, outputPath, quality)
2026-06-17 08:05:20 +08:00
case ".png":
2026-06-30 12:30:49 +08:00
err = s.saveOptimizedPNG(img, outputPath, quality)
2026-06-17 08:05:20 +08:00
case ".gif":
err = imaging.Save(img, outputPath, imaging.GIFNumColors(256))
2026-06-23 15:33:22 +08:00
case ".bmp":
err = imaging.Save(img, outputPath)
case ".tiff", ".tif":
err = imaging.Save(img, outputPath)
2026-06-17 08:05:20 +08:00
default:
2026-06-30 12:30:49 +08:00
err = s.saveOptimizedJPEG(img, outputPath, quality)
2026-06-17 08:05:20 +08:00
}
if err != nil {
return fmt.Errorf("保存压缩图片失败: %v", err)
}
2026-06-30 12:30:49 +08:00
// 检查压缩后的文件大小
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))
}
2026-06-17 08:05:20 +08:00
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
}
2026-06-23 15:33:22 +08:00
2026-06-30 12:30:49 +08:00
// 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)
}
2026-06-23 15:33:22 +08:00
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)
}
2026-06-30 12:30:49 +08:00
// 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}
}