From d74c402066eba2c185fa6c8eb89b887635916fd5 Mon Sep 17 00:00:00 2001 From: Dmitry Zuev Date: Fri, 29 Nov 2019 15:27:35 +0300 Subject: [PATCH] Implement Max Bytes Filter (#275) * Implement Max Bytes Filter * Update according to code review comments * Refactor according to code review comments --- docs/generating_the_url_advanced.md | 13 ++++++++++ process.go | 39 +++++++++++++++++++++++++++++ processing_options.go | 18 +++++++++++++ 3 files changed, 70 insertions(+) diff --git a/docs/generating_the_url_advanced.md b/docs/generating_the_url_advanced.md index ef27f5cd..38ad3e68 100644 --- a/docs/generating_the_url_advanced.md +++ b/docs/generating_the_url_advanced.md @@ -169,6 +169,19 @@ Redefines quality of the resulting image, percentage. Default: value from the environment variable. +#### Max Bytes + +``` +max_bytes:%max_bytes +mb:%max_bytes +``` + +This filter automatically degrades the quality of the image until the image is under the specified amount of bytes. + +*Warning: this filter processes image multiple times to achieve specified image size* + +Default: 0 + #### Background ``` diff --git a/process.go b/process.go index 91d50f1a..361f6c5a 100644 --- a/process.go +++ b/process.go @@ -133,6 +133,15 @@ func canScaleOnLoad(imgtype imageType, scale float64) bool { return imgtype == imageTypeJPEG || imgtype == imageTypeWEBP } +func canFitToBytes(imgtype imageType) bool { + switch imgtype { + case imageTypeJPEG, imageTypeWEBP, imageTypeHEIC, imageTypeTIFF: + return true + default: + return false + } +} + func calcJpegShink(scale float64, imgtype imageType) int { shrink := int(1.0 / scale) @@ -682,5 +691,35 @@ func processImage(ctx context.Context) ([]byte, context.CancelFunc, error) { checkTimeout(ctx) } + if po.MaxBytes > 0 && canFitToBytes(po.Format) { + return processToFitBytes(po, img) + } + return img.Save(po.Format, po.Quality) } + +func processToFitBytes(po *processingOptions, img *vipsImage) ([]byte, context.CancelFunc, error) { + var diff float64 + quality := po.Quality + + img.CopyMemory() + + for { + result, cancel, err := img.Save(po.Format, quality) + if len(result) <= po.MaxBytes || quality <= 10 || err != nil { + return result, cancel, err + } + cancel() + + delta := float64(len(result)) / float64(po.MaxBytes) + switch { + case delta > 3: + diff = 0.25 + case delta > 1.5: + diff = 0.5 + default: + diff = 0.75 + } + quality = int(float64(quality) * diff) + } +} diff --git a/processing_options.go b/processing_options.go index 6bcf6d4a..78690ad9 100644 --- a/processing_options.go +++ b/processing_options.go @@ -116,6 +116,7 @@ type processingOptions struct { Crop cropOptions Format imageType Quality int + MaxBytes int Flatten bool Background rgbColor Blur float32 @@ -193,6 +194,7 @@ func newProcessingOptions() *processingOptions { Gravity: gravityOptions{Type: gravityCenter}, Enlarge: false, Quality: conf.Quality, + MaxBytes: 0, Format: imageTypeUnknown, Background: rgbColor{255, 255, 255}, Blur: 0, @@ -542,6 +544,20 @@ func applyQualityOption(po *processingOptions, args []string) error { return nil } +func applyMaxBytesOption(po *processingOptions, args []string) error { + if len(args) > 1 { + return fmt.Errorf("Invalid max_bytes arguments: %v", args) + } + + if max, err := strconv.Atoi(args[0]); err == nil && max >= 0 { + po.MaxBytes = max + } else { + return fmt.Errorf("Invalid max_bytes: %s", args[0]) + } + + return nil +} + func applyBackgroundOption(po *processingOptions, args []string) error { switch len(args) { case 1: @@ -744,6 +760,8 @@ func applyProcessingOption(po *processingOptions, name string, args []string) er return applyCropOption(po, args) case "quality", "q": return applyQualityOption(po, args) + case "max_bytes", "mb": + return applyMaxBytesOption(po, args) case "background", "bg": return applyBackgroundOption(po, args) case "blur", "bl":