package tui import ( "image" "image/color" ) const maxPreviewPixels = 1024 func fitPixels(w, h, max int) (int, int) { if w <= max && h <= max { return w, h } if w >= h { nh := h * max / w if nh < 1 { nh = 1 } return max, nh } nw := w * max / h if nw < 1 { nw = 1 } return nw, max } func scaleDown(src image.Image, dw, dh int) image.Image { // averages blocks instead of dropping pixels, which turns a photo into noise // at preview size. dw/dh are the exact target, not a bound, so a caller that // already worked out the aspect ratio doesn't get it recomputed from one number. b := src.Bounds() sw, sh := b.Dx(), b.Dy() if dw < 1 { dw = 1 } if dh < 1 { dh = 1 } if dw >= sw && dh >= sh { return src } dst := image.NewRGBA(image.Rect(0, 0, dw, dh)) switch s := src.(type) { case *image.YCbCr: scaleYCbCr(dst, s, b, sw, sh, dw, dh) case *image.RGBA: scaleRGBA(dst, s, b, sw, sh, dw, dh) case *image.NRGBA: scaleNRGBA(dst, s, b, sw, sh, dw, dh) default: scaleGeneric(dst, src, b, sw, sh, dw, dh) } return dst } func scaleYCbCr(dst *image.RGBA, s *image.YCbCr, b image.Rectangle, sw, sh, dw, dh int) { for y := 0; y < dh; y++ { y0 := b.Min.Y + y*sh/dh y1 := b.Min.Y + (y+1)*sh/dh if y1 <= y0 { y1 = y0 + 1 } for x := 0; x < dw; x++ { x0 := b.Min.X + x*sw/dw x1 := b.Min.X + (x+1)*sw/dw if x1 <= x0 { x1 = x0 + 1 } var r, g, bl, n uint32 for yy := y0; yy < y1; yy++ { for xx := x0; xx < x1; xx++ { c := s.YCbCrAt(xx, yy) cr, cg, cb := color.YCbCrToRGB(c.Y, c.Cb, c.Cr) r += uint32(cr) g += uint32(cg) bl += uint32(cb) n++ } } dst.SetRGBA(x, y, color.RGBA{uint8(r / n), uint8(g / n), uint8(bl / n), 0xff}) } } } func scaleRGBA(dst *image.RGBA, s *image.RGBA, b image.Rectangle, sw, sh, dw, dh int) { off := func(x, y int) int { return s.PixOffset(x, y) } for y := 0; y < dh; y++ { y0 := b.Min.Y + y*sh/dh y1 := b.Min.Y + (y+1)*sh/dh if y1 <= y0 { y1 = y0 + 1 } for x := 0; x < dw; x++ { x0 := b.Min.X + x*sw/dw x1 := b.Min.X + (x+1)*sw/dw if x1 <= x0 { x1 = x0 + 1 } var r, g, bl, a, n uint32 for yy := y0; yy < y1; yy++ { for xx := x0; xx < x1; xx++ { i := off(xx, yy) r += uint32(s.Pix[i]) g += uint32(s.Pix[i+1]) bl += uint32(s.Pix[i+2]) a += uint32(s.Pix[i+3]) n++ } } dst.SetRGBA(x, y, color.RGBA{uint8(r / n), uint8(g / n), uint8(bl / n), uint8(a / n)}) } } } func scaleNRGBA(dst *image.RGBA, s *image.NRGBA, b image.Rectangle, sw, sh, dw, dh int) { off := func(x, y int) int { return s.PixOffset(x, y) } for y := 0; y < dh; y++ { y0 := b.Min.Y + y*sh/dh y1 := b.Min.Y + (y+1)*sh/dh if y1 <= y0 { y1 = y0 + 1 } for x := 0; x < dw; x++ { x0 := b.Min.X + x*sw/dw x1 := b.Min.X + (x+1)*sw/dw if x1 <= x0 { x1 = x0 + 1 } var r, g, bl, a, n uint32 for yy := y0; yy < y1; yy++ { for xx := x0; xx < x1; xx++ { i := off(xx, yy) al := uint32(s.Pix[i+3]) r += uint32(s.Pix[i]) * al / 0xff g += uint32(s.Pix[i+1]) * al / 0xff bl += uint32(s.Pix[i+2]) * al / 0xff a += al n++ } } if a == 0 { continue } dst.SetRGBA(x, y, color.RGBA{uint8(r * 0xff / a), uint8(g * 0xff / a), uint8(bl * 0xff / a), uint8(a / n)}) } } } func scaleGeneric(dst *image.RGBA, src image.Image, b image.Rectangle, sw, sh, dw, dh int) { for y := 0; y < dh; y++ { y0 := b.Min.Y + y*sh/dh y1 := b.Min.Y + (y+1)*sh/dh if y1 <= y0 { y1 = y0 + 1 } for x := 0; x < dw; x++ { x0 := b.Min.X + x*sw/dw x1 := b.Min.X + (x+1)*sw/dw if x1 <= x0 { x1 = x0 + 1 } var r, g, bl, a, n uint32 for yy := y0; yy < y1; yy++ { for xx := x0; xx < x1; xx++ { cr, cg, cb, ca := src.At(xx, yy).RGBA() r += cr >> 8 g += cg >> 8 bl += cb >> 8 a += ca >> 8 n++ } } dst.SetRGBA(x, y, color.RGBA{uint8(r / n), uint8(g / n), uint8(bl / n), uint8(a / n)}) } } } func pngExceedsPreview(data []byte) bool { const ihdr = 16 if len(data) < ihdr+8 || string(data[12:16]) != "IHDR" { return false } w := int(data[16])<<24 | int(data[17])<<16 | int(data[18])<<8 | int(data[19]) h := int(data[20])<<24 | int(data[21])<<16 | int(data[22])<<8 | int(data[23]) return w > maxPreviewPixels || h > maxPreviewPixels }