github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/pkg/images/images.go (about)

     1  /*
     2  Copyright 2012 Google Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8       http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package images
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"image"
    23  	"image/draw"
    24  	"io"
    25  	"log"
    26  	"os"
    27  	"strconv"
    28  	"time"
    29  
    30  	_ "image/gif"
    31  	_ "image/png"
    32  
    33  	"camlistore.org/pkg/images/resize"
    34  	"camlistore.org/third_party/github.com/camlistore/goexif/exif"
    35  )
    36  
    37  var disableThumbCache, _ = strconv.ParseBool(os.Getenv("CAMLI_DISABLE_THUMB_CACHE"))
    38  
    39  // thumbnailVersion should be incremented whenever we want to
    40  // invalidate the cache of previous thumbnails on the server's
    41  // cache and in browsers.
    42  const thumbnailVersion = "2"
    43  
    44  // ThumbnailVersion returns a string safe for URL query components
    45  // which is a generation number. Whenever the thumbnailing code is
    46  // updated, so will this string. It should be placed in some URL
    47  // component (typically "tv").
    48  func ThumbnailVersion() string {
    49  	if disableThumbCache {
    50  		return fmt.Sprintf("nocache%d", time.Now().UnixNano())
    51  	}
    52  	return thumbnailVersion
    53  }
    54  
    55  // Exif Orientation Tag values
    56  // http://sylvana.net/jpegcrop/exif_orientation.html
    57  const (
    58  	topLeftSide     = 1
    59  	topRightSide    = 2
    60  	bottomRightSide = 3
    61  	bottomLeftSide  = 4
    62  	leftSideTop     = 5
    63  	rightSideTop    = 6
    64  	rightSideBottom = 7
    65  	leftSideBottom  = 8
    66  )
    67  
    68  // The FlipDirection type is used by the Flip option in DecodeOpts
    69  // to indicate in which direction to flip an image.
    70  type FlipDirection int
    71  
    72  // FlipVertical and FlipHorizontal are two possible FlipDirections
    73  // values to indicate in which direction an image will be flipped.
    74  const (
    75  	FlipVertical FlipDirection = 1 << iota
    76  	FlipHorizontal
    77  )
    78  
    79  type DecodeOpts struct {
    80  	// Rotate specifies how to rotate the image.
    81  	// If nil, the image is rotated automatically based on EXIF metadata.
    82  	// If an int, Rotate is the number of degrees to rotate
    83  	// counter clockwise and must be one of 0, 90, -90, 180, or
    84  	// -180.
    85  	Rotate interface{}
    86  
    87  	// Flip specifies how to flip the image.
    88  	// If nil, the image is flipped automatically based on EXIF metadata.
    89  	// Otherwise, Flip is a FlipDirection bitfield indicating how to flip.
    90  	Flip interface{}
    91  
    92  	// MaxWidgth and MaxHeight optionally specify bounds on the
    93  	// image's size. Rescaling is done before flipping or rotating.
    94  	// Proportions are conserved, so the smallest of the two is used
    95  	// as the decisive one if needed.
    96  	MaxWidth, MaxHeight int
    97  
    98  	// ScaleWidth and ScaleHeight optionally specify how to rescale the
    99  	// image's dimensions. Rescaling is done before flipping or rotating.
   100  	// Proportions are conserved, so the smallest of the two is used
   101  	// as the decisive one if needed.
   102  	// They overrule MaxWidth and MaxHeight.
   103  	ScaleWidth, ScaleHeight float32
   104  
   105  	// TODO: consider alternate options if scaled ratio doesn't
   106  	// match original ratio:
   107  	//   Crop    bool
   108  	//   Stretch bool
   109  }
   110  
   111  // Config is like the standard library's image.Config as used by DecodeConfig.
   112  type Config struct {
   113  	Width, Height int
   114  	Format        string
   115  	Modified      bool // true if Decode actually rotated or flipped the image.
   116  }
   117  
   118  func (c *Config) setBounds(im image.Image) {
   119  	if im != nil {
   120  		c.Width = im.Bounds().Dx()
   121  		c.Height = im.Bounds().Dy()
   122  	}
   123  }
   124  
   125  func rotate(im image.Image, angle int) image.Image {
   126  	var rotated *image.NRGBA
   127  	// trigonometric (i.e counter clock-wise)
   128  	switch angle {
   129  	case 90:
   130  		newH, newW := im.Bounds().Dx(), im.Bounds().Dy()
   131  		rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH))
   132  		for y := 0; y < newH; y++ {
   133  			for x := 0; x < newW; x++ {
   134  				rotated.Set(x, y, im.At(newH-1-y, x))
   135  			}
   136  		}
   137  	case -90:
   138  		newH, newW := im.Bounds().Dx(), im.Bounds().Dy()
   139  		rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH))
   140  		for y := 0; y < newH; y++ {
   141  			for x := 0; x < newW; x++ {
   142  				rotated.Set(x, y, im.At(y, newW-1-x))
   143  			}
   144  		}
   145  	case 180, -180:
   146  		newW, newH := im.Bounds().Dx(), im.Bounds().Dy()
   147  		rotated = image.NewNRGBA(image.Rect(0, 0, newW, newH))
   148  		for y := 0; y < newH; y++ {
   149  			for x := 0; x < newW; x++ {
   150  				rotated.Set(x, y, im.At(newW-1-x, newH-1-y))
   151  			}
   152  		}
   153  	default:
   154  		return im
   155  	}
   156  	return rotated
   157  }
   158  
   159  // flip returns a flipped version of the image im, according to
   160  // the direction(s) in dir.
   161  // It may flip the imput im in place and return it, or it may allocate a
   162  // new NRGBA (if im is an *image.YCbCr).
   163  func flip(im image.Image, dir FlipDirection) image.Image {
   164  	if dir == 0 {
   165  		return im
   166  	}
   167  	ycbcr := false
   168  	var nrgba image.Image
   169  	dx, dy := im.Bounds().Dx(), im.Bounds().Dy()
   170  	di, ok := im.(draw.Image)
   171  	if !ok {
   172  		if _, ok := im.(*image.YCbCr); !ok {
   173  			log.Printf("failed to flip image: input does not satisfy draw.Image")
   174  			return im
   175  		}
   176  		// because YCbCr does not implement Set, we replace it with a new NRGBA
   177  		ycbcr = true
   178  		nrgba = image.NewNRGBA(image.Rect(0, 0, dx, dy))
   179  		di, ok = nrgba.(draw.Image)
   180  		if !ok {
   181  			log.Print("failed to flip image: could not cast an NRGBA to a draw.Image")
   182  			return im
   183  		}
   184  	}
   185  	if dir&FlipHorizontal != 0 {
   186  		for y := 0; y < dy; y++ {
   187  			for x := 0; x < dx/2; x++ {
   188  				old := im.At(x, y)
   189  				di.Set(x, y, im.At(dx-1-x, y))
   190  				di.Set(dx-1-x, y, old)
   191  			}
   192  		}
   193  	}
   194  	if dir&FlipVertical != 0 {
   195  		for y := 0; y < dy/2; y++ {
   196  			for x := 0; x < dx; x++ {
   197  				old := im.At(x, y)
   198  				di.Set(x, y, im.At(x, dy-1-y))
   199  				di.Set(x, dy-1-y, old)
   200  			}
   201  		}
   202  	}
   203  	if ycbcr {
   204  		return nrgba
   205  	}
   206  	return im
   207  }
   208  
   209  // ScaledDimensions returns the newWidth and newHeight obtained
   210  // when an image of dimensions w x h has to be rescaled under
   211  // mw x mh, while conserving the proportions.
   212  // It returns 1,1 if any of the parameter is 0.
   213  func ScaledDimensions(w, h, mw, mh int) (newWidth int, newHeight int) {
   214  	if w == 0 || h == 0 || mw == 0 || mh == 0 {
   215  		imageDebug("ScaledDimensions was given as 0; returning 1x1 as dimensions.")
   216  		return 1, 1
   217  	}
   218  	newWidth, newHeight = mw, mh
   219  	if float32(h)/float32(mh) > float32(w)/float32(mw) {
   220  		newWidth = w * mh / h
   221  	} else {
   222  		newHeight = h * mw / w
   223  	}
   224  	return
   225  }
   226  
   227  func rescale(im image.Image, opts *DecodeOpts, swapDimensions bool) image.Image {
   228  	mw, mh := opts.MaxWidth, opts.MaxHeight
   229  	mwf, mhf := opts.ScaleWidth, opts.ScaleHeight
   230  	b := im.Bounds()
   231  	// only do downscaling, otherwise just serve the original image
   232  	if !opts.wantRescale(b, swapDimensions) {
   233  		return im
   234  	}
   235  
   236  	if swapDimensions {
   237  		mw, mh = mh, mw
   238  	}
   239  
   240  	// ScaleWidth and ScaleHeight overrule MaxWidth and MaxHeight
   241  	if mwf > 0.0 && mwf <= 1 {
   242  		mw = int(mwf * float32(b.Dx()))
   243  	}
   244  	if mhf > 0.0 && mhf <= 1 {
   245  		mh = int(mhf * float32(b.Dy()))
   246  	}
   247  	// If it's gigantic, it's more efficient to downsample first
   248  	// and then resize; resizing will smooth out the roughness.
   249  	// (trusting the moustachio guys on that one).
   250  	if b.Dx() > mw*2 || b.Dy() > mh*2 {
   251  		w, h := ScaledDimensions(b.Dx(), b.Dy(), mw*2, mh*2)
   252  		im = resize.ResampleInplace(im, b, w, h)
   253  		return resize.HalveInplace(im)
   254  	}
   255  	mw, mh = ScaledDimensions(b.Dx(), b.Dy(), mw, mh)
   256  	return resize.Resize(im, b, mw, mh)
   257  }
   258  
   259  func (opts *DecodeOpts) wantRescale(b image.Rectangle, swapDimensions bool) bool {
   260  	if opts == nil {
   261  		return false
   262  	}
   263  
   264  	// In rescale Scale* trumps Max* so we assume the same relationship here.
   265  
   266  	// Floating point compares probably only allow this to work if the values
   267  	// were specified as the literal 1 or 1.0, computed values will likely be
   268  	// off.  If Scale{Width,Height} end up being 1.0-epsilon we'll rescale
   269  	// when it probably wouldn't even be noticible but that's okay.
   270  	if opts.ScaleWidth == 1.0 && opts.ScaleHeight == 1.0 {
   271  		return false
   272  	}
   273  	if opts.ScaleWidth > 0 && opts.ScaleWidth < 1.0 ||
   274  		opts.ScaleHeight > 0 && opts.ScaleHeight < 1.0 {
   275  		return true
   276  	}
   277  
   278  	w, h := b.Dx(), b.Dy()
   279  	if swapDimensions {
   280  		w, h = h, w
   281  	}
   282  
   283  	// Same size, don't rescale.
   284  	if opts.MaxWidth == w && opts.MaxHeight == h {
   285  		return false
   286  	}
   287  	return opts.MaxWidth > 0 && opts.MaxWidth < w ||
   288  		opts.MaxHeight > 0 && opts.MaxHeight < h
   289  }
   290  
   291  func (opts *DecodeOpts) forcedRotate() bool {
   292  	return opts != nil && opts.Rotate != nil
   293  }
   294  
   295  func (opts *DecodeOpts) forcedFlip() bool {
   296  	return opts != nil && opts.Flip != nil
   297  }
   298  
   299  func (opts *DecodeOpts) useEXIF() bool {
   300  	return !(opts.forcedRotate() || opts.forcedFlip())
   301  }
   302  
   303  var debug, _ = strconv.ParseBool(os.Getenv("CAMLI_DEBUG_IMAGES"))
   304  
   305  func imageDebug(msg string) {
   306  	if debug {
   307  		log.Print(msg)
   308  	}
   309  }
   310  
   311  // DecodeConfig returns the image Config similarly to
   312  // the standard library's image.DecodeConfig with the
   313  // addition that it also checks for an EXIF orientation,
   314  // and sets the Width and Height as they would visibly
   315  // be after correcting for that orientation.
   316  func DecodeConfig(r io.Reader) (Config, error) {
   317  	var c Config
   318  	var buf bytes.Buffer
   319  	tr := io.TeeReader(io.LimitReader(r, 2<<20), &buf)
   320  	swapDimensions := false
   321  
   322  	ex, err := exif.Decode(tr)
   323  	if err != nil {
   324  		imageDebug("No valid EXIF.")
   325  	} else {
   326  		tag, err := ex.Get(exif.Orientation)
   327  		if err != nil {
   328  			imageDebug("No \"Orientation\" tag in EXIF.")
   329  		} else {
   330  			orient := tag.Int(0)
   331  			switch orient {
   332  			// those are the orientations that require
   333  			// a rotation of ±90
   334  			case leftSideTop, rightSideTop, rightSideBottom, leftSideBottom:
   335  				swapDimensions = true
   336  			}
   337  		}
   338  	}
   339  	conf, format, err := image.DecodeConfig(io.MultiReader(&buf, r))
   340  	if err != nil {
   341  		imageDebug(fmt.Sprintf("Image Decoding failed: %v", err))
   342  		return c, err
   343  	}
   344  	c.Format = format
   345  	if swapDimensions {
   346  		c.Width, c.Height = conf.Height, conf.Width
   347  	} else {
   348  		c.Width, c.Height = conf.Width, conf.Height
   349  	}
   350  	return c, err
   351  }
   352  
   353  // Decode decodes an image from r using the provided decoding options.
   354  // The Config returned is similar to the one from the image package,
   355  // with the addition of the Modified field which indicates if the
   356  // image was actually flipped, rotated, or scaled.
   357  // If opts is nil, the defaults are used.
   358  func Decode(r io.Reader, opts *DecodeOpts) (image.Image, Config, error) {
   359  	var c Config
   360  	var buf bytes.Buffer
   361  	tr := io.TeeReader(io.LimitReader(r, 2<<20), &buf)
   362  	angle := 0
   363  	flipMode := FlipDirection(0)
   364  	if opts.useEXIF() {
   365  		ex, err := exif.Decode(tr)
   366  		maybeRescale := func() (image.Image, Config, error) {
   367  			im, format, err := image.Decode(io.MultiReader(&buf, r))
   368  			if err == nil && opts.wantRescale(im.Bounds(), false) {
   369  				im = rescale(im, opts, false)
   370  				c.Modified = true
   371  			}
   372  			c.Format = format
   373  			c.setBounds(im)
   374  			return im, c, err
   375  		}
   376  		if err != nil {
   377  			imageDebug("No valid EXIF; will not rotate or flip.")
   378  			return maybeRescale()
   379  		}
   380  		tag, err := ex.Get(exif.Orientation)
   381  		if err != nil {
   382  			imageDebug("No \"Orientation\" tag in EXIF; will not rotate or flip.")
   383  			return maybeRescale()
   384  		}
   385  		orient := tag.Int(0)
   386  		switch orient {
   387  		case topLeftSide:
   388  			// do nothing
   389  		case topRightSide:
   390  			flipMode = 2
   391  		case bottomRightSide:
   392  			angle = 180
   393  		case bottomLeftSide:
   394  			angle = 180
   395  			flipMode = 2
   396  		case leftSideTop:
   397  			angle = -90
   398  			flipMode = 2
   399  		case rightSideTop:
   400  			angle = -90
   401  		case rightSideBottom:
   402  			angle = 90
   403  			flipMode = 2
   404  		case leftSideBottom:
   405  			angle = 90
   406  		}
   407  	} else {
   408  		if opts.forcedRotate() {
   409  			var ok bool
   410  			angle, ok = opts.Rotate.(int)
   411  			if !ok {
   412  				return nil, c, fmt.Errorf("Rotate should be an int, not a %T", opts.Rotate)
   413  			}
   414  		}
   415  		if opts.forcedFlip() {
   416  			var ok bool
   417  			flipMode, ok = opts.Flip.(FlipDirection)
   418  			if !ok {
   419  				return nil, c, fmt.Errorf("Flip should be a FlipDirection, not a %T", opts.Flip)
   420  			}
   421  		}
   422  	}
   423  
   424  	im, format, err := image.Decode(io.MultiReader(&buf, r))
   425  	if err != nil {
   426  		return nil, c, err
   427  	}
   428  	rescaled := false
   429  	// Orientation changing rotations should have their dimensions swapped
   430  	// when scaling.
   431  	var swapDimensions bool
   432  	switch angle {
   433  	case 90, -90:
   434  		swapDimensions = true
   435  	}
   436  	if opts.wantRescale(im.Bounds(), swapDimensions) {
   437  		im = rescale(im, opts, swapDimensions)
   438  		rescaled = true
   439  	}
   440  	im = flip(rotate(im, angle), flipMode)
   441  	modified := true
   442  	if angle == 0 && flipMode == 0 && !rescaled {
   443  		modified = false
   444  	}
   445  
   446  	c.Format = format
   447  	c.Modified = modified
   448  	c.setBounds(im)
   449  	return im, c, nil
   450  }