119 lines
3.2 KiB
Go
119 lines
3.2 KiB
Go
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}
|
|
}
|