├── LICENSE ├── README.md ├── example_test.go ├── go.mod ├── go.sum ├── imageorient.go ├── imageorient_test.go └── testdata ├── example_output.jpg ├── orientation_0.jpg ├── orientation_1.jpg ├── orientation_2.jpg ├── orientation_3.jpg ├── orientation_4.jpg ├── orientation_5.jpg ├── orientation_6.jpg ├── orientation_7.jpg └── orientation_8.jpg /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Grigory Dryapak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imageorient 2 | 3 | [![GoDoc](https://godoc.org/github.com/disintegration/imageorient?status.svg)](https://godoc.org/github.com/disintegration/imageorient) 4 | 5 | Package `imageorient` provides image decoding functions similar to standard library's 6 | `image.Decode` and `image.DecodeConfig` with the addition that they also handle the 7 | EXIF orientation tag (if present). 8 | 9 | License: MIT. 10 | 11 | See also: http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ 12 | 13 | ## Install / Update 14 | 15 | go get -u github.com/disintegration/imageorient 16 | 17 | ## Documentation 18 | 19 | http://godoc.org/github.com/disintegration/imageorient 20 | 21 | ## Usage example 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "image/jpeg" 28 | "log" 29 | "os" 30 | 31 | "github.com/disintegration/imageorient" 32 | ) 33 | 34 | func main() { 35 | // Open the test image. This particular image have the EXIF 36 | // orientation tag set to 3 (rotated by 180 deg). 37 | f, err := os.Open("testdata/orientation_3.jpg") 38 | if err != nil { 39 | log.Fatalf("os.Open failed: %v", err) 40 | } 41 | 42 | // Decode the test image using the imageorient.Decode function 43 | // to handle the image orientation correctly. 44 | img, _, err := imageorient.Decode(f) 45 | if err != nil { 46 | log.Fatalf("imageorient.Decode failed: %v", err) 47 | } 48 | 49 | // Save the decoded image to a new file. If we used image.Decode 50 | // instead of imageorient.Decode on the previous step, the saved 51 | // image would appear rotated. 52 | f, err = os.Create("testdata/example_output.jpg") 53 | if err != nil { 54 | log.Fatalf("os.Create failed: %v", err) 55 | } 56 | err = jpeg.Encode(f, img, nil) 57 | if err != nil { 58 | log.Fatalf("jpeg.Encode failed: %v", err) 59 | } 60 | } 61 | ``` -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package imageorient_test 2 | 3 | import ( 4 | "image/jpeg" 5 | "log" 6 | "os" 7 | 8 | "github.com/disintegration/imageorient" 9 | ) 10 | 11 | func ExampleDecode() { 12 | // Open the test image. This particular image has the EXIF 13 | // orientation tag set to 3 (rotated by 180 deg). 14 | f, err := os.Open("testdata/orientation_3.jpg") 15 | if err != nil { 16 | log.Fatalf("os.Open failed: %v", err) 17 | } 18 | 19 | // Decode the test image using the imageorient.Decode function 20 | // to handle the image orientation correctly. 21 | img, _, err := imageorient.Decode(f) 22 | if err != nil { 23 | log.Fatalf("imageorient.Decode failed: %v", err) 24 | } 25 | 26 | // Save the decoded image to a new file. If we used image.Decode 27 | // instead of imageorient.Decode on the previous step, the saved 28 | // image would appear rotated. 29 | f, err = os.Create("testdata/example_output.jpg") 30 | if err != nil { 31 | log.Fatalf("os.Create failed: %v", err) 32 | } 33 | err = jpeg.Encode(f, img, nil) 34 | if err != nil { 35 | log.Fatalf("jpeg.Encode failed: %v", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/disintegration/imageorient 2 | 3 | require github.com/disintegration/gift v1.1.2 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/disintegration/gift v1.1.2 h1:9ZyHJr+kPamiH10FX3Pynt1AxFUob812bU9Wt4GMzhs= 2 | github.com/disintegration/gift v1.1.2/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= 3 | -------------------------------------------------------------------------------- /imageorient.go: -------------------------------------------------------------------------------- 1 | // Package imageorient provides image decoding functions similar to standard library's 2 | // image.Decode and image.DecodeConfig with the addition that they also handle the 3 | // EXIF orientation tag (if present). 4 | // 5 | // See also: http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/ 6 | // 7 | package imageorient 8 | 9 | import ( 10 | "bytes" 11 | "encoding/binary" 12 | "image" 13 | "io" 14 | "io/ioutil" 15 | 16 | "github.com/disintegration/gift" 17 | ) 18 | 19 | // maxBufLen is the maximum size of a buffer that should be enough to read 20 | // the EXIF metadata. According to the EXIF specs, it is located inside the 21 | // APP1 block that goes right after the start of image (SOI). 22 | const maxBufLen = 1 << 20 23 | 24 | // Decode decodes an image and changes its orientation 25 | // according to the EXIF orientation tag (if present). 26 | func Decode(r io.Reader) (image.Image, string, error) { 27 | orientation, r := getOrientation(r) 28 | 29 | img, format, err := image.Decode(r) 30 | if err != nil { 31 | return img, format, err 32 | } 33 | 34 | return fixOrientation(img, orientation), format, nil 35 | } 36 | 37 | // DecodeConfig decodes the color model and dimensions of an image 38 | // with the respect to the EXIF orientation tag (if present). 39 | // 40 | // Note that after using imageorient.Decode on the same image, 41 | // the color model of the decoded image may be different if the 42 | // orientation-related transformation is needed. 43 | func DecodeConfig(r io.Reader) (image.Config, string, error) { 44 | orientation, r := getOrientation(r) 45 | 46 | cfg, format, err := image.DecodeConfig(r) 47 | if err != nil { 48 | return cfg, format, err 49 | } 50 | 51 | if orientation >= 5 && orientation <= 8 { 52 | cfg.Width, cfg.Height = cfg.Height, cfg.Width 53 | } 54 | 55 | return cfg, format, nil 56 | } 57 | 58 | // getOrientation returns the EXIF orientation tag from the given image 59 | // and a new io.Reader with the same state as the original reader r. 60 | func getOrientation(r io.Reader) (int, io.Reader) { 61 | buf := new(bytes.Buffer) 62 | tr := io.TeeReader(io.LimitReader(r, maxBufLen), buf) 63 | orientation := readOrientation(tr) 64 | return orientation, io.MultiReader(buf, r) 65 | } 66 | 67 | // readOrientation reads the EXIF orientation tag from the given image. 68 | // It returns 0 if the orientation tag is not found or invalid. 69 | func readOrientation(r io.Reader) int { 70 | const ( 71 | markerSOI = 0xffd8 72 | markerAPP1 = 0xffe1 73 | exifHeader = 0x45786966 74 | byteOrderBE = 0x4d4d 75 | byteOrderLE = 0x4949 76 | orientationTag = 0x0112 77 | ) 78 | 79 | // Check if JPEG SOI marker is present. 80 | var soi uint16 81 | if err := binary.Read(r, binary.BigEndian, &soi); err != nil { 82 | return 0 83 | } 84 | if soi != markerSOI { 85 | return 0 // Missing JPEG SOI marker. 86 | } 87 | 88 | // Find JPEG APP1 marker. 89 | for { 90 | var marker, size uint16 91 | if err := binary.Read(r, binary.BigEndian, &marker); err != nil { 92 | return 0 93 | } 94 | if err := binary.Read(r, binary.BigEndian, &size); err != nil { 95 | return 0 96 | } 97 | if marker>>8 != 0xff { 98 | return 0 // Invalid JPEG marker. 99 | } 100 | if marker == markerAPP1 { 101 | break 102 | } 103 | if size < 2 { 104 | return 0 // Invalid block size. 105 | } 106 | if _, err := io.CopyN(ioutil.Discard, r, int64(size-2)); err != nil { 107 | return 0 108 | } 109 | } 110 | 111 | // Check if EXIF header is present. 112 | var header uint32 113 | if err := binary.Read(r, binary.BigEndian, &header); err != nil { 114 | return 0 115 | } 116 | if header != exifHeader { 117 | return 0 118 | } 119 | if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { 120 | return 0 121 | } 122 | 123 | // Read byte order information. 124 | var ( 125 | byteOrderTag uint16 126 | byteOrder binary.ByteOrder 127 | ) 128 | if err := binary.Read(r, binary.BigEndian, &byteOrderTag); err != nil { 129 | return 0 130 | } 131 | switch byteOrderTag { 132 | case byteOrderBE: 133 | byteOrder = binary.BigEndian 134 | case byteOrderLE: 135 | byteOrder = binary.LittleEndian 136 | default: 137 | return 0 // Invalid byte order flag. 138 | } 139 | if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { 140 | return 0 141 | } 142 | 143 | // Skip the EXIF offset. 144 | var offset uint32 145 | if err := binary.Read(r, byteOrder, &offset); err != nil { 146 | return 0 147 | } 148 | if offset < 8 { 149 | return 0 // Invalid offset value. 150 | } 151 | if _, err := io.CopyN(ioutil.Discard, r, int64(offset-8)); err != nil { 152 | return 0 153 | } 154 | 155 | // Read the number of tags. 156 | var numTags uint16 157 | if err := binary.Read(r, byteOrder, &numTags); err != nil { 158 | return 0 159 | } 160 | 161 | // Find the orientation tag. 162 | for i := 0; i < int(numTags); i++ { 163 | var tag uint16 164 | if err := binary.Read(r, byteOrder, &tag); err != nil { 165 | return 0 166 | } 167 | if tag != orientationTag { 168 | if _, err := io.CopyN(ioutil.Discard, r, 10); err != nil { 169 | return 0 170 | } 171 | continue 172 | } 173 | if _, err := io.CopyN(ioutil.Discard, r, 6); err != nil { 174 | return 0 175 | } 176 | var val uint16 177 | if err := binary.Read(r, byteOrder, &val); err != nil { 178 | return 0 179 | } 180 | if val < 1 || val > 8 { 181 | return 0 // Invalid tag value. 182 | } 183 | return int(val) 184 | } 185 | return 0 // Missing orientation tag. 186 | } 187 | 188 | // Filters needed to fix the given image orientation. 189 | var filters = map[int]gift.Filter{ 190 | 2: gift.FlipHorizontal(), 191 | 3: gift.Rotate180(), 192 | 4: gift.FlipVertical(), 193 | 5: gift.Transpose(), 194 | 6: gift.Rotate270(), 195 | 7: gift.Transverse(), 196 | 8: gift.Rotate90(), 197 | } 198 | 199 | // fixOrientation changes the image orientation based on the EXIF orientation tag value. 200 | func fixOrientation(img image.Image, orientation int) image.Image { 201 | filter, ok := filters[orientation] 202 | if !ok { 203 | return img 204 | } 205 | g := gift.New(filter) 206 | newImg := image.NewRGBA(g.Bounds(img.Bounds())) 207 | g.Draw(newImg, img) 208 | return newImg 209 | } 210 | -------------------------------------------------------------------------------- /imageorient_test.go: -------------------------------------------------------------------------------- 1 | package imageorient 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "image/color" 7 | _ "image/jpeg" 8 | "os" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | var testFiles = []struct { 14 | path string 15 | orientation int 16 | }{ 17 | {"testdata/orientation_0.jpg", 0}, 18 | {"testdata/orientation_1.jpg", 1}, 19 | {"testdata/orientation_2.jpg", 2}, 20 | {"testdata/orientation_3.jpg", 3}, 21 | {"testdata/orientation_4.jpg", 4}, 22 | {"testdata/orientation_5.jpg", 5}, 23 | {"testdata/orientation_6.jpg", 6}, 24 | {"testdata/orientation_7.jpg", 7}, 25 | {"testdata/orientation_8.jpg", 8}, 26 | } 27 | 28 | const ( 29 | testWidth = 50 30 | testHeight = 70 31 | ) 32 | 33 | func TestReadOrientation(t *testing.T) { 34 | for _, tf := range testFiles { 35 | f, err := os.Open(tf.path) 36 | if err != nil { 37 | t.Fatalf("os.Open(%q): %v", tf.path, err) 38 | } 39 | 40 | o := readOrientation(f) 41 | if o != tf.orientation { 42 | t.Fatalf("expected orientation=%d but got %d (%s)", tf.orientation, o, tf.path) 43 | } 44 | } 45 | } 46 | 47 | func TestDecodeConfig(t *testing.T) { 48 | for _, tf := range testFiles { 49 | f, err := os.Open(tf.path) 50 | if err != nil { 51 | t.Fatalf("Open(%q): %v", tf.path, err) 52 | } 53 | 54 | cfg, format, err := DecodeConfig(f) 55 | if err != nil { 56 | t.Fatalf("Decode(%q): %v", tf.path, err) 57 | } 58 | 59 | if cfg.Width != testWidth { 60 | t.Fatalf("bad decoded width (%q): want %d got %d", tf.path, testWidth, cfg.Width) 61 | } 62 | 63 | if cfg.Height != testHeight { 64 | t.Fatalf("bad decoded width (%q): want %d got %d", tf.path, testHeight, cfg.Height) 65 | } 66 | 67 | if format != "jpeg" { 68 | t.Fatalf("bad decoded format (%q): %s", tf.path, format) 69 | } 70 | } 71 | 72 | _, _, err := DecodeConfig(strings.NewReader("bad data")) 73 | if err == nil { 74 | t.Fatalf("expected error on bad data") 75 | } 76 | } 77 | 78 | func TestDecode(t *testing.T) { 79 | f, err := os.Open("testdata/orientation_0.jpg") 80 | if err != nil { 81 | t.Fatalf("os.Open(%q): %v", "testdata/orientation_0.jpg", err) 82 | } 83 | orig, _, err := image.Decode(f) 84 | if err != nil { 85 | t.Fatalf("image.Decode(%q): %v", "testdata/orientation_0.jpg", err) 86 | } 87 | 88 | for _, tf := range testFiles { 89 | f, err := os.Open(tf.path) 90 | if err != nil { 91 | t.Fatalf("Open(%q): %v", tf.path, err) 92 | } 93 | 94 | img, format, err := Decode(f) 95 | if err != nil { 96 | t.Fatalf("Decode(%q): %v", tf.path, err) 97 | } 98 | 99 | if err := compareImages(img, orig); err != nil { 100 | t.Fatalf("decoded image differs from orig (%q): %v", tf.path, err) 101 | } 102 | 103 | if img.Bounds().Dx() != testWidth { 104 | t.Fatalf("bad decoded width (%q): want %d got %d", tf.path, testWidth, img.Bounds().Dx()) 105 | } 106 | 107 | if img.Bounds().Dy() != testHeight { 108 | t.Fatalf("bad decoded width (%q): want %d got %d", tf.path, testHeight, img.Bounds().Dy()) 109 | } 110 | 111 | if format != "jpeg" { 112 | t.Fatalf("bad decoded format (%q): %s", tf.path, format) 113 | } 114 | } 115 | 116 | _, _, err = Decode(strings.NewReader("bad data")) 117 | if err == nil { 118 | t.Fatalf("expected error on bad data") 119 | } 120 | } 121 | 122 | func compareImages(m1, m2 image.Image) error { 123 | if m1.Bounds() != m2.Bounds() { 124 | return fmt.Errorf("bounds not equal: %v vs %v", m1.Bounds(), m2.Bounds()) 125 | } 126 | for x := m1.Bounds().Min.X; x < m1.Bounds().Max.X; x++ { 127 | for y := m1.Bounds().Min.Y; y < m1.Bounds().Max.Y; y++ { 128 | c1 := color.GrayModel.Convert(m1.At(x, y)).(color.Gray) 129 | c2 := color.GrayModel.Convert(m2.At(x, y)).(color.Gray) 130 | d := int(c1.Y) - int(c2.Y) 131 | if d < -5 || d > 5 { 132 | return fmt.Errorf("pixels at (%d, %d) not equal: %v vs %v", x, y, c1, c2) 133 | } 134 | } 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /testdata/example_output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/example_output.jpg -------------------------------------------------------------------------------- /testdata/orientation_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_0.jpg -------------------------------------------------------------------------------- /testdata/orientation_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_1.jpg -------------------------------------------------------------------------------- /testdata/orientation_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_2.jpg -------------------------------------------------------------------------------- /testdata/orientation_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_3.jpg -------------------------------------------------------------------------------- /testdata/orientation_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_4.jpg -------------------------------------------------------------------------------- /testdata/orientation_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_5.jpg -------------------------------------------------------------------------------- /testdata/orientation_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_6.jpg -------------------------------------------------------------------------------- /testdata/orientation_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_7.jpg -------------------------------------------------------------------------------- /testdata/orientation_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/disintegration/imageorient/8147d86e83ec79f4e95cd95ec4c798b492478159/testdata/orientation_8.jpg --------------------------------------------------------------------------------