package main import ( "image" "image/color" _ "image/gif" "image/jpeg" _ "image/png" "io" "path/filepath" "strings" ) func init() { thumbnailers = append(thumbnailers, &imageThumbnailer{}) } // isImageFile reports whether name is a raster image we can thumbnail and thus // embed on a page. Video files also carry thumbnails but are not embeddable, so // this keys on the image thumbnailer's own extension set rather than // hasThumbnail. func isImageFile(name string) bool { return (&imageThumbnailer{}).CanHandle(strings.ToLower(filepath.Ext(name))) } type imageThumbnailer struct{} func (it *imageThumbnailer) CanHandle(ext string) bool { switch ext { case ".jpg", ".jpeg", ".png", ".gif": return true } return false } func (it *imageThumbnailer) Generate(src io.Reader, dst io.Writer, width int) error { img, _, err := image.Decode(src) if err != nil { return err } return jpeg.Encode(dst, resizeBox(img, width), &jpeg.Options{Quality: 80}) } // resizeBox downsamples src to the requested width using a box filter. // Aspect ratio is preserved. Upscaling is a no-op (returns src unchanged). // Each source pixel is visited exactly once; alpha is discarded. func resizeBox(src image.Image, width int) image.Image { b := src.Bounds() srcW, srcH := b.Dx(), b.Dy() if srcW <= width { return src } dstW := width dstH := srcH * width / srcW if dstH < 1 { dstH = 1 } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) // JPEGs decode to *image.YCbCr. Reading its planes directly avoids an // interface call and a colour conversion per source pixel, which makes the // resize ~4x faster; everything else takes the generic At() path. avg := boxAvg if ycc, ok := src.(*image.YCbCr); ok { avg = func(_ image.Image, r image.Rectangle) color.RGBA { return boxAvgYCbCr(ycc, r) } } for y := 0; y < dstH; y++ { sy0 := y * srcH / dstH sy1 := (y + 1) * srcH / dstH if sy1 == sy0 { sy1 = sy0 + 1 } for x := 0; x < dstW; x++ { sx0 := x * srcW / dstW sx1 := (x + 1) * srcW / dstW if sx1 == sx0 { sx1 = sx0 + 1 } box := image.Rect(sx0, sy0, sx1, sy1).Add(b.Min) dst.SetRGBA(x, y, avg(src, box)) } } return dst } // boxAvg returns the mean colour of the pixels of src inside r. func boxAvg(src image.Image, r image.Rectangle) color.RGBA { var red, g, bl, n uint64 for y := r.Min.Y; y < r.Max.Y; y++ { for x := r.Min.X; x < r.Max.X; x++ { sr, sg, sb, _ := src.At(x, y).RGBA() red += uint64(sr >> 8) g += uint64(sg >> 8) bl += uint64(sb >> 8) n++ } } return color.RGBA{R: uint8(red / n), G: uint8(g / n), B: uint8(bl / n), A: 255} } // boxAvgYCbCr is boxAvg for *image.YCbCr, reading the sample planes directly. // Each pixel is still converted to RGB before averaging so out-of-gamut // samples clamp exactly as they do through At(). func boxAvgYCbCr(src *image.YCbCr, r image.Rectangle) color.RGBA { var red, g, bl, n uint64 for y := r.Min.Y; y < r.Max.Y; y++ { for x := r.Min.X; x < r.Max.X; x++ { ci := src.COffset(x, y) sr, sg, sb := color.YCbCrToRGB(src.Y[src.YOffset(x, y)], src.Cb[ci], src.Cr[ci]) red += uint64(sr) g += uint64(sg) bl += uint64(sb) n++ } } return color.RGBA{R: uint8(red / n), G: uint8(g / n), B: uint8(bl / n), A: 255} }