mirror of
https://github.com/woodchen-ink/webp_server_go.git
synced 2025-07-18 05:32:02 +08:00
334 lines
8.4 KiB
Go
334 lines
8.4 KiB
Go
package encoder
|
||
|
||
import (
|
||
"os"
|
||
"path"
|
||
"runtime"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
"webp_server_go/config"
|
||
"webp_server_go/helper"
|
||
|
||
"github.com/davidbyttow/govips/v2/vips"
|
||
log "github.com/sirupsen/logrus"
|
||
)
|
||
|
||
var (
|
||
boolFalse vips.BoolParameter
|
||
intMinusOne vips.IntParameter
|
||
// Source image encoder ignore list for WebP and AVIF
|
||
// We shouldn't convert Unknown and AVIF to WebP
|
||
webpIgnore = []vips.ImageType{vips.ImageTypeUnknown, vips.ImageTypeAVIF}
|
||
// We shouldn't convert Unknown,AVIF and GIF to AVIF
|
||
avifIgnore = append(webpIgnore, vips.ImageTypeGIF)
|
||
)
|
||
|
||
func init() {
|
||
vips.LoggingSettings(nil, vips.LogLevelError)
|
||
vips.Startup(&vips.Config{
|
||
ConcurrencyLevel: runtime.NumCPU(),
|
||
})
|
||
boolFalse.Set(false)
|
||
intMinusOne.Set(-1)
|
||
}
|
||
|
||
func ConvertFilter(rawPath, jxlPath, avifPath, webpPath string, extraParams config.ExtraParams, supportedFormats map[string]bool, c chan int) {
|
||
// Wait for the conversion to complete and return the converted image
|
||
retryDelay := 100 * time.Millisecond // Initial retry delay
|
||
|
||
for {
|
||
if _, found := config.ConvertLock.Get(rawPath); found {
|
||
log.Debugf("文件 %s 在转换过程中被锁定,请在 %s 后重试", rawPath, retryDelay)
|
||
time.Sleep(retryDelay)
|
||
} else {
|
||
// The lock is released, indicating that the conversion is complete
|
||
break
|
||
}
|
||
}
|
||
|
||
// If there is a lock here, it means that another thread is converting the same image
|
||
// Lock rawPath to prevent concurrent conversion
|
||
config.ConvertLock.Set(rawPath, true, -1)
|
||
defer config.ConvertLock.Delete(rawPath)
|
||
|
||
var wg sync.WaitGroup
|
||
wg.Add(3)
|
||
if !helper.ImageExists(avifPath) && config.Config.EnableAVIF && supportedFormats["avif"] {
|
||
go func() {
|
||
err := convertImage(rawPath, avifPath, "avif", extraParams)
|
||
if err != nil {
|
||
log.Errorln(err)
|
||
}
|
||
defer wg.Done()
|
||
}()
|
||
} else {
|
||
wg.Done()
|
||
}
|
||
|
||
if !helper.ImageExists(webpPath) && config.Config.EnableWebP && supportedFormats["webp"] {
|
||
go func() {
|
||
err := convertImage(rawPath, webpPath, "webp", extraParams)
|
||
if err != nil {
|
||
log.Errorln(err)
|
||
}
|
||
defer wg.Done()
|
||
}()
|
||
} else {
|
||
wg.Done()
|
||
}
|
||
|
||
if !helper.ImageExists(jxlPath) && config.Config.EnableJXL && supportedFormats["jxl"] {
|
||
go func() {
|
||
err := convertImage(rawPath, jxlPath, "jxl", extraParams)
|
||
if err != nil {
|
||
log.Errorln(err)
|
||
}
|
||
defer wg.Done()
|
||
}()
|
||
} else {
|
||
wg.Done()
|
||
}
|
||
|
||
wg.Wait()
|
||
|
||
if c != nil {
|
||
c <- 1
|
||
}
|
||
}
|
||
|
||
func convertImage(rawPath, optimizedPath, imageType string, extraParams config.ExtraParams) error {
|
||
// 创建目标目录
|
||
err := os.MkdirAll(path.Dir(optimizedPath), 0755)
|
||
if err != nil {
|
||
log.Error(err.Error())
|
||
return err
|
||
}
|
||
|
||
// 如果原始图像是 NEF 格式,先转换为 JPG
|
||
if strings.HasSuffix(strings.ToLower(rawPath), ".nef") {
|
||
convertedRaw, converted := ConvertRawToJPG(rawPath, optimizedPath)
|
||
if converted {
|
||
rawPath = convertedRaw
|
||
defer func() {
|
||
log.Infoln("移除中间转换文件:", convertedRaw)
|
||
if err := os.Remove(convertedRaw); err != nil {
|
||
log.Warnln("删除转换文件失败", err)
|
||
}
|
||
}()
|
||
}
|
||
}
|
||
|
||
// 打开图像
|
||
img, err := vips.LoadImageFromFile(rawPath, &vips.ImportParams{
|
||
FailOnError: boolFalse,
|
||
NumPages: intMinusOne,
|
||
})
|
||
if err != nil {
|
||
log.Warnf("无法打开源图像: %v", err)
|
||
return err
|
||
}
|
||
defer img.Close()
|
||
|
||
// 预处理图像(自动旋转、调整大小等)
|
||
shouldCopyOriginal, err := preProcessImage(img, imageType, extraParams)
|
||
if err != nil {
|
||
log.Warnf("无法预处理源图像: %v", err)
|
||
if shouldCopyOriginal {
|
||
log.Infof("由于预处理错误,将复制原图")
|
||
return helper.CopyFile(rawPath, optimizedPath)
|
||
}
|
||
return err
|
||
}
|
||
|
||
// 根据图像类型进行编码
|
||
var encoderErr error
|
||
switch imageType {
|
||
case "webp":
|
||
encoderErr = webpEncoder(img, rawPath, optimizedPath)
|
||
case "avif":
|
||
encoderErr = avifEncoder(img, rawPath, optimizedPath)
|
||
case "jxl":
|
||
encoderErr = jxlEncoder(img, rawPath, optimizedPath)
|
||
}
|
||
|
||
if encoderErr != nil {
|
||
log.Warnf("图像编码失败: %v", encoderErr)
|
||
// 如果编码失败,我们也复制原图
|
||
return helper.CopyFile(rawPath, optimizedPath)
|
||
}
|
||
|
||
// 比较转换后的文件大小
|
||
originalInfo, err := os.Stat(rawPath)
|
||
if err != nil {
|
||
log.Errorf("获取原图文件信息失败: %v", err)
|
||
return err
|
||
}
|
||
convertedInfo, err := os.Stat(optimizedPath)
|
||
if err != nil {
|
||
log.Errorf("获取转换后文件信息失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
if convertedInfo.Size() > originalInfo.Size() {
|
||
// log.Infof("转换后的图片大于原图,使用原图: %s", rawPath)
|
||
// 删除转换后的大文件
|
||
if err := os.Remove(optimizedPath); err != nil {
|
||
log.Warnf("删除大的转换文件失败: %v", err)
|
||
}
|
||
// 将原图复制到目标路径
|
||
return helper.CopyFile(rawPath, optimizedPath)
|
||
}
|
||
|
||
// log.Infof("图像处理成功: 目标文件=%s", optimizedPath)
|
||
return nil
|
||
}
|
||
|
||
func jxlEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error {
|
||
var (
|
||
buf []byte
|
||
quality = config.Config.Quality
|
||
err error
|
||
)
|
||
|
||
// If quality >= 100, we use lossless mode
|
||
if quality >= 100 {
|
||
buf, _, err = img.ExportJxl(&vips.JxlExportParams{
|
||
Effort: 1,
|
||
Tier: 4,
|
||
Lossless: true,
|
||
Distance: 1.0,
|
||
})
|
||
} else {
|
||
buf, _, err = img.ExportJxl(&vips.JxlExportParams{
|
||
Effort: 1,
|
||
Tier: 4,
|
||
Quality: quality,
|
||
Lossless: false,
|
||
Distance: 1.0,
|
||
})
|
||
}
|
||
|
||
if err != nil {
|
||
log.Warnf("Can't encode source image: %v to JXL", err)
|
||
return err
|
||
}
|
||
|
||
if err := os.WriteFile(optimizedPath, buf, 0600); err != nil {
|
||
log.Error(err)
|
||
return err
|
||
}
|
||
|
||
// convertLog("JXL", rawPath, optimizedPath, quality)
|
||
return nil
|
||
}
|
||
|
||
func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error {
|
||
var (
|
||
buf []byte
|
||
quality = config.Config.Quality
|
||
err error
|
||
)
|
||
|
||
// If quality >= 100, we use lossless mode
|
||
if quality >= 100 {
|
||
buf, _, err = img.ExportAvif(&vips.AvifExportParams{
|
||
Lossless: true,
|
||
StripMetadata: config.Config.StripMetadata,
|
||
})
|
||
} else {
|
||
buf, _, err = img.ExportAvif(&vips.AvifExportParams{
|
||
Quality: quality,
|
||
Lossless: false,
|
||
StripMetadata: config.Config.StripMetadata,
|
||
})
|
||
}
|
||
|
||
if err != nil {
|
||
log.Warnf("无法将源图像:%v 编码为 AVIF", err)
|
||
return err
|
||
}
|
||
|
||
if err := os.WriteFile(optimizedPath, buf, 0600); err != nil {
|
||
log.Error(err)
|
||
return err
|
||
}
|
||
|
||
// convertLog("AVIF", rawPath, optimizedPath, quality)
|
||
return nil
|
||
}
|
||
|
||
func webpEncoder(img *vips.ImageRef, rawPath string, optimizedPath string) error {
|
||
var (
|
||
buf []byte
|
||
quality = config.Config.Quality
|
||
err error
|
||
)
|
||
|
||
// If quality >= 100, we use lossless mode
|
||
if quality >= 100 {
|
||
// Lossless mode will not encounter problems as below, because in libvips as code below
|
||
// config.method = ExUtilGetInt(argv[++c], 0, &parse_error);
|
||
// use_lossless_preset = 0; // disable -z option
|
||
buf, _, err = img.ExportWebp(&vips.WebpExportParams{
|
||
Lossless: true,
|
||
StripMetadata: config.Config.StripMetadata,
|
||
})
|
||
} else {
|
||
// If some special images cannot encode with default ReductionEffort(0), then retry from 0 to 6
|
||
// Example: https://github.com/webp-sh/webp_server_go/issues/234
|
||
ep := vips.WebpExportParams{
|
||
Quality: quality,
|
||
Lossless: false,
|
||
StripMetadata: config.Config.StripMetadata,
|
||
}
|
||
for i := range 7 {
|
||
ep.ReductionEffort = i
|
||
buf, _, err = img.ExportWebp(&ep)
|
||
if err != nil && strings.Contains(err.Error(), "unable to encode") {
|
||
log.Warnf("无法使用 ReductionEffort %d 将图像编码为 WebP,请尝试更高的值...", i)
|
||
} else if err != nil {
|
||
log.Warnf("无法将源图像编码为 WebP:%v", err)
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
buf, _, err = img.ExportWebp(&ep)
|
||
}
|
||
|
||
if err != nil {
|
||
log.Warnf("无法将源图像:%v 编码为 WebP", err)
|
||
return err
|
||
}
|
||
|
||
if err := os.WriteFile(optimizedPath, buf, 0600); err != nil {
|
||
log.Error(err)
|
||
return err
|
||
}
|
||
|
||
// convertLog("WebP", rawPath, optimizedPath, quality)
|
||
return nil
|
||
}
|
||
|
||
// func convertLog(itype, rawPath string, optimizedPath string, quality int) {
|
||
// oldf, err := os.Stat(rawPath)
|
||
// if err != nil {
|
||
// log.Error(err)
|
||
// return
|
||
// }
|
||
|
||
// newf, err := os.Stat(optimizedPath)
|
||
// if err != nil {
|
||
// log.Error(err)
|
||
// return
|
||
// }
|
||
|
||
// // 计算压缩率
|
||
// deflateRate := float32(newf.Size()) / float32(oldf.Size()) * 100
|
||
|
||
// // 记录转换信息
|
||
// log.Infof("图像转换: 类型=%s, 质量=%d%%", itype, quality)
|
||
// log.Infof("文件路径: 原始=%s, 优化=%s", rawPath, optimizedPath)
|
||
// log.Infof("文件大小: 原始=%d字节, 优化=%d字节, 压缩率=%.2f%%", oldf.Size(), newf.Size(), deflateRate)
|
||
// }
|