git/intake/main
internal/tui/scale.go
189 lines4 KB last change raw · history
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
}