From f368ed13e93892430339de14e5ff26e9245add16 Mon Sep 17 00:00:00 2001 From: DarthSim Date: Mon, 18 Jul 2022 17:49:04 +0600 Subject: [PATCH] IMGPROXY_PREFERRED_FORMATS config --- CHANGELOG.md | 1 + config/config.go | 19 ++++++++++++ config/configurators/configurators.go | 6 ++-- docs/configuration.md | 16 ++++++++++ main.go | 6 ++++ processing/processing.go | 42 +++++++++++++++++++++++---- 6 files changed, 82 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 356a962e..7806ae1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Add support of 16-bit BMP. - Add `IMGPROXY_NEW_RELIC_LABELS` config. - Add support of JPEG files with differential Huffman coding or arithmetic coding. +- Add `IMGPROXY_PREFERRED_FORMATS` config. ### Fix - Fix trimming of CMYK images. diff --git a/config/config.go b/config/config.go index f715ec81..312e47d6 100644 --- a/config/config.go +++ b/config/config.go @@ -59,6 +59,8 @@ var ( EnforceAvif bool EnableClientHints bool + PreferredFormats []imagetype.Type + SkipProcessingFormats []imagetype.Type UseLinearColorspace bool @@ -220,6 +222,15 @@ func Reset() { EnforceAvif = false EnableClientHints = false + PreferredFormats = []imagetype.Type{ + imagetype.JPEG, + imagetype.PNG, + imagetype.GIF, + imagetype.WEBP, + imagetype.AVIF, + imagetype.ICO, + } + SkipProcessingFormats = make([]imagetype.Type, 0) UseLinearColorspace = false @@ -376,6 +387,10 @@ func Configure() error { configurators.String(&HealthCheckPath, "IMGPROXY_HEALTH_CHECK_PATH") + if err := configurators.ImageTypes(&PreferredFormats, "IMGPROXY_PREFERRED_FORMATS"); err != nil { + return err + } + if err := configurators.ImageTypes(&SkipProcessingFormats, "IMGPROXY_SKIP_PROCESSING_FORMATS"); err != nil { return err } @@ -557,6 +572,10 @@ func Configure() error { return fmt.Errorf("Quality can't be greater than 100, now - %d\n", Quality) } + if len(PreferredFormats) == 0 { + return fmt.Errorf("At least one preferred format should be specified") + } + if IgnoreSslVerification { log.Warning("Ignoring SSL verification is very unsafe") } diff --git a/config/configurators/configurators.go b/config/configurators/configurators.go index 6ffd3cb6..518c9210 100644 --- a/config/configurators/configurators.go +++ b/config/configurators/configurators.go @@ -103,17 +103,17 @@ func Bool(b *bool, name string) { } func ImageTypes(it *[]imagetype.Type, name string) error { - *it = []imagetype.Type{} - if env := os.Getenv(name); len(env) > 0 { parts := strings.Split(env, ",") + *it = make([]imagetype.Type, 0, len(parts)) + for _, p := range parts { pt := strings.TrimSpace(p) if t, ok := imagetype.Types[pt]; ok { *it = append(*it, t) } else { - return fmt.Errorf("Unknown image format to skip: %s", pt) + return fmt.Errorf("Unknown image format: %s", pt) } } } diff --git a/docs/configuration.md b/docs/configuration.md index a0550a58..6828555f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -249,6 +249,22 @@ You can set up a fallback image that will be used in case imgproxy is unable to * `IMGPROXY_FALLBACK_IMAGE_TTL`: a duration (in seconds) sent via the `Expires` and `Cache-Control: max-age` HTTP headers when a fallback image was used. When blank or `0`, the value from `IMGPROXY_TTL` is used. * `IMGPROXY_FALLBACK_IMAGES_CACHE_SIZE`: ![pro](/assets/pro.svg) the size of custom fallback images cache. When set to `0`, the fallback image cache is disabled. 256 fallback images are cached by default. +## Preferred formats + +When the resulting image format is not explicitly specified in the imgproxy URL via the extension or the `format` processing option, imgproxy will choose one of the preferred formats as the resultant: + +* `IMGPROXY_PREFERRED_FORMATS`: a list of preferred formats, comma divided. Default: `jpeg,png,gif,webp,avif,ico` + +imgproxy is guided by the following rules when choosing the resulting format: + +1. If the preferred formats list contains the source image format, it will be used as the resultant +2. If the resulting image is animated, the resulting image format should support animations +3. If the resulting image contains transparency, the resulting image format should support transparency +4. imgproxy chooses the first preferred format that meets those requirements +5. If none of the preferred formats meet the requirements, the first preferred format is used as the resultant + +**📝Note:** When AVIF/WebP support detection is enabled and the browser supports AVIF/WebP, it may be used as the resultant format even if the preferred formats list doesn't contain it. + ## Skip processing You can configure imgproxy to skip processing of some formats: diff --git a/main.go b/main.go index f5a7db8b..36cbb4dd 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "github.com/imgproxy/imgproxy/v3/metrics" "github.com/imgproxy/imgproxy/v3/metrics/prometheus" "github.com/imgproxy/imgproxy/v3/options" + "github.com/imgproxy/imgproxy/v3/processing" "github.com/imgproxy/imgproxy/v3/version" "github.com/imgproxy/imgproxy/v3/vips" ) @@ -51,6 +52,11 @@ func initialize() error { return err } + if err := processing.ValidatePreferredFormats(); err != nil { + vips.Shutdown() + return err + } + if err := options.ParsePresets(config.Presets); err != nil { vips.Shutdown() return err diff --git a/processing/processing.go b/processing/processing.go index cca1f6c7..f32a5156 100644 --- a/processing/processing.go +++ b/processing/processing.go @@ -37,9 +37,40 @@ var mainPipeline = pipeline{ finalize, } -func imageTypeGoodForWeb(imgtype imagetype.Type) bool { - return imgtype != imagetype.TIFF && - imgtype != imagetype.BMP +func isImageTypePreferred(imgtype imagetype.Type) bool { + for _, t := range config.PreferredFormats { + if imgtype == t { + return true + } + } + + return false +} + +func findBestFormat(srcType imagetype.Type, animated, expectAlpha bool) imagetype.Type { + for _, t := range config.PreferredFormats { + if animated && !t.SupportsAnimation() { + continue + } + + if expectAlpha && !t.SupportsAlpha() { + continue + } + + return t + } + + return config.PreferredFormats[0] +} + +func ValidatePreferredFormats() error { + for _, t := range config.PreferredFormats { + if !vips.SupportsSave(t) { + return fmt.Errorf("%s can't be a preferred format as it's saving is not supported", t) + } + } + + return nil } func canFitToBytes(imgtype imagetype.Type) bool { @@ -223,6 +254,7 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options originWidth, originHeight := getImageSize(img) animated := img.IsAnimated() + expectAlpha := !po.Flatten && (img.HasAlpha() || po.Padding.Enabled || po.Extend.Enabled) switch { case po.Format == imagetype.Unknown: @@ -231,10 +263,10 @@ func ProcessImage(ctx context.Context, imgdata *imagedata.ImageData, po *options po.Format = imagetype.AVIF case po.PreferWebP: po.Format = imagetype.WEBP - case vips.SupportsSave(imgdata.Type) && imageTypeGoodForWeb(imgdata.Type): + case isImageTypePreferred(imgdata.Type): po.Format = imgdata.Type default: - po.Format = imagetype.JPEG + po.Format = findBestFormat(imgdata.Type, animated, expectAlpha) } case po.EnforceAvif && !animated: po.Format = imagetype.AVIF