// Package overflow answers one question about a rendering: is any of it // off the side of the screen it was taken for. // // A full-page screenshot is captured at the page's own content size, not // at the viewport's, so a page that scrolls sideways on a phone comes back // wider than the phone. That number was already in every report and read // as a size, which is how a card that pushed its status badge off a 390px // screen sat in thirty-odd screenshots without anyone calling it a bug. // // Width alone is not enough to act on, though. An absolutely positioned // tooltip bubble -- invisible at rest, still counted -- widens a page by // forty pixels with nothing drawn out there, and a check that reports that // the same way it reports clipped content is a check that gets ignored. So // this also says whether anything is actually painted past the edge. package overflow import "image" // Result is what a rendering looks like at its right-hand edge. type Result struct { // Past is how many CSS pixels the rendering extends beyond the // viewport. Zero means it fits. Past int // Ink says whether anything is drawn out there. False means the page // scrolls sideways onto blank space: still a bug, but a different and // much smaller one than content a person cannot see. Ink bool } // Overflows reports whether there is anything to say at all. func (r Result) Overflows() bool { return r.Past > 0 } // tolerance is how different two pixels in the band must be before one of // them counts as ink, on the 16-bit scale image.Color uses. A background // that shades gently across forty pixels is not content; a badge is. const tolerance = 12 << 8 // Measure compares a rendering against the viewport it was taken for. // // scale is the device pixel ratio the shot was captured at, so that this // works in CSS pixels the way the rest of a report does. func Measure(img image.Image, viewportWidth int, scale float64) Result { b := img.Bounds() res := Result{Past: int(float64(b.Dx())/scale) - viewportWidth} if res.Past <= 0 { res.Past = 0 return res } // Start a little inside the band. A border or a shadow drawn at the // viewport's edge is anti-aliased across it, and half a border pixel // is not content that fell off the screen. x0 := b.Min.X + int(float64(viewportWidth)*scale) + 2 if x0 >= b.Max.X { return res } for y := b.Min.Y; y < b.Max.Y; y++ { r0, g0, b0, _ := img.At(x0, y).RGBA() for x := x0 + 1; x < b.Max.X; x++ { r, g, bl, _ := img.At(x, y).RGBA() if delta(r, r0) > tolerance || delta(g, g0) > tolerance || delta(bl, b0) > tolerance { res.Ink = true return res } } } return res } func delta(a, b uint32) uint32 { if a > b { return a - b } return b - a }