1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17 package server
18
19 import (
20 "bytes"
21 "context"
22 "errors"
23 "expvar"
24 "fmt"
25 "image"
26 "image/jpeg"
27 "image/png"
28 "io"
29 "log"
30 "net/http"
31 "strconv"
32 "strings"
33 "time"
34
35 _ "github.com/nf/cr2"
36 "go4.org/readerutil"
37 "go4.org/syncutil"
38 "go4.org/syncutil/singleflight"
39 "go4.org/types"
40 "perkeep.org/internal/httputil"
41 "perkeep.org/internal/images"
42 "perkeep.org/internal/magic"
43 "perkeep.org/pkg/blob"
44 "perkeep.org/pkg/blobserver"
45 "perkeep.org/pkg/constants"
46 "perkeep.org/pkg/schema"
47 "perkeep.org/pkg/search"
48 )
49
50 const imageDebug = false
51
52 var (
53 imageBytesServedVar = expvar.NewInt("image-bytes-served")
54 imageBytesFetchedVar = expvar.NewInt("image-bytes-fetched")
55 thumbCacheMiss = expvar.NewInt("thumbcache-miss")
56 thumbCacheHitFull = expvar.NewInt("thumbcache-hit-full")
57 thumbCacheHitFile = expvar.NewInt("thumbcache-hit-file")
58 thumbCacheHeader304 = expvar.NewInt("thumbcache-header-304")
59 )
60
61 type ImageHandler struct {
62 Fetcher blob.Fetcher
63 Search *search.Handler
64 Cache blobserver.Storage
65 MaxWidth, MaxHeight int
66 Square bool
67 ThumbMeta *ThumbMeta
68 ResizeSem *syncutil.Sem
69 }
70
71 type subImager interface {
72 SubImage(image.Rectangle) image.Image
73 }
74
75 func squareImage(i image.Image) image.Image {
76 si, ok := i.(subImager)
77 if !ok {
78 log.Fatalf("image %T isn't a subImager", i)
79 }
80 b := i.Bounds()
81 if b.Dx() > b.Dy() {
82 thin := (b.Dx() - b.Dy()) / 2
83 newB := b
84 newB.Min.X += thin
85 newB.Max.X -= thin
86 return si.SubImage(newB)
87 }
88 thin := (b.Dy() - b.Dx()) / 2
89 newB := b
90 newB.Min.Y += thin
91 newB.Max.Y -= thin
92 return si.SubImage(newB)
93 }
94
95 func writeToCache(ctx context.Context, cache blobserver.Storage, thumbBytes []byte, name string) (br blob.Ref, err error) {
96 tr := bytes.NewReader(thumbBytes)
97 if len(thumbBytes) < constants.MaxBlobSize {
98 br = blob.RefFromBytes(thumbBytes)
99 _, err = blobserver.Receive(ctx, cache, br, tr)
100 } else {
101
102
103 br, err = schema.WriteFileFromReader(ctx, cache, name, tr)
104 }
105 if err != nil {
106 return br, errors.New("failed to cache " + name + ": " + err.Error())
107 }
108 if imageDebug {
109 log.Printf("Image Cache: saved as %v\n", br)
110 }
111 return br, nil
112 }
113
114
115
116 func (ih *ImageHandler) cacheScaled(ctx context.Context, thumbBytes []byte, name string) error {
117 br, err := writeToCache(ctx, ih.Cache, thumbBytes, name)
118 if err != nil {
119 return err
120 }
121 ih.ThumbMeta.Put(name, br)
122 return nil
123 }
124
125
126
127
128
129
130 func (ih *ImageHandler) cached(ctx context.Context, br blob.Ref) (io.ReadCloser, error) {
131 rsc, _, err := ih.Cache.Fetch(ctx, br)
132 if err != nil {
133 return nil, err
134 }
135 slurp, err := io.ReadAll(rsc)
136 rsc.Close()
137 if err != nil {
138 return nil, err
139 }
140
141
142 if strings.HasPrefix(magic.MIMEType(slurp), "image/") {
143 thumbCacheHitFull.Add(1)
144 if imageDebug {
145 log.Printf("Image Cache: hit: %v\n", br)
146 }
147 return io.NopCloser(bytes.NewReader(slurp)), nil
148 }
149
150
151
152 fileBlob, err := schema.BlobFromReader(br, bytes.NewReader(slurp))
153 if err != nil {
154 log.Printf("Failed to parse non-image thumbnail cache blob %v: %v", br, err)
155 return nil, err
156 }
157 fr, err := fileBlob.NewFileReader(ih.Cache)
158 if err != nil {
159 log.Printf("cached(%v) NewFileReader = %v", br, err)
160 return nil, err
161 }
162 thumbCacheHitFile.Add(1)
163 if imageDebug {
164 log.Printf("Image Cache: fileref hit: %v\n", br)
165 }
166 return fr, nil
167 }
168
169
170
171 func cacheKey(bref string, width int, height int) string {
172 return fmt.Sprintf("scaled:%v:%dx%d:tv%v", bref, width, height, images.ThumbnailVersion())
173 }
174
175
176
177
178
179
180 func (ih *ImageHandler) scaledCached(ctx context.Context, buf *bytes.Buffer, file blob.Ref) (format string) {
181 key := cacheKey(file.String(), ih.MaxWidth, ih.MaxHeight)
182 br, err := ih.ThumbMeta.Get(key)
183 if err == errCacheMiss {
184 return
185 }
186 if err != nil {
187 log.Printf("Warning: thumbnail cachekey(%q)->meta lookup error: %v", key, err)
188 return
189 }
190 fr, err := ih.cached(ctx, br)
191 if err != nil {
192 if imageDebug {
193 log.Printf("Could not get cached image %v: %v\n", br, err)
194 }
195 return
196 }
197 defer fr.Close()
198 _, err = io.Copy(buf, fr)
199 if err != nil {
200 return
201 }
202 mime := magic.MIMEType(buf.Bytes())
203 if format = strings.TrimPrefix(mime, "image/"); format == mime {
204 log.Printf("Warning: unescaped MIME type %q of %v file for thumbnail %q", mime, br, key)
205 return
206 }
207 return format
208 }
209
210
211
212 type formatAndImage struct {
213 format string
214 image []byte
215 }
216
217
218
219
220
221
222 func imageConfigFromReader(r io.Reader) (io.Reader, image.Config, error) {
223 header := new(bytes.Buffer)
224 tr := io.TeeReader(r, header)
225
226
227
228
229
230 conf, format, err := image.DecodeConfig(tr)
231 if err == nil && format == "heic" {
232 err = images.ErrHEIC
233 }
234 return io.MultiReader(header, r), conf, err
235 }
236
237 func (ih *ImageHandler) newFileReader(ctx context.Context, fileRef blob.Ref) (io.ReadCloser, error) {
238 fi, ok := fileInfoPacked(ctx, ih.Search, ih.Fetcher, nil, fileRef)
239 if debugPack {
240 log.Printf("pkg/server/image.go: fileInfoPacked: ok=%v, %+v", ok, fi)
241 }
242 if ok {
243
244
245
246
247 return struct {
248 io.Reader
249 io.Closer
250 }{
251 fi.rs,
252 types.CloseFunc(fi.close),
253 }, nil
254 }
255
256 return schema.NewFileReader(ctx, ih.Fetcher, fileRef)
257 }
258
259 func (ih *ImageHandler) scaleImage(ctx context.Context, fileRef blob.Ref) (*formatAndImage, error) {
260 fr, err := ih.newFileReader(ctx, fileRef)
261 if err != nil {
262 return nil, err
263 }
264 defer fr.Close()
265
266 sr := readerutil.NewStatsReader(imageBytesFetchedVar, fr)
267 sr, conf, err := imageConfigFromReader(sr)
268 if err == images.ErrHEIC {
269 jpegBytes, err := images.HEIFToJPEG(sr, &images.Dimensions{MaxWidth: ih.MaxWidth, MaxHeight: ih.MaxHeight})
270 if err != nil {
271 log.Printf("cannot convert with heiftojpeg: %v", err)
272 return nil, errors.New("error converting HEIC image to jpeg")
273 }
274 return &formatAndImage{format: "jpeg", image: jpegBytes}, nil
275 }
276 if err != nil {
277 return nil, err
278 }
279
280
281
282
283
284
285
286
287
288
289 ramSize := int64(conf.Width) * int64(conf.Height) * 3
290
291 if err = ih.ResizeSem.Acquire(ramSize); err != nil {
292 return nil, err
293 }
294 defer ih.ResizeSem.Release(ramSize)
295
296 i, imConfig, err := images.Decode(sr, &images.DecodeOpts{
297 MaxWidth: ih.MaxWidth,
298 MaxHeight: ih.MaxHeight,
299 })
300 if err != nil {
301 return nil, err
302 }
303 b := i.Bounds()
304 format := imConfig.Format
305
306 isSquare := b.Dx() == b.Dy()
307 if ih.Square && !isSquare {
308 i = squareImage(i)
309 b = i.Bounds()
310 }
311
312
313 var buf bytes.Buffer
314 switch format {
315 case "png":
316 err = png.Encode(&buf, i)
317 case "cr2":
318
319 format = "jpeg"
320 fallthrough
321 default:
322 err = jpeg.Encode(&buf, i, &jpeg.Options{
323 Quality: 90,
324 })
325 }
326 if err != nil {
327 return nil, err
328 }
329
330 return &formatAndImage{format: format, image: buf.Bytes()}, nil
331 }
332
333
334
335
336 var singleResize singleflight.Group
337
338 func (ih *ImageHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request, file blob.Ref) {
339 ctx := req.Context()
340 if !httputil.IsGet(req) {
341 http.Error(rw, "Invalid method", http.StatusBadRequest)
342 return
343 }
344 mw, mh := ih.MaxWidth, ih.MaxHeight
345 if mw == 0 || mh == 0 || mw > search.MaxImageSize || mh > search.MaxImageSize {
346 http.Error(rw, "bogus dimensions", http.StatusBadRequest)
347 return
348 }
349
350 key := cacheKey(file.String(), mw, mh)
351 etag := blob.RefFromString(key).String()[5:]
352 inm := req.Header.Get("If-None-Match")
353 if inm != "" {
354 if strings.Trim(inm, `"`) == etag {
355 thumbCacheHeader304.Add(1)
356 rw.WriteHeader(http.StatusNotModified)
357 return
358 }
359 } else {
360 if !disableThumbCache && req.Header.Get("If-Modified-Since") != "" {
361 thumbCacheHeader304.Add(1)
362 rw.WriteHeader(http.StatusNotModified)
363 return
364 }
365 }
366
367 var imageData []byte
368 format := ""
369 cacheHit := false
370 if ih.ThumbMeta != nil && !disableThumbCache {
371 var buf bytes.Buffer
372 format = ih.scaledCached(ctx, &buf, file)
373 if format != "" {
374 cacheHit = true
375 imageData = buf.Bytes()
376 }
377 }
378
379 if !cacheHit {
380 thumbCacheMiss.Add(1)
381 imi, err := singleResize.Do(key, func() (interface{}, error) {
382 return ih.scaleImage(ctx, file)
383 })
384 if err != nil {
385 http.Error(rw, err.Error(), 500)
386 return
387 }
388 im := imi.(*formatAndImage)
389 imageData = im.image
390 format = im.format
391 if ih.ThumbMeta != nil {
392 err := ih.cacheScaled(ctx, imageData, key)
393 if err != nil {
394 log.Printf("image resize: %v", err)
395 }
396 }
397 }
398
399 h := rw.Header()
400 if !disableThumbCache {
401 h.Set("Expires", time.Now().Add(oneYear).Format(http.TimeFormat))
402 h.Set("Last-Modified", time.Now().Format(http.TimeFormat))
403 h.Set("Etag", strconv.Quote(etag))
404 }
405 h.Set("Content-Type", imageContentTypeOfFormat(format))
406 size := len(imageData)
407 h.Set("Content-Length", fmt.Sprint(size))
408 imageBytesServedVar.Add(int64(size))
409
410 if req.Method == "GET" {
411 n, err := rw.Write(imageData)
412 if err != nil {
413 if strings.Contains(err.Error(), "broken pipe") {
414
415 return
416 }
417
418 log.Printf("error serving thumbnail of file schema %s: %v", file, err)
419 return
420 }
421 if n != size {
422 log.Printf("error serving thumbnail of file schema %s: sent %d, expected size of %d",
423 file, n, size)
424 return
425 }
426 }
427 }
428
429 func imageContentTypeOfFormat(format string) string {
430 if format == "jpeg" {
431 return "image/jpeg"
432 }
433 return "image/png"
434 }