Skip to content

Commit 67619a6

Browse files
committed
add support for exif orientation tag
if any transformation is requested, first apply any additional transformation necessary to correct for the EXIF orientation tag, since it is stripped from the resulting image. Fixes #63
1 parent 07c54b4 commit 67619a6

2 files changed

Lines changed: 138 additions & 1 deletion

File tree

transform.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import (
2121
_ "image/gif" // register gif format
2222
"image/jpeg"
2323
"image/png"
24+
"io"
2425
"math"
2526

2627
"github.com/disintegration/imaging"
28+
"github.com/rwcarlsen/goexif/exif"
2729
"golang.org/x/image/tiff" // register tiff format
2830
_ "golang.org/x/image/webp" // register webp format
2931
"willnorris.com/go/gifresize"
@@ -32,6 +34,9 @@ import (
3234
// default compression quality of resized jpegs
3335
const defaultQuality = 95
3436

37+
// maximum distance into image to look for EXIF tags
38+
const maxExifSize = 1 << 20
39+
3540
// resample filter used when resizing images
3641
var resampleFilter = imaging.Lanczos
3742

@@ -50,6 +55,15 @@ func Transform(img []byte, opt Options) ([]byte, error) {
5055
return nil, err
5156
}
5257

58+
// apply EXIF orientation for jpeg and tiff source images. Read at most
59+
// up to maxExifSize looking for EXIF tags.
60+
if format == "jpeg" || format == "tiff" {
61+
r := io.LimitReader(bytes.NewReader(img), maxExifSize)
62+
if exifOpt := exifOrientation(r); exifOpt.transform() {
63+
m = transformImage(m, exifOpt)
64+
}
65+
}
66+
5367
// encode webp and tiff as jpeg by default
5468
if format == "tiff" || format == "webp" {
5569
format = "jpeg"
@@ -187,6 +201,57 @@ func cropParams(m image.Image, opt Options) (x0, y0, x1, y1 int, crop bool) {
187201
return x0, y0, x1, y1, true
188202
}
189203

204+
// read EXIF orientation tag from r and adjust opt to orient image correctly.
205+
func exifOrientation(r io.Reader) (opt Options) {
206+
// Exif Orientation Tag values
207+
// http://sylvana.net/jpegcrop/exif_orientation.html
208+
const (
209+
topLeftSide = 1
210+
topRightSide = 2
211+
bottomRightSide = 3
212+
bottomLeftSide = 4
213+
leftSideTop = 5
214+
rightSideTop = 6
215+
rightSideBottom = 7
216+
leftSideBottom = 8
217+
)
218+
219+
ex, err := exif.Decode(r)
220+
if err != nil {
221+
return opt
222+
}
223+
tag, err := ex.Get(exif.Orientation)
224+
if err != nil {
225+
return opt
226+
}
227+
orient, err := tag.Int(0)
228+
if err != nil {
229+
return opt
230+
}
231+
232+
switch orient {
233+
case topLeftSide:
234+
// do nothing
235+
case topRightSide:
236+
opt.FlipHorizontal = true
237+
case bottomRightSide:
238+
opt.Rotate = 180
239+
case bottomLeftSide:
240+
opt.FlipVertical = true
241+
case leftSideTop:
242+
opt.Rotate = 90
243+
opt.FlipVertical = true
244+
case rightSideTop:
245+
opt.Rotate = -90
246+
case rightSideBottom:
247+
opt.Rotate = 90
248+
opt.FlipHorizontal = true
249+
case leftSideBottom:
250+
opt.Rotate = 90
251+
}
252+
return opt
253+
}
254+
190255
// transformImage modifies the image m based on the transformations specified
191256
// in opt.
192257
func transformImage(m image.Image, opt Options) image.Image {

transform_test.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package imageproxy
1616

1717
import (
1818
"bytes"
19+
"encoding/base64"
1920
"image"
2021
"image/color"
2122
"image/draw"
@@ -39,7 +40,7 @@ var (
3940
// newImage creates a new NRGBA image with the specified dimensions and pixel
4041
// color data. If the length of pixels is 1, the entire image is filled with
4142
// that color.
42-
func newImage(w, h int, pixels ...color.NRGBA) image.Image {
43+
func newImage(w, h int, pixels ...color.Color) image.Image {
4344
m := image.NewNRGBA(image.Rect(0, 0, w, h))
4445
if len(pixels) == 1 {
4546
draw.Draw(m, m.Bounds(), &image.Uniform{pixels[0]}, image.ZP, draw.Src)
@@ -145,6 +146,77 @@ func TestTransform(t *testing.T) {
145146
}
146147
}
147148

149+
// Test that each of the eight EXIF orientations is applied to the transformed
150+
// image appropriately.
151+
func TestTransform_EXIF(t *testing.T) {
152+
ref := newImage(2, 2, red, green, blue, yellow)
153+
154+
// reference image encoded as TIF, with each of the 8 EXIF orientations
155+
// applied in reverse and the EXIF tag set. When orientation is
156+
// applied, each should display as the ref image.
157+
tests := []string{
158+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAAAQAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGQAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPrPwPAfDBn+////n+E/IAAA//9DzAj4AA==", // Orientation=1
159+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAAAgAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGQAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGL4z/D/PwPD////GcAUIAAA//9HyAj4AA==", // Orientation=2
160+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAAAwAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFwAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPr/n+E/AwOY/A9iAAIAAP//T8AI+AA=", // Orientation=3
161+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABAAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGgAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGJg+P///3+G//8ZGP6DICAAAP//S8QI+A==", // Orientation=4
162+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABQAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGAAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPrPwABC/xn+M/wHkYAAAAD//0PMCPg=", // Orientation=5
163+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABgAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGAAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGL4z/D/PwgzMIDQf0AAAAD//0vECPg=", // Orientation=6
164+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABwAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFgAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPr/nwECGf7/BxGAAAAA//9PwAj4", // Orientation=7
165+
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAACAAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFQAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGJg+P//P4QAQ0AAAAD//0fICPgA", // Orientation=8
166+
}
167+
168+
for _, src := range tests {
169+
in, err := base64.StdEncoding.DecodeString(src)
170+
if err != nil {
171+
t.Errorf("error decoding source: %v", err)
172+
}
173+
out, err := Transform(in, Options{Height: -1, Width: -1, Format: "tiff"})
174+
if err != nil {
175+
t.Errorf("Transform(%q) returned error: %v", src, err)
176+
}
177+
d, _, err := image.Decode(bytes.NewReader(out))
178+
if err != nil {
179+
t.Errorf("error decoding transformed image: %v", err)
180+
}
181+
182+
// construct new image with same colors as decoded image for easy comparison
183+
got := newImage(2, 2, d.At(0, 0), d.At(1, 0), d.At(0, 1), d.At(1, 1))
184+
if want := ref; !reflect.DeepEqual(got, want) {
185+
t.Errorf("Transform(%v) returned image %#v, want %#v", src, got, want)
186+
}
187+
}
188+
}
189+
190+
// Test that EXIF orientation and any additional transforms don't conflict.
191+
// This is tested with orientation=7, which involves both a rotation and a
192+
// flip, combined with an additional rotation transform.
193+
func TestTransform_EXIF_Rotate(t *testing.T) {
194+
// base64-encoded TIF image (2x2 yellow green blue red) with EXIF
195+
// orientation=7. When orientation applied, displays as (2x2 red green
196+
// blue yellow).
197+
src := "SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABwAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFgAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPr/nwECGf7/BxGAAAAA//9PwAj4"
198+
199+
in, err := base64.StdEncoding.DecodeString(src)
200+
if err != nil {
201+
t.Errorf("error decoding source: %v", err)
202+
}
203+
out, err := Transform(in, Options{Rotate: 90, Format: "tiff"})
204+
if err != nil {
205+
t.Errorf("Transform(%q) returned error: %v", src, err)
206+
}
207+
d, _, err := image.Decode(bytes.NewReader(out))
208+
if err != nil {
209+
t.Errorf("error decoding transformed image: %v", err)
210+
}
211+
212+
// construct new image with same colors as decoded image for easy comparison
213+
got := newImage(2, 2, d.At(0, 0), d.At(1, 0), d.At(0, 1), d.At(1, 1))
214+
want := newImage(2, 2, green, yellow, red, blue)
215+
if !reflect.DeepEqual(got, want) {
216+
t.Errorf("Transform(%v) returned image %#v, want %#v", src, got, want)
217+
}
218+
}
219+
148220
func TestTransformImage(t *testing.T) {
149221
// ref is a 2x2 reference image containing four colors
150222
ref := newImage(2, 2, red, green, blue, yellow)

0 commit comments

Comments
 (0)