forked from perkeep/perkeep
-
Notifications
You must be signed in to change notification settings - Fork 0
/
image.go
395 lines (357 loc) · 10.7 KB
/
image.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/*
Copyright 2011 Google Inc.
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 server
import (
"bytes"
"errors"
"expvar"
"fmt"
"image"
"image/png"
"io"
"io/ioutil"
"log"
"net/http"
"strconv"
"strings"
"time"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/blobserver"
"camlistore.org/pkg/constants"
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/images"
"camlistore.org/pkg/magic"
"camlistore.org/pkg/schema"
"camlistore.org/pkg/search"
"camlistore.org/pkg/singleflight"
"camlistore.org/pkg/syncutil"
"camlistore.org/pkg/types"
_ "camlistore.org/third_party/github.com/nf/cr2"
"camlistore.org/third_party/go/pkg/image/jpeg"
)
const imageDebug = false
var (
imageBytesServedVar = expvar.NewInt("image-bytes-served")
imageBytesFetchedVar = expvar.NewInt("image-bytes-fetched")
thumbCacheMiss = expvar.NewInt("thumbcache-miss")
thumbCacheHitFull = expvar.NewInt("thumbcache-hit-full")
thumbCacheHitFile = expvar.NewInt("thumbcache-hit-file")
thumbCacheHeader304 = expvar.NewInt("thumbcache-header-304")
)
type ImageHandler struct {
Fetcher blob.Fetcher
Cache blobserver.Storage // optional
MaxWidth, MaxHeight int
Square bool
ThumbMeta *ThumbMeta // optional cache index for scaled images
ResizeSem *syncutil.Sem // Limit peak RAM used by concurrent image thumbnail calls.
}
type subImager interface {
SubImage(image.Rectangle) image.Image
}
func squareImage(i image.Image) image.Image {
si, ok := i.(subImager)
if !ok {
log.Fatalf("image %T isn't a subImager", i)
}
b := i.Bounds()
if b.Dx() > b.Dy() {
thin := (b.Dx() - b.Dy()) / 2
newB := b
newB.Min.X += thin
newB.Max.X -= thin
return si.SubImage(newB)
}
thin := (b.Dy() - b.Dx()) / 2
newB := b
newB.Min.Y += thin
newB.Max.Y -= thin
return si.SubImage(newB)
}
func writeToCache(cache blobserver.Storage, thumbBytes []byte, name string) (br blob.Ref, err error) {
tr := bytes.NewReader(thumbBytes)
if len(thumbBytes) < constants.MaxBlobSize {
br = blob.SHA1FromBytes(thumbBytes)
_, err = blobserver.Receive(cache, br, tr)
} else {
// TODO: don't use rolling checksums when writing this. Tell
// the filewriter to use 16 MB chunks instead.
br, err = schema.WriteFileFromReader(cache, name, tr)
}
if err != nil {
return br, errors.New("failed to cache " + name + ": " + err.Error())
}
if imageDebug {
log.Printf("Image Cache: saved as %v\n", br)
}
return br, nil
}
// cacheScaled saves in the image handler's cache the scaled image bytes
// in thumbBytes, and puts its blobref in the scaledImage under the key name.
func (ih *ImageHandler) cacheScaled(thumbBytes []byte, name string) error {
br, err := writeToCache(ih.Cache, thumbBytes, name)
if err != nil {
return err
}
ih.ThumbMeta.Put(name, br)
return nil
}
// cached returns a FileReader for the given blobref, which may
// point to either a blob representing the entire thumbnail (max
// 16MB) or a file schema blob.
//
// The ReadCloser should be closed when done reading.
func (ih *ImageHandler) cached(br blob.Ref) (io.ReadCloser, error) {
rsc, _, err := ih.Cache.Fetch(br)
if err != nil {
return nil, err
}
slurp, err := ioutil.ReadAll(rsc)
rsc.Close()
if err != nil {
return nil, err
}
// In the common case, when the scaled image itself is less than 16 MB, it's
// all together in one blob.
if strings.HasPrefix(magic.MIMEType(slurp), "image/") {
thumbCacheHitFull.Add(1)
if imageDebug {
log.Printf("Image Cache: hit: %v\n", br)
}
return ioutil.NopCloser(bytes.NewReader(slurp)), nil
}
// For large scaled images, the cached blob is a file schema blob referencing
// the sub-chunks.
fileBlob, err := schema.BlobFromReader(br, bytes.NewReader(slurp))
if err != nil {
log.Printf("Failed to parse non-image thumbnail cache blob %v: %v", br, err)
return nil, err
}
fr, err := fileBlob.NewFileReader(ih.Cache)
if err != nil {
log.Printf("cached(%v) NewFileReader = %v", br, err)
return nil, err
}
thumbCacheHitFile.Add(1)
if imageDebug {
log.Printf("Image Cache: fileref hit: %v\n", br)
}
return fr, nil
}
// Key format: "scaled:" + bref + ":" + width "x" + height
// where bref is the blobref of the unscaled image.
func cacheKey(bref string, width int, height int) string {
return fmt.Sprintf("scaled:%v:%dx%d:tv%v", bref, width, height, images.ThumbnailVersion())
}
// ScaledCached reads the scaled version of the image in file,
// if it is in cache and writes it to buf.
//
// On successful read and population of buf, the returned format is non-empty.
// Almost all errors are not interesting. Real errors will be logged.
func (ih *ImageHandler) scaledCached(buf *bytes.Buffer, file blob.Ref) (format string) {
key := cacheKey(file.String(), ih.MaxWidth, ih.MaxHeight)
br, err := ih.ThumbMeta.Get(key)
if err == errCacheMiss {
return
}
if err != nil {
log.Printf("Warning: thumbnail cachekey(%q)->meta lookup error: %v", key, err)
return
}
fr, err := ih.cached(br)
if err != nil {
if imageDebug {
log.Printf("Could not get cached image %v: %v\n", br, err)
}
return
}
defer fr.Close()
_, err = io.Copy(buf, fr)
if err != nil {
return
}
mime := magic.MIMEType(buf.Bytes())
if format = strings.TrimPrefix(mime, "image/"); format == mime {
log.Printf("Warning: unescaped MIME type %q of %v file for thumbnail %q", mime, br, key)
return
}
return format
}
// Gate the number of concurrent image resizes to limit RAM & CPU use.
type formatAndImage struct {
format string
image []byte
}
// imageConfigFromReader calls image.DecodeConfig on r. It returns an
// io.Reader that is the concatentation of the bytes read and the remaining r,
// the image configuration, and the error from image.DecodeConfig.
func imageConfigFromReader(r io.Reader) (io.Reader, image.Config, error) {
header := new(bytes.Buffer)
tr := io.TeeReader(r, header)
// We just need width & height for memory considerations, so we use the
// standard library's DecodeConfig, skipping the EXIF parsing and
// orientation correction for images.DecodeConfig.
conf, _, err := image.DecodeConfig(tr)
return io.MultiReader(header, r), conf, err
}
func (ih *ImageHandler) scaleImage(fileRef blob.Ref) (*formatAndImage, error) {
fr, err := schema.NewFileReader(ih.Fetcher, fileRef)
if err != nil {
return nil, err
}
defer fr.Close()
sr := types.NewStatsReader(imageBytesFetchedVar, fr)
sr, conf, err := imageConfigFromReader(sr)
if err != nil {
return nil, err
}
// TODO(wathiede): build a size table keyed by conf.ColorModel for
// common color models for a more exact size estimate.
// This value is an estimate of the memory required to decode an image.
// PNGs range from 1-64 bits per pixel (not all of which are supported by
// the Go standard parser). JPEGs encoded in YCbCr 4:4:4 are 3 byte/pixel.
// For all other JPEGs this is an overestimate. For GIFs it is 3x larger
// than needed. How accurate this estimate is depends on the mix of
// images being resized concurrently.
ramSize := int64(conf.Width) * int64(conf.Height) * 3
if err = ih.ResizeSem.Acquire(ramSize); err != nil {
return nil, err
}
defer ih.ResizeSem.Release(ramSize)
i, imConfig, err := images.Decode(sr, &images.DecodeOpts{
MaxWidth: ih.MaxWidth,
MaxHeight: ih.MaxHeight,
})
if err != nil {
return nil, err
}
b := i.Bounds()
format := imConfig.Format
isSquare := b.Dx() == b.Dy()
if ih.Square && !isSquare {
i = squareImage(i)
b = i.Bounds()
}
// Encode as a new image
var buf bytes.Buffer
switch format {
case "png":
err = png.Encode(&buf, i)
case "cr2":
// Recompress CR2 files as JPEG
format = "jpeg"
fallthrough
default:
err = jpeg.Encode(&buf, i, &jpeg.Options{
Quality: 90,
})
}
if err != nil {
return nil, err
}
return &formatAndImage{format: format, image: buf.Bytes()}, nil
}
// singleResize prevents generating the same thumbnail at once from
// two different requests. (e.g. sending out a link to a new photo
// gallery to a big audience)
var singleResize singleflight.Group
func (ih *ImageHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, file blob.Ref) {
if !httputil.IsGet(req) {
http.Error(rw, "Invalid method", 400)
return
}
mw, mh := ih.MaxWidth, ih.MaxHeight
if mw == 0 || mh == 0 || mw > search.MaxImageSize || mh > search.MaxImageSize {
http.Error(rw, "bogus dimensions", 400)
return
}
key := cacheKey(file.String(), mw, mh)
etag := blob.SHA1FromString(key).String()[5:]
inm := req.Header.Get("If-None-Match")
if inm != "" {
if strings.Trim(inm, `"`) == etag {
thumbCacheHeader304.Add(1)
rw.WriteHeader(http.StatusNotModified)
return
}
} else {
if !disableThumbCache && req.Header.Get("If-Modified-Since") != "" {
thumbCacheHeader304.Add(1)
rw.WriteHeader(http.StatusNotModified)
return
}
}
var imageData []byte
format := ""
cacheHit := false
if ih.ThumbMeta != nil && !disableThumbCache {
var buf bytes.Buffer
format = ih.scaledCached(&buf, file)
if format != "" {
cacheHit = true
imageData = buf.Bytes()
}
}
if !cacheHit {
thumbCacheMiss.Add(1)
imi, err := singleResize.Do(key, func() (interface{}, error) {
return ih.scaleImage(file)
})
if err != nil {
http.Error(rw, err.Error(), 500)
return
}
im := imi.(*formatAndImage)
imageData = im.image
format = im.format
if ih.ThumbMeta != nil {
err := ih.cacheScaled(imageData, key)
if err != nil {
log.Printf("image resize: %v", err)
}
}
}
h := rw.Header()
if !disableThumbCache {
h.Set("Expires", time.Now().Add(oneYear).Format(http.TimeFormat))
h.Set("Last-Modified", time.Now().Format(http.TimeFormat))
h.Set("Etag", strconv.Quote(etag))
}
h.Set("Content-Type", imageContentTypeOfFormat(format))
size := len(imageData)
h.Set("Content-Length", fmt.Sprint(size))
imageBytesServedVar.Add(int64(size))
if req.Method == "GET" {
n, err := rw.Write(imageData)
if err != nil {
if strings.Contains(err.Error(), "broken pipe") {
// boring.
return
}
// TODO: vlog this:
log.Printf("error serving thumbnail of file schema %s: %v", file, err)
return
}
if n != size {
log.Printf("error serving thumbnail of file schema %s: sent %d, expected size of %d",
file, n, size)
return
}
}
}
func imageContentTypeOfFormat(format string) string {
if format == "jpeg" {
return "image/jpeg"
}
return "image/png"
}