mirror of https://github.com/perkeep/perkeep.git
images: move rescaling from server/image to pkg/images
Fixes http://code.google.com/p/camlistore/issues/detail?id=94 Change-Id: Ifa73e0a3ccbbcaef31ae8870d39f63b8a90aad26
This commit is contained in:
parent
1a79ebeb30
commit
fbcb4411df
|
@ -21,7 +21,6 @@ import (
|
|||
"fmt"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
@ -29,6 +28,7 @@ import (
|
|||
_ "image/gif"
|
||||
_ "image/png"
|
||||
|
||||
"camlistore.org/pkg/misc/resize"
|
||||
"camlistore.org/third_party/github.com/camlistore/goexif/exif"
|
||||
)
|
||||
|
||||
|
@ -57,9 +57,18 @@ type DecodeOpts struct {
|
|||
Flip interface{}
|
||||
|
||||
// MaxWidgth and MaxHeight optionally specify bounds on the
|
||||
// final image's size.
|
||||
// image's size. Rescaling is done before flipping or rotating.
|
||||
// Proportions are conserved, so the smallest of the two is used
|
||||
// as the decisive one if needed.
|
||||
MaxWidth, MaxHeight int
|
||||
|
||||
// ScaleWidth and ScaleHeight optionally specify how to rescale the
|
||||
// image's dimensions. Rescaling is done before flipping or rotating.
|
||||
// Proportions are conserved, so the smallest of the two is used
|
||||
// as the decisive one if needed.
|
||||
// They overrule MaxWidth and MaxHeight.
|
||||
ScaleWidth, ScaleHeight float32
|
||||
|
||||
// TODO: consider alternate options if scaled ratio doesn't
|
||||
// match original ratio:
|
||||
// Crop bool
|
||||
|
@ -164,6 +173,53 @@ func flip(im image.Image, dir FlipDirection) image.Image {
|
|||
return im
|
||||
}
|
||||
|
||||
func rescale(im image.Image, opts *DecodeOpts) image.Image {
|
||||
mw, mh := opts.MaxWidth, opts.MaxHeight
|
||||
mwf, mhf := opts.ScaleWidth, opts.ScaleHeight
|
||||
b := im.Bounds()
|
||||
// only do downscaling, otherwise just serve the original image
|
||||
if !opts.wantRescale(b) {
|
||||
return im
|
||||
}
|
||||
// ScaleWidth and ScaleHeight overrule MaxWidth and MaxHeight
|
||||
if mwf > 0.0 && mwf <= 1 {
|
||||
mw = int(mwf * float32(b.Dx()))
|
||||
}
|
||||
if mhf > 0.0 && mhf <= 1 {
|
||||
mh = int(mhf * float32(b.Dy()))
|
||||
}
|
||||
|
||||
const huge = 2400
|
||||
// If it's gigantic, it's more efficient to downsample first
|
||||
// and then resize; resizing will smooth out the roughness.
|
||||
// (trusting the moustachio guys on that one).
|
||||
if b.Dx() > huge || b.Dy() > huge {
|
||||
w, h := mw*2, mh*2
|
||||
if b.Dx() > b.Dy() {
|
||||
w = b.Dx() * h / b.Dy()
|
||||
} else {
|
||||
h = b.Dy() * w / b.Dx()
|
||||
}
|
||||
im = resize.Resample(im, b, w, h)
|
||||
b = im.Bounds()
|
||||
}
|
||||
// conserve proportions. use the smallest of the two as the decisive one.
|
||||
if mw > mh {
|
||||
mw = b.Dx() * mh / b.Dy()
|
||||
} else {
|
||||
mh = b.Dy() * mw / b.Dx()
|
||||
}
|
||||
return resize.Resize(im, b, mw, mh)
|
||||
}
|
||||
|
||||
func (opts *DecodeOpts) wantRescale(b image.Rectangle) bool {
|
||||
return opts != nil &&
|
||||
(opts.MaxWidth > 0 && opts.MaxWidth < b.Dx() ||
|
||||
opts.MaxHeight > 0 && opts.MaxHeight < b.Dy() ||
|
||||
opts.ScaleWidth > 0.0 && opts.ScaleWidth < float32(b.Dx()) ||
|
||||
opts.ScaleHeight > 0.0 && opts.ScaleHeight < float32(b.Dy()))
|
||||
}
|
||||
|
||||
func (opts *DecodeOpts) forcedRotate() bool {
|
||||
return opts != nil && opts.Rotate != nil
|
||||
}
|
||||
|
@ -195,20 +251,24 @@ func Decode(r io.Reader, opts *DecodeOpts) (image.Image, Config, error) {
|
|||
flipMode := FlipDirection(0)
|
||||
if opts.useEXIF() {
|
||||
ex, err := exif.Decode(tr)
|
||||
if err != nil {
|
||||
imageDebug("No valid EXIF; will not rotate or flip.")
|
||||
maybeRescale := func() (image.Image, Config, error) {
|
||||
im, format, err := image.Decode(io.MultiReader(&buf, r))
|
||||
if err == nil && opts.wantRescale(im.Bounds()) {
|
||||
im = rescale(im, opts)
|
||||
c.Modified = true
|
||||
}
|
||||
c.Format = format
|
||||
c.setBounds(im)
|
||||
return im, c, err
|
||||
}
|
||||
if err != nil {
|
||||
imageDebug("No valid EXIF; will not rotate or flip.")
|
||||
return maybeRescale()
|
||||
}
|
||||
tag, err := ex.Get(exif.Orientation)
|
||||
if err != nil {
|
||||
imageDebug("No \"Orientation\" tag in EXIF; will not rotate or flip.")
|
||||
im, format, err := image.Decode(io.MultiReader(&buf, r))
|
||||
c.Format = format
|
||||
c.setBounds(im)
|
||||
return im, c, err
|
||||
return maybeRescale()
|
||||
}
|
||||
orient := tag.Int(0)
|
||||
switch orient {
|
||||
|
@ -249,16 +309,22 @@ func Decode(r io.Reader, opts *DecodeOpts) (image.Image, Config, error) {
|
|||
}
|
||||
}
|
||||
|
||||
im, err := jpeg.Decode(io.MultiReader(&buf, r))
|
||||
im, format, err := image.Decode(io.MultiReader(&buf, r))
|
||||
if err != nil {
|
||||
return nil, c, err
|
||||
}
|
||||
rescaled := false
|
||||
if opts.wantRescale(im.Bounds()) {
|
||||
im = rescale(im, opts)
|
||||
rescaled = true
|
||||
}
|
||||
im = flip(rotate(im, angle), flipMode)
|
||||
modified := true
|
||||
if angle == 0 && flipMode == 0 {
|
||||
if angle == 0 && flipMode == 0 && !rescaled {
|
||||
modified = false
|
||||
}
|
||||
c.Format = "jpeg"
|
||||
|
||||
c.Format = format
|
||||
c.Modified = modified
|
||||
c.setBounds(im)
|
||||
return im, c, nil
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"image"
|
||||
"image/jpeg"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -35,7 +35,7 @@ func equals(im1, im2 image.Image) bool {
|
|||
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) {
|
||||
if r1 != r2 || g1 != g2 || b1 != b2 || a1 != a2 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,20 @@ func equals(im1, im2 image.Image) bool {
|
|||
}
|
||||
|
||||
func straightFImage(t *testing.T) image.Image {
|
||||
g, err := os.Open(path.Join(datadir, "f1.jpg"))
|
||||
g, err := os.Open(filepath.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 smallStraightFImage(t *testing.T) image.Image {
|
||||
g, err := os.Open(filepath.Join(datadir, "f1-s.jpg"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -69,14 +82,16 @@ func sampleNames(t *testing.T) []string {
|
|||
return samples
|
||||
}
|
||||
|
||||
func TestExifCorrection(t *testing.T) {
|
||||
// TestEXIFCorrection tests that the input files with EXIF metadata
|
||||
// are correctly automatically rotated/flipped when decoded.
|
||||
func TestEXIFCorrection(t *testing.T) {
|
||||
samples := sampleNames(t)
|
||||
straightF := straightFImage(t)
|
||||
for _, v := range samples {
|
||||
if !strings.Contains(v, "exif") {
|
||||
if !strings.Contains(v, "exif") || strings.HasSuffix(v, "-s.jpg") {
|
||||
continue
|
||||
}
|
||||
name := path.Join(datadir, v)
|
||||
name := filepath.Join(datadir, v)
|
||||
t.Logf("correcting %s with EXIF Orientation", name)
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
|
@ -93,11 +108,17 @@ func TestExifCorrection(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestForcedCorrection tests that manually specifying the
|
||||
// rotation/flipping to be applied when decoding works as
|
||||
// expected.
|
||||
func TestForcedCorrection(t *testing.T) {
|
||||
samples := sampleNames(t)
|
||||
straightF := straightFImage(t)
|
||||
for _, v := range samples {
|
||||
name := path.Join(datadir, v)
|
||||
if strings.HasSuffix(v, "-s.jpg") {
|
||||
continue
|
||||
}
|
||||
name := filepath.Join(datadir, v)
|
||||
t.Logf("forced correction of %s", name)
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
|
@ -137,10 +158,61 @@ func TestForcedCorrection(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestRescale verifies that rescaling an image, without
|
||||
// any rotation/flipping, produces the expected image.
|
||||
func TestRescale(t *testing.T) {
|
||||
name := filepath.Join(datadir, "f1.jpg")
|
||||
t.Logf("rescaling %s with half-width and half-height", name)
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
rescaledIm, _, err := Decode(f, &DecodeOpts{ScaleWidth: 0.5, ScaleHeight: 0.5})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
smallIm := smallStraightFImage(t)
|
||||
|
||||
if !equals(rescaledIm, smallIm) {
|
||||
t.Fatalf("%v not properly rescaled", name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRescaleEXIF verifies that rescaling an image, followed
|
||||
// by the automatic EXIF correction (rotation/flipping),
|
||||
// produces the expected image. All the possible correction
|
||||
// modes are tested.
|
||||
func TestRescaleEXIF(t *testing.T) {
|
||||
smallStraightF := smallStraightFImage(t)
|
||||
samples := sampleNames(t)
|
||||
for _, v := range samples {
|
||||
if !strings.Contains(v, "exif") {
|
||||
continue
|
||||
}
|
||||
name := filepath.Join(datadir, v)
|
||||
t.Logf("rescaling %s with half-width and half-height", name)
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
rescaledIm, _, err := Decode(f, &DecodeOpts{ScaleWidth: 0.5, ScaleHeight: 0.5})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !equals(rescaledIm, smallStraightF) {
|
||||
t.Fatalf("%v not properly rescaled", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mpl): move this test to the goexif lib if/when we contribute
|
||||
// back the DateTime stuff to upstream.
|
||||
func TestDateTime(t *testing.T) {
|
||||
f, err := os.Open(path.Join(datadir, "f1-exif.jpg"))
|
||||
f, err := os.Open(filepath.Join(datadir, "f1-exif.jpg"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 960 B |
|
@ -33,7 +33,6 @@ import (
|
|||
"camlistore.org/pkg/blobserver"
|
||||
"camlistore.org/pkg/images"
|
||||
"camlistore.org/pkg/magic"
|
||||
"camlistore.org/pkg/misc/resize"
|
||||
"camlistore.org/pkg/schema"
|
||||
)
|
||||
|
||||
|
@ -149,8 +148,6 @@ func (ih *ImageHandler) scaledCached(buf *bytes.Buffer, file *blobref.BlobRef) (
|
|||
}
|
||||
|
||||
func (ih *ImageHandler) scaleImage(buf *bytes.Buffer, file *blobref.BlobRef) (format string, err error) {
|
||||
mw, mh := ih.MaxWidth, ih.MaxHeight
|
||||
|
||||
fr, err := schema.NewFileReader(ih.storageSeekFetcher(), file)
|
||||
if err != nil {
|
||||
return format, err
|
||||
|
@ -161,7 +158,8 @@ func (ih *ImageHandler) scaleImage(buf *bytes.Buffer, file *blobref.BlobRef) (fo
|
|||
if err != nil {
|
||||
return format, fmt.Errorf("image resize: error reading image %s: %v", file, err)
|
||||
}
|
||||
i, imConfig, err := images.Decode(bytes.NewReader(buf.Bytes()), nil)
|
||||
i, imConfig, err := images.Decode(bytes.NewReader(buf.Bytes()),
|
||||
&images.DecodeOpts{MaxWidth: ih.MaxWidth, MaxHeight: ih.MaxHeight})
|
||||
if err != nil {
|
||||
return format, err
|
||||
}
|
||||
|
@ -176,34 +174,7 @@ func (ih *ImageHandler) scaleImage(buf *bytes.Buffer, file *blobref.BlobRef) (fo
|
|||
b = i.Bounds()
|
||||
}
|
||||
|
||||
// only do downscaling, otherwise just serve the original image
|
||||
if mw < b.Dx() || mh < b.Dy() {
|
||||
useBytesUnchanged = false
|
||||
|
||||
const huge = 2400
|
||||
// If it's gigantic, it's more efficient to downsample first
|
||||
// and then resize; resizing will smooth out the roughness.
|
||||
// (trusting the moustachio guys on that one).
|
||||
if b.Dx() > huge || b.Dy() > huge {
|
||||
w, h := mw*2, mh*2
|
||||
if b.Dx() > b.Dy() {
|
||||
w = b.Dx() * h / b.Dy()
|
||||
} else {
|
||||
h = b.Dy() * w / b.Dx()
|
||||
}
|
||||
i = resize.Resample(i, i.Bounds(), w, h)
|
||||
b = i.Bounds()
|
||||
}
|
||||
// conserve proportions. use the smallest of the two as the decisive one.
|
||||
if mw > mh {
|
||||
mw = b.Dx() * mh / b.Dy()
|
||||
} else {
|
||||
mh = b.Dy() * mw / b.Dx()
|
||||
}
|
||||
}
|
||||
|
||||
if !useBytesUnchanged {
|
||||
i = resize.Resize(i, b, mw, mh)
|
||||
// Encode as a new image
|
||||
buf.Reset()
|
||||
switch format {
|
||||
|
|
Loading…
Reference in New Issue