Files
file-tool-wails/services/image_service.go
2026-06-17 08:05:20 +08:00

830 lines
19 KiB
Go

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
}