summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoe Mooring <joe.mooring@veriphor.com>2023-12-15 12:04:05 -0800
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2023-12-20 14:18:24 +0100
commit648d00c7d8243d71e33c33e99a6e4f509beddf98 (patch)
tree31e27c902b91101e1273efc4bedd654b2e233a61
parent8adba648cc130a97d2c814c65aa8396044c251fd (diff)
resources/images: Create AutoOrient image filter
Closes #11717
-rw-r--r--docs/assets/images/examples/landscape-exif-orientation-5.jpgbin0 -> 38639 bytes
-rw-r--r--docs/content/en/functions/images/AutoOrient.md52
-rw-r--r--docs/layouts/shortcodes/img.html379
-rw-r--r--resources/image.go16
-rw-r--r--resources/images/auto_orient.go60
-rw-r--r--resources/images/filters.go8
6 files changed, 509 insertions, 6 deletions
diff --git a/docs/assets/images/examples/landscape-exif-orientation-5.jpg b/docs/assets/images/examples/landscape-exif-orientation-5.jpg
new file mode 100644
index 000000000..ad64835eb
--- /dev/null
+++ b/docs/assets/images/examples/landscape-exif-orientation-5.jpg
Binary files differ
diff --git a/docs/content/en/functions/images/AutoOrient.md b/docs/content/en/functions/images/AutoOrient.md
new file mode 100644
index 000000000..588f4874c
--- /dev/null
+++ b/docs/content/en/functions/images/AutoOrient.md
@@ -0,0 +1,52 @@
+---
+title: images.AutoOrient
+description: Returns an image filter that rotates and flips an image as needed per its EXIF orientation tag.
+categories: []
+keywords: []
+action:
+ aliases: []
+ related:
+ - functions/images/Filter
+ - methods/resource/Filter
+ returnType: images.filter
+ signatures: [images.AutoOrient]
+toc: true
+---
+
+{{< new-in 0.122.0 >}}
+
+## Usage
+
+Create the filter:
+
+```go-html-template
+{{ $filter := images.AutoOrient }}
+```
+
+{{% include "functions/images/_common/apply-image-filter.md" %}}
+
+{{% note %}}
+When using with other filters, specify `images.AutoOrient` first.
+{{% /note %}}
+
+```go-html-template
+{{ $filters := slice
+ images.AutoOrient
+ (images.Process "resize 200x")
+}}
+{{ with resources.Get "images/original.jpg" }}
+ {{ with images.Filter $filters . }}
+ <img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
+ {{ end }}
+{{ end }}
+```
+
+## Example
+
+{{< img
+ src="images/examples/landscape-exif-orientation-5.jpg"
+ alt="Zion National Park"
+ filter="AutoOrient"
+ filterArgs=""
+ example=true
+>}}
diff --git a/docs/layouts/shortcodes/img.html b/docs/layouts/shortcodes/img.html
new file mode 100644
index 000000000..50d4da9ed
--- /dev/null
+++ b/docs/layouts/shortcodes/img.html
@@ -0,0 +1,379 @@
+{{- /*
+Renders the given image using the given filter, if any.
+
+@param {string} src The path to the image which must be a remote, page, or global resource.
+@param {string} [filter] The filter to apply to the image (case-insensitive).
+@param {string} [filterArgs] A comma-delimited list of arguments to pass to the filter.
+@param {bool} [example=false] If true, renders a before/after example.
+@param {int} [exampleWidth=384] Image width, in pixels, when rendering a before/after example.
+
+@returns {template.HTML}
+
+@examples
+
+ {{< img src="zion-national-park.jpg" >}}
+
+ {{< img src="zion-national-park.jpg" alt="Zion National Park" >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="grayscale"
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="process"
+ filterArgs="resize 400x webp"
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="colorize"
+ filterArgs="180,50,20"
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="grayscale"
+ example=true
+ >}}
+
+ {{< img
+ src="zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="grayscale"
+ example=true
+ exampleWidth=400
+ >}}
+
+ When using the text filter, provide the arguments in this order:
+
+ 0. The text
+ 1. The horizontal offset, in pixels, relative to the left of the image (default 20)
+ 2. The vertical offset, in pixels, relative to the top of the image (default 20)
+ 3. The font size in pixels (default 64)
+ 4. The line height (default 1.2)
+ 5. The font color (default #ffffff)
+
+ {{< img
+ src="images/examples/zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="Text"
+ filterArgs="Zion National Park,25,250,56"
+ example=true
+ >}}
+
+ When using the padding filter, provide all arguments in this order:
+
+ 0. Padding top
+ 1. Padding right
+ 2. Padding bottom
+ 3. Padding right
+ 4. Canvas color
+
+ {{< img
+ src="images/examples/zion-national-park.jpg"
+ alt="Zion National Park"
+ filter="Padding"
+ filterArgs="20,50,20,50,#0705"
+ example=true
+ >}}
+
+*/}}
+
+{{- /* Initialize. */}}
+{{- $alt := "" }}
+{{- $src := "" }}
+{{- $filter := "" }}
+{{- $filterArgs := slice }}
+{{- $example := false }}
+{{- $exampleWidth := 384 }}
+
+{{- /* Default values to use with the text filter. */}}
+{{ $textFilterOpts := dict
+ "xOffset" 20
+ "yOffset" 20
+ "fontSize" 64
+ "lineHeight" 1.2
+ "fontColor" "#ffffff"
+ "fontPath" "https://github.com/google/fonts/raw/main/ofl/lato/Lato-Regular.ttf"
+}}
+
+{{- /* Get and validate parameters. */}}
+{{- with .Get "alt" }}
+ {{- $alt = .}}
+{{- end }}
+
+{{- with .Get "src" }}
+ {{- $src = . }}
+{{- else }}
+ {{- errorf "The %q shortcode requires a file parameter. See %s" .Name .Position }}
+{{- end }}
+
+{{- with .Get "filter" }}
+ {{- $filter = . | lower }}
+{{- end }}
+
+{{- $validFilters := slice
+ "autoorient" "brightness" "colorbalance" "colorize" "contrast" "gamma"
+ "gaussianblur" "grayscale" "hue" "invert" "none" "opacity" "overlay"
+ "padding" "pixelate" "process" "saturation" "sepia" "sigmoid" "text"
+ "unsharpmask"
+}}
+
+{{- with $filter }}
+ {{- if not (in $validFilters .) }}
+ {{- errorf "The filter passed to the %q shortcode is invalid. The filter must be one of %s. See %s" $.Name (delimit $validFilters ", " ", or ") $.Position }}
+ {{- end }}
+{{- end }}
+
+{{- with .Get "filterArgs" }}
+ {{- $filterArgs = split . "," }}
+ {{- $filterArgs = apply $filterArgs "trim" "." " " }}
+{{- end }}
+
+{{- if in (slice "false" false 0) (.Get "example") }}
+ {{- $example = false }}
+{{- else if in (slice "true" true 1) (.Get "example")}}
+ {{- $example = true }}
+{{- end }}
+
+{{- with .Get "exampleWidth" }}
+ {{- $exampleWidth = . | int }}
+{{- end }}
+
+{{- /* Get image. */}}
+{{- $ctx := dict "page" .Page "src" $src "name" .Name "position" .Position }}
+{{- $i := partial "inline/get-resource.html" $ctx }}
+
+{{- /* Resize if rendering before/after examples. */}}
+{{- if $example }}
+ {{- $i = $i.Resize (printf "%dx" $exampleWidth) }}
+{{- end }}
+
+{{- /* Create filter. */}}
+{{- $f := "" }}
+{{- $ctx := dict "filter" $filter "args" $filterArgs "name" .Name "position" .Position }}
+{{- if eq $filter "autoorient" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.AutoOrient }}
+{{- else if eq $filter "brightness" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Brightness (index $filterArgs 0) }}
+{{- else if eq $filter "colorbalance" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage red" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage green" "argValue" (index $filterArgs 1) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage blue" "argValue" (index $filterArgs 2) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.ColorBalance (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
+{{- else if eq $filter "colorize" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "hue" "argValue" (index $filterArgs 0) "min" 0 "max" 360) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "saturation" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 2) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Colorize (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
+{{- else if eq $filter "contrast" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Contrast (index $filterArgs 0) }}
+{{- else if eq $filter "gamma" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "gamma" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Gamma (index $filterArgs 0) }}
+{{- else if eq $filter "gaussianblur" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.GaussianBlur (index $filterArgs 0) }}
+{{- else if eq $filter "grayscale" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Grayscale }}
+{{- else if eq $filter "hue" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "shift" "argValue" (index $filterArgs 0) "min" -180 "max" 180) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Hue (index $filterArgs 0) }}
+{{- else if eq $filter "invert" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 0) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Invert }}
+{{- else if eq $filter "opacity" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "opacity" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Opacity (index $filterArgs 0) }}
+{{- else if eq $filter "overlay" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $ctx := dict "src" (index $filterArgs 0) "name" .Name "position" .Position }}
+ {{- $overlayImg := partial "inline/get-resource.html" $ctx }}
+ {{- $f = images.Overlay $overlayImg (index $filterArgs 1 | float ) (index $filterArgs 2 | float) }}
+{{- else if eq $filter "padding" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 5) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Padding
+ (index $filterArgs 0 | int)
+ (index $filterArgs 1 | int)
+ (index $filterArgs 2 | int)
+ (index $filterArgs 3 | int)
+ (index $filterArgs 4)
+ }}
+{{- else if eq $filter "pixelate" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "size" "argValue" (index $filterArgs 0) "min" 0 "max" 1000) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Pixelate (index $filterArgs 0) }}
+{{- else if eq $filter "process" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $f = images.Process (index $filterArgs 0) }}
+{{- else if eq $filter "saturation" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" -100 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Saturation (index $filterArgs 0) }}
+{{- else if eq $filter "sepia" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "percentage" "argValue" (index $filterArgs 0) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Sepia (index $filterArgs 0) }}
+{{- else if eq $filter "sigmoid" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 2) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "midpoint" "argValue" (index $filterArgs 0) "min" 0 "max" 1) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "factor" "argValue" (index $filterArgs 1) "min" -10 "max" 10) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.Sigmoid (index $filterArgs 0) (index $filterArgs 1) }}
+{{- else if eq $filter "text" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 1) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $ctx := dict "src" $textFilterOpts.fontPath "name" .Name "position" .Position }}
+ {{- $font := or (partial "inline/get-resource.html" $ctx) }}
+ {{- $fontSize := or (index $filterArgs 3 | int) $textFilterOpts.fontSize }}
+ {{- $lineHeight := math.Max (or (index $filterArgs 4 | float) $textFilterOpts.lineHeight) 1 }}
+ {{- $opts := dict
+ "x" (or (index $filterArgs 1 | int) $textFilterOpts.xOffset)
+ "y" (or (index $filterArgs 2 | int) $textFilterOpts.yOffset)
+ "size" $fontSize
+ "linespacing" (mul (sub $lineHeight 1) $fontSize)
+ "color" (or (index $filterArgs 5) $textFilterOpts.fontColor)
+ "font" $font
+ }}
+ {{- $f = images.Text (index $filterArgs 0) $opts }}
+{{- else if eq $filter "unsharpmask" }}
+ {{- $ctx = merge $ctx (dict "argsRequired" 3) }}
+ {{- template "validate-arg-count" $ctx }}
+ {{- $filterArgs = apply $filterArgs "float" "." }}
+ {{- $ctx = merge $ctx (dict "argName" "sigma" "argValue" (index $filterArgs 0) "min" 0 "max" 500) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "amount" "argValue" (index $filterArgs 1) "min" 0 "max" 100) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $ctx = merge $ctx (dict "argName" "threshold" "argValue" (index $filterArgs 2) "min" 0 "max" 1) }}
+ {{- template "validate-arg-value" $ctx }}
+ {{- $f = images.UnsharpMask (index $filterArgs 0) (index $filterArgs 1) (index $filterArgs 2) }}
+{{- end }}
+
+{{- /* Apply filter. */}}
+{{- $fi := $i }}
+{{- with $f }}
+ {{- $fi = $i.Filter . }}
+{{- end }}
+
+{{- /* Render. */}}
+{{- if $example }}
+ <p>Original</p>
+ <img class='di ba b--black-20' style="width: initial;" src="{{ $i.RelPermalink }}" alt="{{ $alt }}">
+ <p>Processed</p>
+ <img class='di ba b--black-20' style="width: initial;" src="{{ $fi.RelPermalink }}" alt="{{ $alt }}">
+{{- else -}}
+ <img class='di' style="width: initial;" src="{{ $fi.RelPermalink }}" alt="{{ $alt }}">
+{{- end }}
+
+{{- define "validate-arg-count" }}
+ {{- $msg := "When using the %q filter, the %q shortcode requires an args parameter with %d %s. See %s" }}
+ {{- if lt (len .args) .argsRequired }}
+ {{- $text := "values" }}
+ {{- if eq 1 .argsRequired }}
+ {{- $text = "value" }}
+ {{- end }}
+ {{- errorf $msg .filter .name .argsRequired $text .position }}
+ {{- end }}
+{{- end }}
+
+{{- define "validate-arg-value" }}
+ {{- $msg := "The %q argument passed to the %q shortcode is invalid. Expected a value in the range [%v,%v], but received %v. See %s" }}
+ {{- if or (lt .argValue .min) (gt .argValue .max) }}
+ {{- errorf $msg .argName .name .min .max .argValue .position }}
+ {{- end }}
+{{- end }}
+
+{{- define "partials/inline/get-resource.html" }}
+ {{- $r := "" }}
+ {{- $u := urls.Parse .src }}
+ {{- $msg := "The %q shortcode was unable to resolve %s. See %s" }}
+ {{- if $u.IsAbs }}
+ {{- with resources.GetRemote $u.String }}
+ {{- with .Err }}
+ {{- errorf "%s" }}
+ {{- else }}
+ {{- /* This is a remote resource. */}}
+ {{- $r = . }}
+ {{- end }}
+ {{- else }}
+ {{- errorf $msg $.name $u.String $.position }}
+ {{- end }}
+ {{- else }}
+ {{- with .page.Resources.Get (strings.TrimPrefix "./" $u.Path) }}
+ {{- /* This is a page resource. */}}
+ {{- $r = . }}
+ {{- else }}
+ {{- with resources.Get $u.Path }}
+ {{- /* This is a global resource. */}}
+ {{- $r = . }}
+ {{- else }}
+ {{- errorf $msg $.name $u.Path $.position }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+ {{- return $r}}
+{{- end -}}
diff --git a/resources/image.go b/resources/image.go
index cb0181a5f..6c34795f8 100644
--- a/resources/image.go
+++ b/resources/image.go
@@ -279,8 +279,8 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
}
return i.doWithImageConfig(conf, func(src image.Image) (image.Image, error) {
- filters := gfilters
- for j, f := range gfilters {
+ var filters []gift.Filter
+ for _, f := range gfilters {
f = images.UnwrapFilter(f)
if specProvider, ok := f.(images.ImageProcessSpecProvider); ok {
processSpec := specProvider.ImageProcessSpec()
@@ -293,10 +293,14 @@ func (i *imageResource) Filter(filters ...any) (images.ImageResource, error) {
if err != nil {
return nil, err
}
- // Replace the filter with the new filters.
- // This slice will be empty if this is just a format conversion.
- filters = append(filters[:j], append(pFilters, filters[j+1:]...)...)
-
+ filters = append(filters, pFilters...)
+ } else if orientationProvider, ok := f.(images.ImageFilterFromOrientationProvider); ok {
+ tf := orientationProvider.AutoOrient(i.Exif())
+ if tf != nil {
+ filters = append(filters, tf)
+ }
+ } else {
+ filters = append(filters, f)
}
}
return i.Proc.Filter(src, filters...)
diff --git a/resources/images/auto_orient.go b/resources/images/auto_orient.go
new file mode 100644
index 000000000..194efefb5
--- /dev/null
+++ b/resources/images/auto_orient.go
@@ -0,0 +1,60 @@
+// Copyright 2023 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package images
+
+import (
+ "image"
+ "image/draw"
+
+ "github.com/disintegration/gift"
+ "github.com/gohugoio/hugo/resources/images/exif"
+)
+
+var _ gift.Filter = (*autoOrientFilter)(nil)
+
+var transformationFilters = map[int]gift.Filter{
+ 2: gift.FlipHorizontal(),
+ 3: gift.Rotate180(),
+ 4: gift.FlipVertical(),
+ 5: gift.Transpose(),
+ 6: gift.Rotate270(),
+ 7: gift.Transverse(),
+ 8: gift.Rotate90(),
+}
+
+type autoOrientFilter struct{}
+
+type ImageFilterFromOrientationProvider interface {
+ AutoOrient(exifInfo *exif.ExifInfo) gift.Filter
+}
+
+func (f autoOrientFilter) Draw(dst draw.Image, src image.Image, options *gift.Options) {
+ panic("not supported")
+}
+
+func (f autoOrientFilter) Bounds(srcBounds image.Rectangle) image.Rectangle {
+ panic("not supported")
+}
+
+func (f autoOrientFilter) AutoOrient(exifInfo *exif.ExifInfo) gift.Filter {
+ if exifInfo != nil {
+ if orientation, ok := exifInfo.Tags["Orientation"].(int); ok {
+ if filter, ok := transformationFilters[orientation]; ok {
+ return filter
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/resources/images/filters.go b/resources/images/filters.go
index 2a1f41a03..572a10d71 100644
--- a/resources/images/filters.go
+++ b/resources/images/filters.go
@@ -174,6 +174,14 @@ func (*Filters) Padding(args ...any) gift.Filter {
}
}
+// AutoOrient creates a filter that rotates and flips an image as needed per
+// its EXIF orientation tag.
+func (*Filters) AutoOrient() gift.Filter {
+ return filter{
+ Filter: autoOrientFilter{},
+ }
+}
+
// Brightness creates a filter that changes the brightness of an image.
// The percentage parameter must be in range (-100, 100).
func (*Filters) Brightness(percentage any) gift.Filter {