chore: add image rotation preserve 847a50ae
Steve Simkins · 2026-05-29 23:29 1 file(s) · +138 −0
apps/cellar/image.go +138 −0
2 2
3 3
import (
4 4
	"bytes"
5 +
	"encoding/binary"
5 6
	"fmt"
6 7
	"image"
8 +
	"image/draw"
7 9
	"image/jpeg"
8 10
	_ "image/png"
9 11
)
10 12
11 13
func processImage(data []byte) ([]byte, error) {
14 +
	orientation := readOrientation(data)
12 15
	img, _, err := image.Decode(bytes.NewReader(data))
13 16
	if err != nil {
14 17
		return nil, fmt.Errorf("Failed to decode image: %w", err)
15 18
	}
19 +
	img = applyOrientation(img, orientation)
16 20
	var out bytes.Buffer
17 21
	if err := jpeg.Encode(&out, img, &jpeg.Options{Quality: 75}); err != nil {
18 22
		return nil, fmt.Errorf("JPEG encoding failed: %w", err)
19 23
	}
20 24
	return out.Bytes(), nil
21 25
}
26 +
27 +
func readOrientation(data []byte) int {
28 +
	exif := extractExifSegment(data)
29 +
	if len(exif) < 8 {
30 +
		return 1
31 +
	}
32 +
	var order binary.ByteOrder
33 +
	switch string(exif[:2]) {
34 +
	case "II":
35 +
		order = binary.LittleEndian
36 +
	case "MM":
37 +
		order = binary.BigEndian
38 +
	default:
39 +
		return 1
40 +
	}
41 +
	if order.Uint16(exif[2:4]) != 42 {
42 +
		return 1
43 +
	}
44 +
	ifd0 := int(order.Uint32(exif[4:8]))
45 +
	if ifd0 < 0 || ifd0+2 > len(exif) {
46 +
		return 1
47 +
	}
48 +
	count := int(order.Uint16(exif[ifd0 : ifd0+2]))
49 +
	entries := ifd0 + 2
50 +
	for i := 0; i < count; i++ {
51 +
		entry := entries + i*12
52 +
		if entry+12 > len(exif) {
53 +
			return 1
54 +
		}
55 +
		tag := order.Uint16(exif[entry : entry+2])
56 +
		if tag == 0x0112 { // Orientation
57 +
			return int(order.Uint16(exif[entry+8 : entry+10]))
58 +
		}
59 +
	}
60 +
	return 1
61 +
}
62 +
63 +
func extractExifSegment(orig []byte) []byte {
64 +
	const prefix = "Exif\x00\x00"
65 +
	if len(orig) < 4 || orig[0] != 0xff || orig[1] != 0xd8 {
66 +
		return nil
67 +
	}
68 +
	pos := 2
69 +
	for pos+4 <= len(orig) {
70 +
		if orig[pos] != 0xff {
71 +
			return nil
72 +
		}
73 +
		marker := orig[pos+1]
74 +
		pos += 2
75 +
		if marker == 0xda || marker == 0xd9 {
76 +
			return nil
77 +
		}
78 +
		if marker >= 0xd0 && marker <= 0xd7 {
79 +
			continue
80 +
		}
81 +
		if pos+2 > len(orig) {
82 +
			return nil
83 +
		}
84 +
		segLen := int(binary.BigEndian.Uint16(orig[pos : pos+2]))
85 +
		if segLen < 2 || pos+segLen > len(orig) {
86 +
			return nil
87 +
		}
88 +
		payload := orig[pos+2 : pos+segLen]
89 +
		if marker == 0xe1 && len(payload) >= len(prefix) && string(payload[:len(prefix)]) == prefix {
90 +
			return payload[len(prefix):]
91 +
		}
92 +
		pos += segLen
93 +
	}
94 +
	return nil
95 +
}
96 +
97 +
func applyOrientation(src image.Image, orientation int) image.Image {
98 +
	if orientation <= 1 || orientation > 8 {
99 +
		return src
100 +
	}
101 +
	b := src.Bounds()
102 +
	w, h := b.Dx(), b.Dy()
103 +
	rgba := image.NewRGBA(image.Rect(0, 0, w, h))
104 +
	draw.Draw(rgba, rgba.Bounds(), src, b.Min, draw.Src)
105 +
106 +
	var dst *image.RGBA
107 +
	switch orientation {
108 +
	case 2: // flip horizontal
109 +
		dst = image.NewRGBA(image.Rect(0, 0, w, h))
110 +
		for y := 0; y < h; y++ {
111 +
			for x := 0; x < w; x++ {
112 +
				dst.Set(w-1-x, y, rgba.At(x, y))
113 +
			}
114 +
		}
115 +
	case 3: // rotate 180
116 +
		dst = image.NewRGBA(image.Rect(0, 0, w, h))
117 +
		for y := 0; y < h; y++ {
118 +
			for x := 0; x < w; x++ {
119 +
				dst.Set(w-1-x, h-1-y, rgba.At(x, y))
120 +
			}
121 +
		}
122 +
	case 4: // flip vertical
123 +
		dst = image.NewRGBA(image.Rect(0, 0, w, h))
124 +
		for y := 0; y < h; y++ {
125 +
			for x := 0; x < w; x++ {
126 +
				dst.Set(x, h-1-y, rgba.At(x, y))
127 +
			}
128 +
		}
129 +
	case 5: // transpose
130 +
		dst = image.NewRGBA(image.Rect(0, 0, h, w))
131 +
		for y := 0; y < h; y++ {
132 +
			for x := 0; x < w; x++ {
133 +
				dst.Set(y, x, rgba.At(x, y))
134 +
			}
135 +
		}
136 +
	case 6: // rotate 90 CW
137 +
		dst = image.NewRGBA(image.Rect(0, 0, h, w))
138 +
		for y := 0; y < h; y++ {
139 +
			for x := 0; x < w; x++ {
140 +
				dst.Set(h-1-y, x, rgba.At(x, y))
141 +
			}
142 +
		}
143 +
	case 7: // transverse
144 +
		dst = image.NewRGBA(image.Rect(0, 0, h, w))
145 +
		for y := 0; y < h; y++ {
146 +
			for x := 0; x < w; x++ {
147 +
				dst.Set(h-1-y, w-1-x, rgba.At(x, y))
148 +
			}
149 +
		}
150 +
	case 8: // rotate 270 CW (90 CCW)
151 +
		dst = image.NewRGBA(image.Rect(0, 0, h, w))
152 +
		for y := 0; y < h; y++ {
153 +
			for x := 0; x < w; x++ {
154 +
				dst.Set(y, w-1-x, rgba.At(x, y))
155 +
			}
156 +
		}
157 +
	}
158 +
	return dst
159 +
}