1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
package main
import (
"encoding/binary"
"image"
"image/draw"
)
// EXIF orientation
// ============================================================================
//
// A photo taken on a phone is almost always stored in the sensor's native
// landscape orientation, with a separate EXIF tag saying how a viewer should
// rotate it. Browsers honour that tag — but only if it survives, and we
// deliberately strip metadata from the derivatives (it is a privacy leak: EXIF
// carries GPS coordinates, camera serial numbers and timestamps).
//
// So the rotation has to be baked into the pixels instead. This reads the one
// tag we care about and applies it, after which the image is upright and
// orientation-free by construction.
//
// This is a deliberately minimal EXIF reader — enough to find one SHORT in
// IFD0 — rather than a dependency on a full metadata library.
// Orientation values as defined by EXIF. The names describe the transform that
// must be applied to *display* the image correctly.
const (
orientNormal = 1 // no transform
orientFlipH = 2 // mirror horizontally
orientRotate180 = 3
orientFlipV = 4 // mirror vertically
orientTransposed = 5 // mirror horizontally, then rotate 270° clockwise
orientRotate90 = 6 // rotate 90° clockwise
orientTransverse = 7 // mirror horizontally, then rotate 90° clockwise
orientRotate270 = 8 // rotate 270° clockwise (i.e. 90° counter-clockwise)
orientMax = 8
exifOrientationTag = 0x0112
)
// jpegOrientation extracts the EXIF orientation from JPEG bytes.
//
// It returns orientNormal for anything it cannot make sense of — a non-JPEG, a
// JPEG without EXIF, a truncated or malformed segment. A photo displayed
// unrotated is a much better failure mode than an error that rejects the
// upload, and untrusted input must not be able to cause a panic here, so every
// read is bounds-checked.
func jpegOrientation(data []byte) int {
// SOI
if len(data) < 4 || data[0] != 0xFF || data[1] != 0xD8 {
return orientNormal
}
i := 2
for {
// Each segment: 0xFF, marker, 2-byte big-endian length (including the
// length field itself).
if i+4 > len(data) || data[i] != 0xFF {
return orientNormal
}
marker := data[i+1]
// Standalone markers without a payload: padding (0xFF fill bytes) and
// the start of compressed data, past which no metadata appears.
if marker == 0xD8 || (marker >= 0xD0 && marker <= 0xD9) {
i += 2
continue
}
if marker == 0xDA { // start of scan — image data follows
return orientNormal
}
length := int(binary.BigEndian.Uint16(data[i+2 : i+4]))
if length < 2 || i+2+length > len(data) {
return orientNormal
}
if marker == 0xE1 { // APP1, where EXIF lives
payload := data[i+4 : i+2+length]
if o, ok := exifOrientationFromAPP1(payload); ok {
return o
}
}
i += 2 + length
}
}
// exifOrientationFromAPP1 parses an APP1 payload ("Exif\0\0" + TIFF block) and
// returns the orientation tag from IFD0.
func exifOrientationFromAPP1(p []byte) (int, bool) {
const header = "Exif\x00\x00"
if len(p) < len(header)+8 || string(p[:len(header)]) != header {
return 0, false
}
// All offsets inside the TIFF block are relative to its own start.
tiff := p[len(header):]
var bo binary.ByteOrder
switch {
case tiff[0] == 'I' && tiff[1] == 'I':
bo = binary.LittleEndian
case tiff[0] == 'M' && tiff[1] == 'M':
bo = binary.BigEndian
default:
return 0, false
}
if bo.Uint16(tiff[2:4]) != 42 { // TIFF magic
return 0, false
}
ifdOff := int(bo.Uint32(tiff[4:8]))
if ifdOff < 8 || ifdOff+2 > len(tiff) {
return 0, false
}
count := int(bo.Uint16(tiff[ifdOff : ifdOff+2]))
entries := ifdOff + 2
for n := 0; n < count; n++ {
e := entries + n*12
if e+12 > len(tiff) {
return 0, false
}
if bo.Uint16(tiff[e:e+2]) != exifOrientationTag {
continue
}
// A SHORT fits in the 4-byte value field, stored in its first two
// bytes; no need to follow an offset.
v := int(bo.Uint16(tiff[e+8 : e+10]))
if v < 1 || v > orientMax {
return 0, false
}
return v, true
}
return 0, false
}
// applyOrientation returns the image rotated and/or mirrored so that it is
// upright, per the EXIF orientation value.
//
// The source is returned unchanged for the (overwhelmingly common) normal
// orientation, so the non-rotated path costs nothing.
func applyOrientation(src image.Image, orientation int) image.Image {
if orientation <= orientNormal || orientation > orientMax {
return src
}
b := src.Bounds()
w, h := b.Dx(), b.Dy()
// Orientations 5–8 exchange the axes, so the destination is transposed.
swap := orientation >= orientTransposed
dw, dh := w, h
if swap {
dw, dh = h, w
}
dst := image.NewNRGBA(image.Rect(0, 0, dw, dh))
// Copy pixel by pixel, mapping each source coordinate to its destination.
// An image is at most a few tens of megapixels and this runs once per
// upload, so clarity beats a per-orientation specialised blit.
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
var nx, ny int
switch orientation {
case orientFlipH:
nx, ny = w-1-x, y
case orientRotate180:
nx, ny = w-1-x, h-1-y
case orientFlipV:
nx, ny = x, h-1-y
case orientTransposed:
nx, ny = y, x
case orientRotate90:
nx, ny = h-1-y, x
case orientTransverse:
nx, ny = h-1-y, w-1-x
case orientRotate270:
nx, ny = y, w-1-x
}
dst.Set(nx, ny, src.At(b.Min.X+x, b.Min.Y+y))
}
}
return dst
}
// toNRGBA converts any image to NRGBA, which is the layout the PAM encoder
// writes and the resizer works on. Already-NRGBA images are returned as is.
func toNRGBA(src image.Image) *image.NRGBA {
if n, ok := src.(*image.NRGBA); ok {
return n
}
b := src.Bounds()
dst := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
draw.Draw(dst, dst.Bounds(), src, b.Min, draw.Src)
return dst
}
|