Home Download Docs Code Community
     1	/*
     2	Copyright 2011 The Perkeep Authors
     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 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    // optional
    64		Cache               blobserver.Storage // optional
    65		MaxWidth, MaxHeight int
    66		Square              bool
    67		ThumbMeta           *ThumbMeta    // optional cache index for scaled images
    68		ResizeSem           *syncutil.Sem // Limit peak RAM used by concurrent image thumbnail calls.
    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			// TODO: don't use rolling checksums when writing this. Tell
   102			// the filewriter to use 16 MB chunks instead.
   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	// cacheScaled saves in the image handler's cache the scaled image bytes
   115	// in thumbBytes, and puts its blobref in the scaledImage under the key name.
   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	// cached returns a FileReader for the given blobref, which may
   126	// point to either a blob representing the entire thumbnail (max
   127	// 16MB) or a file schema blob.
   128	//
   129	// The ReadCloser should be closed when done reading.
   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		// In the common case, when the scaled image itself is less than 16 MB, it's
   141		// all together in one blob.
   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		// For large scaled images, the cached blob is a file schema blob referencing
   151		// the sub-chunks.
   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	// Key format: "scaled:" + bref + ":" + width "x" + height
   170	// where bref is the blobref of the unscaled image.
   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	// ScaledCached reads the scaled version of the image in file,
   176	// if it is in cache and writes it to buf.
   177	//
   178	// On successful read and population of buf, the returned format is non-empty.
   179	// Almost all errors are not interesting. Real errors will be logged.
   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	// Gate the number of concurrent image resizes to limit RAM & CPU use.
   211	
   212	type formatAndImage struct {
   213		format string
   214		image  []byte
   215	}
   216	
   217	// imageConfigFromReader calls image.DecodeConfig on r. It returns an
   218	// io.Reader that is the concatenation of the bytes read and the remaining r,
   219	// the image configuration, and the error from image.DecodeConfig.
   220	// If the image is HEIC, and its config was decoded properly (but partially,
   221	// because we don't do ColorModel yet), it returns images.ErrHEIC.
   222	func imageConfigFromReader(r io.Reader) (io.Reader, image.Config, error) {
   223		header := new(bytes.Buffer)
   224		tr := io.TeeReader(r, header)
   225		// We just need width & height for memory considerations, so we use the
   226		// standard library's DecodeConfig, skipping the EXIF parsing and
   227		// orientation correction for images.DecodeConfig.
   228		// image.DecodeConfig is able to deal with HEIC because we registered it
   229		// in internal/images.
   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			// This would be less gross if fileInfoPacked just
   244			// returned an io.ReadCloser, but then the download
   245			// handler would need more invasive changes for
   246			// ServeContent. So tolerate this for now.
   247			return struct {
   248				io.Reader
   249				io.Closer
   250			}{
   251				fi.rs,
   252				types.CloseFunc(fi.close),
   253			}, nil
   254		}
   255		// Default path, not going through blobpacked's fast path:
   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		// TODO(wathiede): build a size table keyed by conf.ColorModel for
   281		// common color models for a more exact size estimate.
   282	
   283		// This value is an estimate of the memory required to decode an image.
   284		// PNGs range from 1-64 bits per pixel (not all of which are supported by
   285		// the Go standard parser). JPEGs encoded in YCbCr 4:4:4 are 3 byte/pixel.
   286		// For all other JPEGs this is an overestimate.  For GIFs it is 3x larger
   287		// than needed.  How accurate this estimate is depends on the mix of
   288		// images being resized concurrently.
   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		// Encode as a new image
   313		var buf bytes.Buffer
   314		switch format {
   315		case "png":
   316			err = png.Encode(&buf, i)
   317		case "cr2":
   318			// Recompress CR2 files as JPEG
   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	// singleResize prevents generating the same thumbnail at once from
   334	// two different requests.  (e.g. sending out a link to a new photo
   335	// gallery to a big audience)
   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					// boring.
   415					return
   416				}
   417				// TODO: vlog this:
   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	}
Website layout inspired by memcached.
Content by the authors.