From 0e934e02cdc9eabf534aff2bb33c137a4e937d5c Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 16 Aug 2026 16:29:03 -0700 Subject: [PATCH] Apply EXIF orientation when generating thumbnails --- go.mod | 3 +- go.sum | 4 +-- internal/blob/blob.go | 68 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 28854b7..a0bb72a 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.26 require ( github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f github.com/chromedp/chromedp v0.16.0 + github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd + golang.org/x/image v0.45.0 google.golang.org/api v0.293.0 ) @@ -31,7 +33,6 @@ require ( go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect golang.org/x/crypto v0.54.0 // indirect - golang.org/x/image v0.45.0 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index 84bd9bd..3b9e366 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -78,8 +80,6 @@ golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= diff --git a/internal/blob/blob.go b/internal/blob/blob.go index 39a7b82..7be6c09 100644 --- a/internal/blob/blob.go +++ b/internal/blob/blob.go @@ -18,6 +18,7 @@ import ( _ "image/gif" _ "image/png" + "github.com/rwcarlsen/goexif/exif" "golang.org/x/image/draw" _ "golang.org/x/image/webp" @@ -320,16 +321,77 @@ func thumbnail(src []byte) ([]byte, error) { if err != nil { return nil, err } + o := orientation(src) bounds := img.Bounds() - if bounds.Dx() > thumbWidth { - height := bounds.Dy() * thumbWidth / bounds.Dx() - scaled := image.NewRGBA(image.Rect(0, 0, thumbWidth, height)) + displayWidth := bounds.Dx() + if o >= 5 { + displayWidth = bounds.Dy() + } + if displayWidth > thumbWidth { + w := bounds.Dx() * thumbWidth / displayWidth + h := bounds.Dy() * thumbWidth / displayWidth + scaled := image.NewRGBA(image.Rect(0, 0, w, h)) draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, bounds, draw.Over, nil) img = scaled } + img = reorient(img, o) var buf bytes.Buffer if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 80}); err != nil { return nil, err } return buf.Bytes(), nil } + +func orientation(src []byte) (o int) { + o = 1 + defer func() { recover() }() + parsed, err := exif.Decode(bytes.NewReader(src)) + if err != nil { + return + } + tag, err := parsed.Get(exif.Orientation) + if err != nil { + return + } + value, err := tag.Int(0) + if err != nil || value < 1 || value > 8 { + return + } + return value +} + +func reorient(img image.Image, o int) image.Image { + if o == 1 { + return img + } + b := img.Bounds() + w, h := b.Dx(), b.Dy() + dw, dh := w, h + if o >= 5 { + dw, dh = h, w + } + out := image.NewRGBA(image.Rect(0, 0, dw, dh)) + for y := range h { + for x := range w { + var dx, dy int + switch o { + case 2: + dx, dy = w-1-x, y + case 3: + dx, dy = w-1-x, h-1-y + case 4: + dx, dy = x, h-1-y + case 5: + dx, dy = y, x + case 6: + dx, dy = h-1-y, x + case 7: + dx, dy = h-1-y, w-1-x + case 8: + dx, dy = y, w-1-x + } + out.Set(dx, dy, img.At(b.Min.X+x, b.Min.Y+y)) + } + } + return out +}