Refine convert part (#303)

* Refine convert part

* Only open image once

* More refine
This commit is contained in:
Nova Kwok 2023-12-17 03:12:42 +08:00 committed by GitHub
parent c771160a05
commit d83f6667cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 167 additions and 177 deletions

View File

@ -46,7 +46,7 @@ var (
ProxyMode bool ProxyMode bool
Prefetch bool Prefetch bool
Config = NewWebPConfig() Config = NewWebPConfig()
Version = "0.10.1" Version = "0.10.2"
WriteLock = cache.New(5*time.Minute, 10*time.Minute) WriteLock = cache.New(5*time.Minute, 10*time.Minute)
RemoteRaw = "./remote-raw" RemoteRaw = "./remote-raw"
Metadata = "./metadata" Metadata = "./metadata"

View File

@ -1,7 +1,6 @@
package encoder package encoder
import ( import (
"errors"
"os" "os"
"path" "path"
"runtime" "runtime"
@ -33,35 +32,13 @@ func init() {
intMinusOne.Set(-1) intMinusOne.Set(-1)
} }
func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error { func ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraParams, c chan int) {
imgHeightWidthRatio := float32(img.Metadata().Height) / float32(img.Metadata().Width)
if extraParams.Width > 0 && extraParams.Height > 0 {
err := img.Thumbnail(extraParams.Width, extraParams.Height, vips.InterestingAttention)
if err != nil {
return err
}
} else if extraParams.Width > 0 && extraParams.Height == 0 {
err := img.Thumbnail(extraParams.Width, int(float32(extraParams.Width)*imgHeightWidthRatio), 0)
if err != nil {
return err
}
} else if extraParams.Height > 0 && extraParams.Width == 0 {
err := img.Thumbnail(int(float32(extraParams.Height)/imgHeightWidthRatio), extraParams.Height, 0)
if err != nil {
return err
}
}
return nil
}
func ConvertFilter(raw, avifPath, webpPath string, extraParams config.ExtraParams, c chan int) {
// all absolute paths // all absolute paths
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(2) wg.Add(2)
if !helper.ImageExists(avifPath) && config.Config.EnableAVIF { if !helper.ImageExists(avifPath) && config.Config.EnableAVIF {
go func() { go func() {
err := convertImage(raw, avifPath, "avif", extraParams) err := convertImage(rawPath, avifPath, "avif", extraParams)
if err != nil { if err != nil {
log.Errorln(err) log.Errorln(err)
} }
@ -73,7 +50,7 @@ func ConvertFilter(raw, avifPath, webpPath string, extraParams config.ExtraParam
if !helper.ImageExists(webpPath) { if !helper.ImageExists(webpPath) {
go func() { go func() {
err := convertImage(raw, webpPath, "webp", extraParams) err := convertImage(rawPath, webpPath, "webp", extraParams)
if err != nil { if err != nil {
log.Errorln(err) log.Errorln(err)
} }
@ -89,94 +66,54 @@ func ConvertFilter(raw, avifPath, webpPath string, extraParams config.ExtraParam
} }
} }
func ResizeItself(raw, dest string, extraParams config.ExtraParams) { func convertImage(rawPath, optimizedPath, imageType string, extraParams config.ExtraParams) error {
log.Infof("Resize %s itself to %s", raw, dest)
// we need to create dir first // we need to create dir first
var err = os.MkdirAll(path.Dir(dest), 0755) var err = os.MkdirAll(path.Dir(optimizedPath), 0755)
if err != nil { if err != nil {
log.Error(err.Error()) log.Error(err.Error())
} }
// If original image is NEF, convert NEF image to JPG first
if strings.HasSuffix(strings.ToLower(rawPath), ".nef") {
var convertedRaw, converted = ConvertRawToJPG(rawPath, optimizedPath)
// If converted, use converted file as raw
if converted {
// Use converted file(JPG) as raw input for further convertion
rawPath = convertedRaw
// Remove converted file after convertion
defer func() {
log.Infoln("Removing intermediate conversion file:", convertedRaw)
err := os.Remove(convertedRaw)
if err != nil {
log.Warnln("failed to delete converted file", err)
}
}()
}
}
img, err := vips.LoadImageFromFile(raw, &vips.ImportParams{ // Image is only opened here
img, err := vips.LoadImageFromFile(rawPath, &vips.ImportParams{
FailOnError: boolFalse, FailOnError: boolFalse,
}) })
if err != nil { defer img.Close()
log.Warnf("Could not load %s: %s", raw, err)
return // Pre-process image(auto rotate, resize, etc.)
} preProcessImage(img, imageType, extraParams)
_ = resizeImage(img, extraParams)
buf, _, _ := img.ExportNative()
_ = os.WriteFile(dest, buf, 0600)
img.Close()
}
func convertImage(raw, optimized, imageType string, extraParams config.ExtraParams) error {
// we need to create dir first
var err = os.MkdirAll(path.Dir(optimized), 0755)
if err != nil {
log.Error(err.Error())
}
// Convert NEF image to JPG first
var convertedRaw, converted = ConvertRawToJPG(raw, optimized)
// If converted, use converted file as raw
if converted {
raw = convertedRaw
}
switch imageType { switch imageType {
case "webp": case "webp":
err = webpEncoder(raw, optimized, extraParams) err = webpEncoder(img, rawPath, optimizedPath, extraParams)
case "avif": case "avif":
err = avifEncoder(raw, optimized, extraParams) err = avifEncoder(img, rawPath, optimizedPath, extraParams)
}
// Remove converted file after convertion
if converted {
log.Infoln("Removing intermediate conversion file:", convertedRaw)
err := os.Remove(convertedRaw)
if err != nil {
log.Warnln("failed to delete converted file", err)
}
} }
return err return err
} }
func avifEncoder(p1, p2 string, extraParams config.ExtraParams) error { func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string, extraParams config.ExtraParams) error {
// if convert fails, return error; success nil
var ( var (
buf []byte buf []byte
quality = config.Config.Quality quality = config.Config.Quality
err error
) )
img, err := vips.LoadImageFromFile(p1, &vips.ImportParams{
FailOnError: boolFalse,
})
if err != nil {
return err
}
imageFormat := img.Format()
for _, ignore := range avifIgnore {
if imageFormat == ignore {
// Return err to render original image
return errors.New("AVIF encoder: ignore image type")
}
}
if config.Config.EnableExtraParams {
err = resizeImage(img, extraParams)
if err != nil {
return err
}
}
// AVIF has a maximum resolution of 65536 x 65536 pixels.
if img.Metadata().Width > config.AvifMax || img.Metadata().Height > config.AvifMax {
return errors.New("AVIF: image too large")
}
err = img.AutoRotate()
if err != nil {
return err
}
// If quality >= 100, we use lossless mode // If quality >= 100, we use lossless mode
if quality >= 100 { if quality >= 100 {
@ -197,55 +134,22 @@ func avifEncoder(p1, p2 string, extraParams config.ExtraParams) error {
return err return err
} }
if err := os.WriteFile(p2, buf, 0600); err != nil { if err := os.WriteFile(optimizedPath, buf, 0600); err != nil {
log.Error(err) log.Error(err)
return err return err
} }
img.Close()
convertLog("AVIF", p1, p2, quality) convertLog("AVIF", rawPath, optimizedPath, quality)
return nil return nil
} }
func webpEncoder(p1, p2 string, extraParams config.ExtraParams) error { func webpEncoder(img *vips.ImageRef, rawPath string, optimizedPath string, extraParams config.ExtraParams) error {
// if convert fails, return error; success nil
var ( var (
buf []byte buf []byte
quality = config.Config.Quality quality = config.Config.Quality
err error
) )
img, err := vips.LoadImageFromFile(p1, &vips.ImportParams{
FailOnError: boolFalse,
NumPages: intMinusOne,
})
if err != nil {
return err
}
imageFormat := img.Format()
for _, ignore := range webpIgnore {
if imageFormat == ignore {
// Return err to render original image
return errors.New("WebP encoder: ignore image type")
}
}
if config.Config.EnableExtraParams {
err = resizeImage(img, extraParams)
if err != nil {
return err
}
}
// The maximum pixel dimensions of a WebP image is 16383 x 16383.
if (img.Metadata().Width > config.WebpMax || img.Metadata().Height > config.WebpMax) && img.Format() != vips.ImageTypeGIF {
return errors.New("WebP: image too large")
}
err = img.AutoRotate()
if err != nil {
return err
}
// If quality >= 100, we use lossless mode // If quality >= 100, we use lossless mode
if quality >= 100 { if quality >= 100 {
// Lossless mode will not encounter problems as below, because in libvips as code below // Lossless mode will not encounter problems as below, because in libvips as code below
@ -283,28 +187,27 @@ func webpEncoder(p1, p2 string, extraParams config.ExtraParams) error {
return err return err
} }
if err := os.WriteFile(p2, buf, 0600); err != nil { if err := os.WriteFile(optimizedPath, buf, 0600); err != nil {
log.Error(err) log.Error(err)
return err return err
} }
img.Close()
convertLog("WebP", p1, p2, quality) convertLog("WebP", rawPath, optimizedPath, quality)
return nil return nil
} }
func convertLog(itype, p1 string, p2 string, quality int) { func convertLog(itype, rawPath string, optimizedPath string, quality int) {
oldf, err := os.Stat(p1) oldf, err := os.Stat(rawPath)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return return
} }
newf, err := os.Stat(p2) newf, err := os.Stat(optimizedPath)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
return return
} }
log.Infof("%s@%d%%: %s->%s %d->%d %.2f%% deflated", itype, quality, log.Infof("%s@%d%%: %s->%s %d->%d %.2f%% deflated", itype, quality,
p1, p2, oldf.Size(), newf.Size(), float32(newf.Size())/float32(oldf.Size())*100) rawPath, optimizedPath, oldf.Size(), newf.Size(), float32(newf.Size())/float32(oldf.Size())*100)
} }

94
encoder/process.go Normal file
View File

@ -0,0 +1,94 @@
package encoder
import (
"errors"
"os"
"path"
"slices"
"webp_server_go/config"
"github.com/davidbyttow/govips/v2/vips"
log "github.com/sirupsen/logrus"
)
func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error {
imgHeightWidthRatio := float32(img.Metadata().Height) / float32(img.Metadata().Width)
if extraParams.Width > 0 && extraParams.Height > 0 {
err := img.Thumbnail(extraParams.Width, extraParams.Height, vips.InterestingAttention)
if err != nil {
return err
}
} else if extraParams.Width > 0 && extraParams.Height == 0 {
err := img.Thumbnail(extraParams.Width, int(float32(extraParams.Width)*imgHeightWidthRatio), 0)
if err != nil {
return err
}
} else if extraParams.Height > 0 && extraParams.Width == 0 {
err := img.Thumbnail(int(float32(extraParams.Height)/imgHeightWidthRatio), extraParams.Height, 0)
if err != nil {
return err
}
}
return nil
}
func ResizeItself(raw, dest string, extraParams config.ExtraParams) {
log.Infof("Resize %s itself to %s", raw, dest)
// we need to create dir first
var err = os.MkdirAll(path.Dir(dest), 0755)
if err != nil {
log.Error(err.Error())
}
img, err := vips.LoadImageFromFile(raw, &vips.ImportParams{
FailOnError: boolFalse,
})
if err != nil {
log.Warnf("Could not load %s: %s", raw, err)
return
}
_ = resizeImage(img, extraParams)
buf, _, _ := img.ExportNative()
_ = os.WriteFile(dest, buf, 0600)
img.Close()
}
// Pre-process image(auto rotate, resize, etc.)
func preProcessImage(img *vips.ImageRef, imageType string, extraParams config.ExtraParams) error {
// Check Width/Height and ignore image formats
switch imageType {
case "webp":
if img.Metadata().Width > config.WebpMax || img.Metadata().Height > config.WebpMax {
return errors.New("WebP: image too large")
}
imageFormat := img.Format()
if slices.Contains(webpIgnore, imageFormat) {
// Return err to render original image
return errors.New("WebP encoder: ignore image type")
}
case "avif":
if img.Metadata().Width > config.AvifMax || img.Metadata().Height > config.AvifMax {
return errors.New("AVIF: image too large")
}
imageFormat := img.Format()
if slices.Contains(avifIgnore, imageFormat) {
// Return err to render original image
return errors.New("AVIF encoder: ignore image type")
}
}
// Auto rotate
err := img.AutoRotate()
if err != nil {
return err
}
if config.Config.EnableExtraParams {
err = resizeImage(img, extraParams)
if err != nil {
return err
}
}
return nil
}

View File

@ -2,16 +2,11 @@ package encoder
import ( import (
"path/filepath" "path/filepath"
"strings"
"github.com/jeremytorres/rawparser" "github.com/jeremytorres/rawparser"
) )
func ConvertRawToJPG(rawPath, optimizedPath string) (string, bool) { func ConvertRawToJPG(rawPath, optimizedPath string) (string, bool) {
if !strings.HasSuffix(strings.ToLower(rawPath), ".nef") {
// Maybe can use rawParser to convert other raw files to jpg, but I haven't tested it
return rawPath, false
}
parser, _ := rawparser.NewNefParser(true) parser, _ := rawparser.NewNefParser(true)
info := &rawparser.RawFileInfo{ info := &rawparser.RawFileInfo{
File: rawPath, File: rawPath,

View File

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
"slices"
"strings" "strings"
"webp_server_go/config" "webp_server_go/config"
"webp_server_go/encoder" "webp_server_go/encoder"
@ -23,16 +24,28 @@ func Convert(c *fiber.Ctx) error {
// 3. pass it to encoder, get the result, send it back // 3. pass it to encoder, get the result, send it back
var ( var (
reqHostname = c.Hostname() reqHostname = c.Hostname()
reqHost = c.Protocol() + "://" + reqHostname // http://www.example.com:8000 reqHost = c.Protocol() + "://" + reqHostname // http://www.example.com:8000
reqURI, _ = url.QueryUnescape(c.Path()) // /mypic/123.jpg reqHeader = &c.Request().Header
reqURIwithQuery, _ = url.QueryUnescape(c.OriginalURL()) // /mypic/123.jpg?someother=200&somebugs=200
filename = path.Base(reqURI) reqURIRaw, _ = url.QueryUnescape(c.Path()) // /mypic/123.jpg
realRemoteAddr = "" reqURIwithQueryRaw, _ = url.QueryUnescape(c.OriginalURL()) // /mypic/123.jpg?someother=200&somebugs=200
targetHostName = config.LocalHostAlias reqURI = path.Clean(reqURIRaw) // delete ../ in reqURI to mitigate directory traversal
targetHost = config.Config.ImgPath reqURIwithQuery = path.Clean(reqURIwithQueryRaw) // Sometimes reqURIwithQuery can be https://example.tld/mypic/123.jpg?someother=200&somebugs=200, we need to extract it
proxyMode = config.ProxyMode
mapMode = false filename = path.Base(reqURI)
realRemoteAddr = ""
targetHostName = config.LocalHostAlias
targetHost = config.Config.ImgPath
proxyMode = config.ProxyMode
mapMode = false
width, _ = strconv.Atoi(c.Query("width")) // Extra Params
height, _ = strconv.Atoi(c.Query("height")) // Extra Params
extraParams = config.ExtraParams{
Width: width,
Height: height,
}
) )
log.Debugf("Incoming connection from %s %s %s", c.IP(), reqHostname, reqURIwithQuery) log.Debugf("Incoming connection from %s %s %s", c.IP(), reqHostname, reqURIwithQuery)
@ -45,19 +58,6 @@ func Convert(c *fiber.Ctx) error {
return nil return nil
} }
// Sometimes reqURIwithQuery can be https://example.tld/mypic/123.jpg?someother=200&somebugs=200, we need to extract it.
// delete ../ in reqURI to mitigate directory traversal
reqURI = path.Clean(reqURI)
reqURIwithQuery = path.Clean(reqURIwithQuery)
width, _ := strconv.Atoi(c.Query("width"))
height, _ := strconv.Atoi(c.Query("height"))
var extraParams = config.ExtraParams{
Width: width,
Height: height,
}
// Rewrite the target backend if a mapping rule matches the hostname // Rewrite the target backend if a mapping rule matches the hostname
if hostMap, hostMapFound := config.Config.ImageMap[reqHost]; hostMapFound { if hostMap, hostMapFound := config.Config.ImageMap[reqHost]; hostMapFound {
log.Debugf("Found host mapping %s -> %s", reqHostname, hostMap) log.Debugf("Found host mapping %s -> %s", reqHostname, hostMap)
@ -132,9 +132,9 @@ func Convert(c *fiber.Ctx) error {
} }
} }
goodFormat := helper.GuessSupportedFormat(&c.Request().Header) supportedFormats := helper.GuessSupportedFormat(reqHeader)
// resize itself and return if only one format(raw) is supported // resize itself and return if only one format(raw) is supported
if len(goodFormat) == 1 { if len(supportedFormats) == 1 {
dest := path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id) dest := path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id)
if !helper.ImageExists(dest) { if !helper.ImageExists(dest) {
encoder.ResizeItself(rawImageAbs, dest, extraParams) encoder.ResizeItself(rawImageAbs, dest, extraParams)
@ -156,13 +156,11 @@ func Convert(c *fiber.Ctx) error {
encoder.ConvertFilter(rawImageAbs, avifAbs, webpAbs, extraParams, nil) encoder.ConvertFilter(rawImageAbs, avifAbs, webpAbs, extraParams, nil)
var availableFiles = []string{rawImageAbs} var availableFiles = []string{rawImageAbs}
for _, v := range goodFormat { if slices.Contains(supportedFormats, "avif") {
if v == "avif" { availableFiles = append(availableFiles, avifAbs)
availableFiles = append(availableFiles, avifAbs) }
} if slices.Contains(supportedFormats, "webp") {
if v == "webp" { availableFiles = append(availableFiles, webpAbs)
availableFiles = append(availableFiles, webpAbs)
}
} }
finalFilename := helper.FindSmallestFiles(availableFiles) finalFilename := helper.FindSmallestFiles(availableFiles)