mirror of
https://github.com/willnorris/imageproxy.git
synced 2024-12-16 21:56:43 -05:00
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
This commit is contained in:
parent
07c54b46e3
commit
67619a67ae
2 changed files with 138 additions and 1 deletions
65
transform.go
65
transform.go
|
@ -21,9 +21,11 @@ import (
|
|||
_ "image/gif" // register gif format
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"golang.org/x/image/tiff" // register tiff format
|
||||
_ "golang.org/x/image/webp" // register webp format
|
||||
"willnorris.com/go/gifresize"
|
||||
|
@ -32,6 +34,9 @@ import (
|
|||
// default compression quality of resized jpegs
|
||||
const defaultQuality = 95
|
||||
|
||||
// maximum distance into image to look for EXIF tags
|
||||
const maxExifSize = 1 << 20
|
||||
|
||||
// resample filter used when resizing images
|
||||
var resampleFilter = imaging.Lanczos
|
||||
|
||||
|
@ -50,6 +55,15 @@ func Transform(img []byte, opt Options) ([]byte, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// apply EXIF orientation for jpeg and tiff source images. Read at most
|
||||
// up to maxExifSize looking for EXIF tags.
|
||||
if format == "jpeg" || format == "tiff" {
|
||||
r := io.LimitReader(bytes.NewReader(img), maxExifSize)
|
||||
if exifOpt := exifOrientation(r); exifOpt.transform() {
|
||||
m = transformImage(m, exifOpt)
|
||||
}
|
||||
}
|
||||
|
||||
// encode webp and tiff as jpeg by default
|
||||
if format == "tiff" || format == "webp" {
|
||||
format = "jpeg"
|
||||
|
@ -187,6 +201,57 @@ func cropParams(m image.Image, opt Options) (x0, y0, x1, y1 int, crop bool) {
|
|||
return x0, y0, x1, y1, true
|
||||
}
|
||||
|
||||
// read EXIF orientation tag from r and adjust opt to orient image correctly.
|
||||
func exifOrientation(r io.Reader) (opt Options) {
|
||||
// Exif Orientation Tag values
|
||||
// http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
const (
|
||||
topLeftSide = 1
|
||||
topRightSide = 2
|
||||
bottomRightSide = 3
|
||||
bottomLeftSide = 4
|
||||
leftSideTop = 5
|
||||
rightSideTop = 6
|
||||
rightSideBottom = 7
|
||||
leftSideBottom = 8
|
||||
)
|
||||
|
||||
ex, err := exif.Decode(r)
|
||||
if err != nil {
|
||||
return opt
|
||||
}
|
||||
tag, err := ex.Get(exif.Orientation)
|
||||
if err != nil {
|
||||
return opt
|
||||
}
|
||||
orient, err := tag.Int(0)
|
||||
if err != nil {
|
||||
return opt
|
||||
}
|
||||
|
||||
switch orient {
|
||||
case topLeftSide:
|
||||
// do nothing
|
||||
case topRightSide:
|
||||
opt.FlipHorizontal = true
|
||||
case bottomRightSide:
|
||||
opt.Rotate = 180
|
||||
case bottomLeftSide:
|
||||
opt.FlipVertical = true
|
||||
case leftSideTop:
|
||||
opt.Rotate = 90
|
||||
opt.FlipVertical = true
|
||||
case rightSideTop:
|
||||
opt.Rotate = -90
|
||||
case rightSideBottom:
|
||||
opt.Rotate = 90
|
||||
opt.FlipHorizontal = true
|
||||
case leftSideBottom:
|
||||
opt.Rotate = 90
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
// transformImage modifies the image m based on the transformations specified
|
||||
// in opt.
|
||||
func transformImage(m image.Image, opt Options) image.Image {
|
||||
|
|
|
@ -16,6 +16,7 @@ package imageproxy
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
|
@ -39,7 +40,7 @@ var (
|
|||
// newImage creates a new NRGBA image with the specified dimensions and pixel
|
||||
// color data. If the length of pixels is 1, the entire image is filled with
|
||||
// that color.
|
||||
func newImage(w, h int, pixels ...color.NRGBA) image.Image {
|
||||
func newImage(w, h int, pixels ...color.Color) image.Image {
|
||||
m := image.NewNRGBA(image.Rect(0, 0, w, h))
|
||||
if len(pixels) == 1 {
|
||||
draw.Draw(m, m.Bounds(), &image.Uniform{pixels[0]}, image.ZP, draw.Src)
|
||||
|
@ -145,6 +146,77 @@ func TestTransform(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Test that each of the eight EXIF orientations is applied to the transformed
|
||||
// image appropriately.
|
||||
func TestTransform_EXIF(t *testing.T) {
|
||||
ref := newImage(2, 2, red, green, blue, yellow)
|
||||
|
||||
// reference image encoded as TIF, with each of the 8 EXIF orientations
|
||||
// applied in reverse and the EXIF tag set. When orientation is
|
||||
// applied, each should display as the ref image.
|
||||
tests := []string{
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAAAQAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGQAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPrPwPAfDBn+////n+E/IAAA//9DzAj4AA==", // Orientation=1
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAAAgAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGQAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGL4z/D/PwPD////GcAUIAAA//9HyAj4AA==", // Orientation=2
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAAAwAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFwAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPr/n+E/AwOY/A9iAAIAAP//T8AI+AA=", // Orientation=3
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABAAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGgAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGJg+P///3+G//8ZGP6DICAAAP//S8QI+A==", // Orientation=4
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABQAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGAAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPrPwABC/xn+M/wHkYAAAAD//0PMCPg=", // Orientation=5
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABgAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAGAAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGL4z/D/PwgzMIDQf0AAAAD//0vECPg=", // Orientation=6
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABwAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFgAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPr/nwECGf7/BxGAAAAA//9PwAj4", // Orientation=7
|
||||
"SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAACAAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFQAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nGJg+P//P4QAQ0AAAAD//0fICPgA", // Orientation=8
|
||||
}
|
||||
|
||||
for _, src := range tests {
|
||||
in, err := base64.StdEncoding.DecodeString(src)
|
||||
if err != nil {
|
||||
t.Errorf("error decoding source: %v", err)
|
||||
}
|
||||
out, err := Transform(in, Options{Height: -1, Width: -1, Format: "tiff"})
|
||||
if err != nil {
|
||||
t.Errorf("Transform(%q) returned error: %v", src, err)
|
||||
}
|
||||
d, _, err := image.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
t.Errorf("error decoding transformed image: %v", err)
|
||||
}
|
||||
|
||||
// construct new image with same colors as decoded image for easy comparison
|
||||
got := newImage(2, 2, d.At(0, 0), d.At(1, 0), d.At(0, 1), d.At(1, 1))
|
||||
if want := ref; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Transform(%v) returned image %#v, want %#v", src, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that EXIF orientation and any additional transforms don't conflict.
|
||||
// This is tested with orientation=7, which involves both a rotation and a
|
||||
// flip, combined with an additional rotation transform.
|
||||
func TestTransform_EXIF_Rotate(t *testing.T) {
|
||||
// base64-encoded TIF image (2x2 yellow green blue red) with EXIF
|
||||
// orientation=7. When orientation applied, displays as (2x2 red green
|
||||
// blue yellow).
|
||||
src := "SUkqAAgAAAAOAAABAwABAAAAAgAAAAEBAwABAAAAAgAAAAIBAwAEAAAAtgAAAAMBAwABAAAACAAAAAYBAwABAAAAAgAAABEBBAABAAAAzgAAABIBAwABAAAABwAAABUBAwABAAAABAAAABYBAwABAAAAAgAAABcBBAABAAAAFgAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAAFIBAwABAAAAAgAAAAAAAAAIAAgACAAIAEgAAAABAAAASAAAAAEAAAB4nPr/nwECGf7/BxGAAAAA//9PwAj4"
|
||||
|
||||
in, err := base64.StdEncoding.DecodeString(src)
|
||||
if err != nil {
|
||||
t.Errorf("error decoding source: %v", err)
|
||||
}
|
||||
out, err := Transform(in, Options{Rotate: 90, Format: "tiff"})
|
||||
if err != nil {
|
||||
t.Errorf("Transform(%q) returned error: %v", src, err)
|
||||
}
|
||||
d, _, err := image.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
t.Errorf("error decoding transformed image: %v", err)
|
||||
}
|
||||
|
||||
// construct new image with same colors as decoded image for easy comparison
|
||||
got := newImage(2, 2, d.At(0, 0), d.At(1, 0), d.At(0, 1), d.At(1, 1))
|
||||
want := newImage(2, 2, green, yellow, red, blue)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Transform(%v) returned image %#v, want %#v", src, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformImage(t *testing.T) {
|
||||
// ref is a 2x2 reference image containing four colors
|
||||
ref := newImage(2, 2, red, green, blue, yellow)
|
||||
|
|
Loading…
Reference in a new issue