diff --git a/pkg/images/images.go b/pkg/images/images.go index f9e0b970c..2de7acf10 100644 --- a/pkg/images/images.go +++ b/pkg/images/images.go @@ -17,12 +17,29 @@ limitations under the License. package images import ( + "bytes" + "fmt" "image" + "image/draw" + "image/jpeg" "io" + "log" _ "image/gif" - _ "image/jpeg" _ "image/png" + + "github.com/camlistore/goexif/exif" +) + +// The FlipDirection type is used by the Flip option in DecodeOpts +// to indicate in which direction to flip an image. +type FlipDirection int + +// FlipVertical and FlipHorizontal are two possible FlipDirections +// values to indicate in which direction an image will be flipped. +const ( + FlipVertical FlipDirection = 1 << iota + FlipHorizontal ) type DecodeOpts struct { @@ -33,6 +50,11 @@ type DecodeOpts struct { // -180. Rotate interface{} + // Flip specifies how to flip the image. + // If nil, the image is flipped automatically based on EXIF metadata. + // Otherwise, Flip is a FlipDirection bitfield indicating how to flip. + Flip interface{} + // MaxWidgth and MaxHeight optionally specify bounds on the // final image's size. MaxWidth, MaxHeight int @@ -43,8 +65,160 @@ type DecodeOpts struct { // Stretch bool } +func rotate(im image.Image, angle int) image.Image { + var rotated *image.NRGBA + // trigonometric (i.e counter clock-wise) + switch angle { + 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)) + } + } + default: + return im + } + return rotated +} + +// flip returns a flipped version of the image im, according to +// the direction(s) in dir. +// It may flip the imput im in place and return it, or it may allocate a +// new NRGBA (if im is an *image.YCbCr). +func flip(im image.Image, dir FlipDirection) image.Image { + if dir == 0 { + return im + } + ycbcr := false + var nrgba image.Image + dx, dy := im.Bounds().Dx(), im.Bounds().Dy() + di, ok := im.(draw.Image) + if !ok { + if _, ok := im.(*image.YCbCr); !ok { + log.Printf("failed to flip image: input does not satisfy draw.Image") + return im + } + // because YCbCr does not implement Set, we replace it with a new NRGBA + ycbcr = true + nrgba = image.NewNRGBA(image.Rect(0, 0, dx, dy)) + di, ok = nrgba.(draw.Image) + if !ok { + log.Print("failed to flip image: could not cast an NRGBA to a draw.Image") + return im + } + } + if dir&FlipHorizontal != 0 { + for y := 0; y < dy; y++ { + for x := 0; x < dx/2; x++ { + old := im.At(x, y) + di.Set(x, y, im.At(dx-1-x, y)) + di.Set(dx-1-x, y, old) + } + } + } + if dir&FlipVertical != 0 { + for y := 0; y < dy/2; y++ { + for x := 0; x < dx; x++ { + old := im.At(x, y) + di.Set(x, y, im.At(x, dy-1-y)) + di.Set(x, dy-1-y, old) + } + } + } + if ycbcr { + return nrgba + } + return im +} + +func (opts *DecodeOpts) forcedRotate() bool { + return opts != nil && opts.Rotate != nil +} + +func (opts *DecodeOpts) forcedFlip() bool { + return opts != nil && opts.Flip != nil +} + +func (opts *DecodeOpts) useEXIF() bool { + return !(opts.forcedRotate() || opts.forcedFlip()) +} + // Decode decodes an image from r using the provided decoding options. // If opts is nil, the defaults are used. func Decode(r io.Reader, opts *DecodeOpts) (image.Image, error) { - panic("TODO(mpl): implement") + var buf bytes.Buffer + tr := io.TeeReader(io.LimitReader(r, 2<<20), &buf) + angle := 0 + flipMode := FlipDirection(0) + if opts.useEXIF() { + ex, err := exif.Decode(tr) + if err != nil { + return nil, err + } + tag, err := ex.Get(exif.Orientation) + if err != nil { + return nil, err + } + orient := tag.Val[1] + switch orient { + case 1: + // do nothing + case 2: + flipMode = 2 + case 3: + angle = 180 + case 4: + angle = 180 + flipMode = 2 + case 5: + angle = -90 + flipMode = 2 + case 6: + angle = -90 + case 7: + angle = 90 + flipMode = 2 + case 8: + angle = 90 + } + } else { + if opts.forcedRotate() { + var ok bool + angle, ok = opts.Rotate.(int) + if !ok { + return nil, fmt.Errorf("Rotate should be an int, not a %T", opts.Rotate) + } + } + if opts.forcedFlip() { + var ok bool + flipMode, ok = opts.Flip.(FlipDirection) + if !ok { + return nil, fmt.Errorf("Flip should be a FlipDirection, not a %T", opts.Flip) + } + } + } + + im, err := jpeg.Decode(io.MultiReader(&buf, r)) + if err != nil { + return nil, err + } + return flip(rotate(im, angle), flipMode), nil } diff --git a/pkg/images/images_test.go b/pkg/images/images_test.go new file mode 100644 index 000000000..e834ed451 --- /dev/null +++ b/pkg/images/images_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2012 The Camlistore Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package images + +import ( + "image" + "image/jpeg" + "os" + "path" + "strings" + "testing" +) + +const datadir = "testdata" + +func equals(im1, im2 image.Image) bool { + for y := 0; y < im1.Bounds().Dy(); y++ { + for x := 0; x < im1.Bounds().Dx(); x++ { + r1, g1, b1, a1 := im1.At(x, y).RGBA() + r2, g2, b2, a2 := im2.At(x, y).RGBA() + if !(r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2) { + return false + } + } + } + return true +} + +func straightFImage(t *testing.T) image.Image { + g, err := os.Open(path.Join(datadir, "f1.jpg")) + if err != nil { + t.Fatal(err) + } + defer g.Close() + straightF, err := jpeg.Decode(g) + if err != nil { + t.Fatal(err) + } + return straightF +} + +func sampleNames(t *testing.T) []string { + dir, err := os.Open(datadir) + if err != nil { + t.Fatal(err) + } + defer dir.Close() + samples, err := dir.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + return samples +} + +func TestExifCorrection(t *testing.T) { + samples := sampleNames(t) + straightF := straightFImage(t) + for _, v := range samples { + if !strings.Contains(v, "exif") { + continue + } + name := path.Join(datadir, v) + t.Logf("correcting %s with EXIF Orientation", name) + f, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer f.Close() + im, err := Decode(f, nil) + if err != nil { + t.Fatal(err) + } + if !equals(im, straightF) { + t.Fatalf("%v not properly corrected with exif", name) + } + } +} + +func TestForcedCorrection(t *testing.T) { + samples := sampleNames(t) + straightF := straightFImage(t) + for _, v := range samples { + name := path.Join(datadir, v) + t.Logf("forced correction of %s", name) + f, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer f.Close() + num := name[10] + angle, flipMode := 0, 0 + switch num { + case '1': + // nothing to do + case '2': + flipMode = 2 + case '3': + angle = 180 + case '4': + angle = 180 + flipMode = 2 + case '5': + angle = -90 + flipMode = 2 + case '6': + angle = -90 + case '7': + angle = 90 + flipMode = 2 + case '8': + angle = 90 + } + im, err := Decode(f, &DecodeOpts{Rotate: angle, Flip: FlipDirection(flipMode)}) + if err != nil { + t.Fatal(err) + } + if !equals(im, straightF) { + t.Fatalf("%v not properly corrected", name) + } + } +}