diff --git a/config/config.go b/config/config.go index 34d9f7f6..3fbb6148 100644 --- a/config/config.go +++ b/config/config.go @@ -51,6 +51,7 @@ var ( StripColorProfile bool AutoRotate bool EnforceThumbnail bool + ReturnAttachment bool EnableWebpDetection bool EnforceWebp bool @@ -208,6 +209,7 @@ func Reset() { StripColorProfile = true AutoRotate = true EnforceThumbnail = false + ReturnAttachment = false EnableWebpDetection = false EnforceWebp = false @@ -356,6 +358,7 @@ func Configure() error { configurators.Bool(&StripColorProfile, "IMGPROXY_STRIP_COLOR_PROFILE") configurators.Bool(&AutoRotate, "IMGPROXY_AUTO_ROTATE") configurators.Bool(&EnforceThumbnail, "IMGPROXY_ENFORCE_THUMBNAIL") + configurators.Bool(&ReturnAttachment, "IMGPROXY_RETURN_ATTACHMENT") configurators.Bool(&EnableWebpDetection, "IMGPROXY_ENABLE_WEBP_DETECTION") configurators.Bool(&EnforceWebp, "IMGPROXY_ENFORCE_WEBP") diff --git a/docs/configuration.md b/docs/configuration.md index e702fa24..f6ea32b4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -421,5 +421,6 @@ imgproxy can send logs to syslog, but this feature is disabled by default. To en * `IMGPROXY_STRIP_COLOR_PROFILE`: when `true`, imgproxy will transform the embedded color profile (ICC) to sRGB and remove it from the image. Otherwise, imgproxy will try to keep it as is. Default: `true` * `IMGPROXY_AUTO_ROTATE`: when `true`, imgproxy will automatically rotate images based on the EXIF Orientation parameter (if available in the image meta data). The orientation tag will be removed from the image in all cases. Default: `true` * `IMGPROXY_ENFORCE_THUMBNAIL`: when `true` and the source image has an embedded thumbnail, imgproxy will always use the embedded thumbnail instead of the main image. Currently, only thumbnails embedded in `heic` and `avif` are supported. Default: `false` +* `IMGPROXY_RETURN_ATTACHMENT`: when `true`, response header `Content-Disposition` will include `attachment`. Default: `false` * `IMGPROXY_HEALTH_CHECK_MESSAGE`: the content of the health check response. Default: `imgproxy is running` * `IMGPROXY_HEALTH_CHECK_PATH`: an additional path of the health check. Default: blank diff --git a/docs/generating_the_url.md b/docs/generating_the_url.md index d0ce1f73..9355d215 100644 --- a/docs/generating_the_url.md +++ b/docs/generating_the_url.md @@ -529,6 +529,15 @@ eth:%enforce_thumbnail When set to `1`, `t` or `true` and the source image has an embedded thumbnail, imgproxy will always use the embedded thumbnail instead of the main image. Currently, only thumbnails embedded in `heic` and `avif` are supported. This is normally controlled by the [IMGPROXY_ENFORCE_THUMBNAIL](configuration.md#miscellaneous) configuration but this procesing option allows the configuration to be set for each request. +### Return attachment + +``` +return_attachment:%return_attachment +att:%return_attachment +``` + +When set to `1`, `t` or `true`, imgproxy will return `attachment` in the `Content-Disposition` header, and the browser will open a 'Save as' dialog. This is normally controlled by the [IMGPROXY_RETURN_ATTACHMENT](configuration.md#miscellaneous) configuration but this procesing option allows the configuration to be set for each request. + ### Quality ``` diff --git a/imagetype/imagetype.go b/imagetype/imagetype.go index 16498568..a52e6d1c 100644 --- a/imagetype/imagetype.go +++ b/imagetype/imagetype.go @@ -54,16 +54,16 @@ var ( } contentDispositionsFmt = map[Type]string{ - JPEG: "inline; filename=\"%s.jpg\"", - PNG: "inline; filename=\"%s.png\"", - WEBP: "inline; filename=\"%s.webp\"", - GIF: "inline; filename=\"%s.gif\"", - ICO: "inline; filename=\"%s.ico\"", - SVG: "inline; filename=\"%s.svg\"", - HEIC: "inline; filename=\"%s.heic\"", - AVIF: "inline; filename=\"%s.avif\"", - BMP: "inline; filename=\"%s.bmp\"", - TIFF: "inline; filename=\"%s.tiff\"", + JPEG: "%s; filename=\"%s.jpg\"", + PNG: "%s; filename=\"%s.png\"", + WEBP: "%s; filename=\"%s.webp\"", + GIF: "%s; filename=\"%s.gif\"", + ICO: "%s; filename=\"%s.ico\"", + SVG: "%s; filename=\"%s.svg\"", + HEIC: "%s; filename=\"%s.heic\"", + AVIF: "%s; filename=\"%s.avif\"", + BMP: "%s; filename=\"%s.bmp\"", + TIFF: "%s; filename=\"%s.tiff\"", } ) @@ -93,27 +93,33 @@ func (it Type) Mime() string { return "application/octet-stream" } -func (it Type) ContentDisposition(filename string) string { - format, ok := contentDispositionsFmt[it] - if !ok { - return "inline" +func (it Type) ContentDisposition(filename string, returnAttachment bool) string { + disposition := "inline" + + if returnAttachment { + disposition = "attachment" } - return fmt.Sprintf(format, strings.ReplaceAll(filename, `"`, "%22")) + format, ok := contentDispositionsFmt[it] + if !ok { + return disposition + } + + return fmt.Sprintf(format, disposition, strings.ReplaceAll(filename, `"`, "%22")) } -func (it Type) ContentDispositionFromURL(imageURL string) string { +func (it Type) ContentDispositionFromURL(imageURL string, returnAttachment bool) string { url, err := url.Parse(imageURL) if err != nil { - return it.ContentDisposition(contentDispositionFilenameFallback) + return it.ContentDisposition(contentDispositionFilenameFallback, returnAttachment) } _, filename := filepath.Split(url.Path) if len(filename) == 0 { - return it.ContentDisposition(contentDispositionFilenameFallback) + return it.ContentDisposition(contentDispositionFilenameFallback, returnAttachment) } - return it.ContentDisposition(strings.TrimSuffix(filename, filepath.Ext(filename))) + return it.ContentDisposition(strings.TrimSuffix(filename, filepath.Ext(filename)), returnAttachment) } func (it Type) SupportsAlpha() bool { diff --git a/options/processing_options.go b/options/processing_options.go index 781e7266..206a7094 100644 --- a/options/processing_options.go +++ b/options/processing_options.go @@ -89,6 +89,7 @@ type ProcessingOptions struct { StripColorProfile bool AutoRotate bool EnforceThumbnail bool + ReturnAttachment bool SkipProcessingFormats []imagetype.Type @@ -140,6 +141,7 @@ func NewProcessingOptions() *ProcessingOptions { StripColorProfile: config.StripColorProfile, AutoRotate: config.AutoRotate, EnforceThumbnail: config.EnforceThumbnail, + ReturnAttachment: config.ReturnAttachment, // Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed defaultQuality: config.Quality, @@ -859,6 +861,16 @@ func applyEnforceThumbnailOption(po *ProcessingOptions, args []string) error { return nil } +func applyReturnAttachmentOption(po *ProcessingOptions, args []string) error { + if len(args) > 1 { + return fmt.Errorf("Invalid return_attachment arguments: %v", args) + } + + po.ReturnAttachment = parseBoolOption(args[0]) + + return nil +} + func applyURLOption(po *ProcessingOptions, name string, args []string) error { switch name { case "resize", "rs": @@ -913,6 +925,8 @@ func applyURLOption(po *ProcessingOptions, name string, args []string) error { return applyStripColorProfileOption(po, args) case "enforce_thumbnail", "eth": return applyEnforceThumbnailOption(po, args) + case "return_attachment", "att": + return applyReturnAttachmentOption(po, args) // Saving options case "quality", "q": return applyQualityOption(po, args) diff --git a/processing_handler.go b/processing_handler.go index bc6ded7f..258dbcf1 100644 --- a/processing_handler.go +++ b/processing_handler.go @@ -86,9 +86,9 @@ func setVary(rw http.ResponseWriter) { func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) { var contentDisposition string if len(po.Filename) > 0 { - contentDisposition = resultData.Type.ContentDisposition(po.Filename) + contentDisposition = resultData.Type.ContentDisposition(po.Filename, po.ReturnAttachment) } else { - contentDisposition = resultData.Type.ContentDispositionFromURL(originURL) + contentDisposition = resultData.Type.ContentDispositionFromURL(originURL, po.ReturnAttachment) } rw.Header().Set("Content-Type", resultData.Type.Mime())