diff --git a/pkg/server/image.go b/pkg/server/image.go index 88c102782..c242cd4a8 100644 --- a/pkg/server/image.go +++ b/pkg/server/image.go @@ -41,6 +41,7 @@ type ImageHandler struct { Fetcher blobref.StreamingFetcher Cache blobserver.Storage // optional MaxWidth, MaxHeight int + Rotate int // degrees to rotate counter-clockwise: 0, ±90, ±180 Square bool sc ScaledImage // optional cache for scaled images } @@ -143,6 +144,43 @@ func (ih *ImageHandler) scaledCached(buf *bytes.Buffer, file *blobref.BlobRef) ( return pieces[1], nil } +// rotate returns the given image rotated by ih.Rotate degrees if it is +// ±90 or ±180, or the source image otherwise. +func (ih *ImageHandler) rotate(im image.Image) image.Image { + if ih.Rotate == 0 { + return im + } + var rotated *image.NRGBA + // trigonometric (i.e counter clock-wise) + switch ih.Rotate { + case 90: + newH, newW := im.Bounds().Dx(), im.Bounds().Dy() + rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH)) + for y := 0; y < newH; y++ { + for x := 0; x < newW; x++ { + rotated.Set(x, y, im.At(newH-1-y, x)) + } + } + case -90: + newH, newW := im.Bounds().Dx(), im.Bounds().Dy() + rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH)) + for y := 0; y < newH; y++ { + for x := 0; x < newW; x++ { + rotated.Set(x, y, im.At(y, newW-1-x)) + } + } + case 180, -180: + newW, newH := im.Bounds().Dx(), im.Bounds().Dy() + rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH)) + for y := 0; y < newH; y++ { + for x := 0; x < newW; x++ { + rotated.Set(x, y, im.At(newW-1-x, newH-1-y)) + } + } + } + return rotated +} + func (ih *ImageHandler) scaleImage(buf *bytes.Buffer, file *blobref.BlobRef) (format string, err error) { mw, mh := ih.MaxWidth, ih.MaxHeight @@ -202,7 +240,7 @@ func (ih *ImageHandler) scaleImage(buf *bytes.Buffer, file *blobref.BlobRef) (fo } if !useBytesUnchanged { - i = resize.Resize(i, b, mw, mh) + i = ih.rotate(resize.Resize(i, b, mw, mh)) // Encode as a new image buf.Reset() switch format { diff --git a/pkg/server/ui.go b/pkg/server/ui.go index 52518d8b1..d5fa5cf27 100644 --- a/pkg/server/ui.go +++ b/pkg/server/ui.go @@ -309,15 +309,30 @@ func (ui *UIHandler) serveThumbnail(rw http.ResponseWriter, req *http.Request) { query := req.URL.Query() width, err := strconv.Atoi(query.Get("mw")) if err != nil { - http.Error(rw, "Invalid specified max width 'mw': "+err.Error(), 500) + http.Error(rw, "Invalid specified max width 'mw'", 500) return } height, err := strconv.Atoi(query.Get("mh")) if err != nil { - http.Error(rw, "Invalid specified height 'mh': "+err.Error(), 500) + http.Error(rw, "Invalid specified height 'mh'", 500) return } + // TODO(mpl): delete this; just temporary assistance before EXIF is done + rot := query.Get("rot") + rotateAngle := 0 + if rot != "" { + rotateAngle, err = strconv.Atoi(rot) + if err != nil { + http.Error(rw, "Invalid 'rot' param", 500) + return + } + if rotateAngle%90 != 0 { + http.Error(rw, "Invalid rotate angle", 500) + return + } + } + blobref := blobref.Parse(m[1]) if blobref == nil { http.Error(rw, "Invalid blobref", 400) @@ -329,6 +344,7 @@ func (ui *UIHandler) serveThumbnail(rw http.ResponseWriter, req *http.Request) { Cache: ui.Cache, MaxWidth: width, MaxHeight: height, + Rotate: rotateAngle, sc: ui.sc, } th.ServeHTTP(rw, req, blobref)