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 }