From 965e58e28231a0f07ef99ed0c044d65a74e5a41f Mon Sep 17 00:00:00 2001 From: n0vad3v Date: Fri, 15 Dec 2023 14:37:35 +0800 Subject: [PATCH 1/3] Refine convert part --- config/config.go | 2 +- encoder/encoder.go | 142 ++++++++++++++++++++++----------------------- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/config/config.go b/config/config.go index e501c5159..86a67155e 100644 --- a/config/config.go +++ b/config/config.go @@ -46,7 +46,7 @@ var ( ProxyMode bool Prefetch bool Config = NewWebPConfig() - Version = "0.10.1" + Version = "0.10.2" WriteLock = cache.New(5*time.Minute, 10*time.Minute) RemoteRaw = "./remote-raw" Metadata = "./metadata" diff --git a/encoder/encoder.go b/encoder/encoder.go index d9d03b388..31e95a132 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -5,6 +5,7 @@ import ( "os" "path" "runtime" + "slices" "strings" "sync" "webp_server_go/config" @@ -54,14 +55,13 @@ func resizeImage(img *vips.ImageRef, extraParams config.ExtraParams) error { return nil } -func ConvertFilter(raw, avifPath, webpPath string, extraParams config.ExtraParams, c chan int) { +func ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraParams, c chan int) { // all absolute paths - var wg sync.WaitGroup wg.Add(2) if !helper.ImageExists(avifPath) && config.Config.EnableAVIF { go func() { - err := convertImage(raw, avifPath, "avif", extraParams) + err := convertImage(rawPath, avifPath, "avif", extraParams) if err != nil { log.Errorln(err) } @@ -73,7 +73,7 @@ func ConvertFilter(raw, avifPath, webpPath string, extraParams config.ExtraParam if !helper.ImageExists(webpPath) { go func() { - err := convertImage(raw, webpPath, "webp", extraParams) + err := convertImage(rawPath, webpPath, "webp", extraParams) if err != nil { log.Errorln(err) } @@ -111,37 +111,87 @@ func ResizeItself(raw, dest string, extraParams config.ExtraParams) { img.Close() } -func convertImage(raw, optimized, imageType string, extraParams config.ExtraParams) error { +// Pre-process image(auto rotate, resize, etc.) +func preProcessImage(rawPath string, imageType string, extraParams config.ExtraParams) error { + img, err := vips.LoadImageFromFile(rawPath, &vips.ImportParams{ + FailOnError: boolFalse, + }) + if err != nil { + log.Warnf("Could not load %s: %s", rawPath, err) + return err + } + defer img.Close() + + // 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 +} + +func convertImage(rawPath, optimizedPath, imageType string, extraParams config.ExtraParams) error { // we need to create dir first - var err = os.MkdirAll(path.Dir(optimized), 0755) + var err = os.MkdirAll(path.Dir(optimizedPath), 0755) if err != nil { log.Error(err.Error()) } // Convert NEF image to JPG first - var convertedRaw, converted = ConvertRawToJPG(raw, optimized) + var convertedRaw, converted = ConvertRawToJPG(rawPath, optimizedPath) // If converted, use converted file as raw if converted { - raw = convertedRaw + 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) + } + }() } + // Pre-process image(auto rotate, resize, etc.) + preProcessImage(rawPath, imageType, extraParams) + switch imageType { case "webp": - err = webpEncoder(raw, optimized, extraParams) + err = webpEncoder(rawPath, optimizedPath, extraParams) case "avif": - err = avifEncoder(raw, optimized, 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) - } + err = avifEncoder(rawPath, optimizedPath, extraParams) } return err } func avifEncoder(p1, p2 string, extraParams config.ExtraParams) error { - // if convert fails, return error; success nil var ( buf []byte quality = config.Config.Quality @@ -152,31 +202,7 @@ func avifEncoder(p1, p2 string, extraParams config.ExtraParams) error { 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 - } + defer img.Close() // If quality >= 100, we use lossless mode if quality >= 100 { @@ -201,14 +227,12 @@ func avifEncoder(p1, p2 string, extraParams config.ExtraParams) error { log.Error(err) return err } - img.Close() convertLog("AVIF", p1, p2, quality) return nil } func webpEncoder(p1, p2 string, extraParams config.ExtraParams) error { - // if convert fails, return error; success nil var ( buf []byte quality = config.Config.Quality @@ -221,30 +245,7 @@ func webpEncoder(p1, p2 string, extraParams config.ExtraParams) error { 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 - } + defer img.Close() // If quality >= 100, we use lossless mode if quality >= 100 { @@ -287,7 +288,6 @@ func webpEncoder(p1, p2 string, extraParams config.ExtraParams) error { log.Error(err) return err } - img.Close() convertLog("WebP", p1, p2, quality) return nil From 43c9227b81fc8dac27859e7f5c728c04336c3530 Mon Sep 17 00:00:00 2001 From: n0vad3v Date: Fri, 15 Dec 2023 18:05:48 +0800 Subject: [PATCH 2/3] Only open image once --- encoder/encoder.go | 165 +++++++++--------------------------------- encoder/process.go | 94 ++++++++++++++++++++++++ encoder/rawconvert.go | 5 -- 3 files changed, 128 insertions(+), 136 deletions(-) create mode 100644 encoder/process.go diff --git a/encoder/encoder.go b/encoder/encoder.go index 31e95a132..7e20cf992 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -1,11 +1,9 @@ package encoder import ( - "errors" "os" "path" "runtime" - "slices" "strings" "sync" "webp_server_go/config" @@ -34,27 +32,6 @@ func init() { intMinusOne.Set(-1) } -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 ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraParams, c chan int) { // all absolute paths var wg sync.WaitGroup @@ -89,120 +66,54 @@ func ConvertFilter(rawPath, avifPath, webpPath string, extraParams config.ExtraP } } -func ResizeItself(raw, dest string, extraParams config.ExtraParams) { - log.Infof("Resize %s itself to %s", raw, dest) - +func convertImage(rawPath, optimizedPath, imageType string, extraParams config.ExtraParams) error { // 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 { 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 + // 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) + } + }() + } } - _ = resizeImage(img, extraParams) - buf, _, _ := img.ExportNative() - _ = os.WriteFile(dest, buf, 0600) - img.Close() -} -// Pre-process image(auto rotate, resize, etc.) -func preProcessImage(rawPath string, imageType string, extraParams config.ExtraParams) error { + // Image is only opened here img, err := vips.LoadImageFromFile(rawPath, &vips.ImportParams{ FailOnError: boolFalse, }) - if err != nil { - log.Warnf("Could not load %s: %s", rawPath, err) - return err - } defer img.Close() - // 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 -} - -func convertImage(rawPath, optimizedPath, imageType string, extraParams config.ExtraParams) error { - // we need to create dir first - var err = os.MkdirAll(path.Dir(optimizedPath), 0755) - if err != nil { - log.Error(err.Error()) - } - // Convert NEF image to JPG first - var convertedRaw, converted = ConvertRawToJPG(rawPath, optimizedPath) - // If converted, use converted file as raw - if converted { - 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) - } - }() - } // Pre-process image(auto rotate, resize, etc.) - preProcessImage(rawPath, imageType, extraParams) + preProcessImage(img, imageType, extraParams) switch imageType { case "webp": - err = webpEncoder(rawPath, optimizedPath, extraParams) + err = webpEncoder(img, rawPath, optimizedPath, extraParams) case "avif": - err = avifEncoder(rawPath, optimizedPath, extraParams) + err = avifEncoder(img, rawPath, optimizedPath, extraParams) } return err } -func avifEncoder(p1, p2 string, extraParams config.ExtraParams) error { +func avifEncoder(img *vips.ImageRef, rawPath string, optimizedPath string, extraParams config.ExtraParams) error { var ( buf []byte quality = config.Config.Quality + err error ) - img, err := vips.LoadImageFromFile(p1, &vips.ImportParams{ - FailOnError: boolFalse, - }) - if err != nil { - return err - } - defer img.Close() // If quality >= 100, we use lossless mode if quality >= 100 { @@ -223,30 +134,22 @@ func avifEncoder(p1, p2 string, extraParams config.ExtraParams) error { return err } - if err := os.WriteFile(p2, buf, 0600); err != nil { + if err := os.WriteFile(optimizedPath, buf, 0600); err != nil { log.Error(err) return err } - convertLog("AVIF", p1, p2, quality) + convertLog("AVIF", rawPath, optimizedPath, quality) return nil } -func webpEncoder(p1, p2 string, extraParams config.ExtraParams) error { +func webpEncoder(img *vips.ImageRef, rawPath string, optimizedPath string, extraParams config.ExtraParams) error { var ( buf []byte quality = config.Config.Quality + err error ) - img, err := vips.LoadImageFromFile(p1, &vips.ImportParams{ - FailOnError: boolFalse, - NumPages: intMinusOne, - }) - if err != nil { - return err - } - defer img.Close() - // If quality >= 100, we use lossless mode if quality >= 100 { // Lossless mode will not encounter problems as below, because in libvips as code below @@ -284,27 +187,27 @@ func webpEncoder(p1, p2 string, extraParams config.ExtraParams) error { return err } - if err := os.WriteFile(p2, buf, 0600); err != nil { + if err := os.WriteFile(optimizedPath, buf, 0600); err != nil { log.Error(err) return err } - convertLog("WebP", p1, p2, quality) + convertLog("WebP", rawPath, optimizedPath, quality) return nil } -func convertLog(itype, p1 string, p2 string, quality int) { - oldf, err := os.Stat(p1) +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(p2) + newf, err := os.Stat(optimizedPath) if err != nil { log.Error(err) return } 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) } diff --git a/encoder/process.go b/encoder/process.go new file mode 100644 index 000000000..d8af428cc --- /dev/null +++ b/encoder/process.go @@ -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 +} diff --git a/encoder/rawconvert.go b/encoder/rawconvert.go index b4fae7bc2..c55a0133d 100644 --- a/encoder/rawconvert.go +++ b/encoder/rawconvert.go @@ -2,16 +2,11 @@ package encoder import ( "path/filepath" - "strings" "github.com/jeremytorres/rawparser" ) 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) info := &rawparser.RawFileInfo{ File: rawPath, From d58a426cfa3e3f625328182bb698b51078db1080 Mon Sep 17 00:00:00 2001 From: n0vad3v Date: Fri, 15 Dec 2023 18:36:24 +0800 Subject: [PATCH 3/3] More refine --- handler/router.go | 62 +++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/handler/router.go b/handler/router.go index bbc431f4d..6b2659c41 100644 --- a/handler/router.go +++ b/handler/router.go @@ -4,6 +4,7 @@ import ( "net/http" "net/url" "regexp" + "slices" "strings" "webp_server_go/config" "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 var ( - reqHostname = c.Hostname() - reqHost = c.Protocol() + "://" + reqHostname // http://www.example.com:8000 - reqURI, _ = url.QueryUnescape(c.Path()) // /mypic/123.jpg - reqURIwithQuery, _ = url.QueryUnescape(c.OriginalURL()) // /mypic/123.jpg?someother=200&somebugs=200 - filename = path.Base(reqURI) - realRemoteAddr = "" - targetHostName = config.LocalHostAlias - targetHost = config.Config.ImgPath - proxyMode = config.ProxyMode - mapMode = false + reqHostname = c.Hostname() + reqHost = c.Protocol() + "://" + reqHostname // http://www.example.com:8000 + reqHeader = &c.Request().Header + + reqURIRaw, _ = url.QueryUnescape(c.Path()) // /mypic/123.jpg + reqURIwithQueryRaw, _ = url.QueryUnescape(c.OriginalURL()) // /mypic/123.jpg?someother=200&somebugs=200 + reqURI = path.Clean(reqURIRaw) // delete ../ in reqURI to mitigate directory traversal + reqURIwithQuery = path.Clean(reqURIwithQueryRaw) // Sometimes reqURIwithQuery can be https://example.tld/mypic/123.jpg?someother=200&somebugs=200, we need to extract it + + 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) @@ -45,19 +58,6 @@ func Convert(c *fiber.Ctx) error { 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 if hostMap, hostMapFound := config.Config.ImageMap[reqHost]; hostMapFound { 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 - if len(goodFormat) == 1 { + if len(supportedFormats) == 1 { dest := path.Join(config.Config.ExhaustPath, targetHostName, metadata.Id) if !helper.ImageExists(dest) { encoder.ResizeItself(rawImageAbs, dest, extraParams) @@ -156,13 +156,11 @@ func Convert(c *fiber.Ctx) error { encoder.ConvertFilter(rawImageAbs, avifAbs, webpAbs, extraParams, nil) var availableFiles = []string{rawImageAbs} - for _, v := range goodFormat { - if v == "avif" { - availableFiles = append(availableFiles, avifAbs) - } - if v == "webp" { - availableFiles = append(availableFiles, webpAbs) - } + if slices.Contains(supportedFormats, "avif") { + availableFiles = append(availableFiles, avifAbs) + } + if slices.Contains(supportedFormats, "webp") { + availableFiles = append(availableFiles, webpAbs) } finalFilename := helper.FindSmallestFiles(availableFiles)