diff --git a/clip_test.go b/clip_test.go new file mode 100644 index 0000000..c12694b --- /dev/null +++ b/clip_test.go @@ -0,0 +1,49 @@ +package render + +import ( + "image/color" + "testing" + + "github.com/go-pdfkit/reader" +) + +func TestAClipThatIsNotThereLetsEverythingThrough(t *testing.T) { + // Every place that asks the clip how much of a pixel it allows checks + // first whether there is one; the method answers for itself as well, so + // that adding a place cannot make a page vanish. + var none *clip + if got := none.at(0, 0); got != 1 { + t.Errorf("no clip at all let through %v", got) + } +} + +func TestTwoClipsThatDoNotOverlapLetNothingThrough(t *testing.T) { + // Clipping to one corner and then to another leaves nothing: what is left + // is what both allow, and they allow nothing in common. + d := shadedPage(t, "q 0 0 40 40 re W n 60 60 40 40 re W n 0 0 100 100 re f Q", + func(w *reader.Writer) reader.Dict { return reader.Dict{} }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + for _, at := range [][2]int{{20, 20}, {20, 80}, {80, 20}, {80, 80}, {50, 50}} { + wantWhite(t, img, at[0], at[1]) + } +} + +func TestAClipKeepsToItsOwnCorner(t *testing.T) { + // And when they do overlap, what is drawn is the overlap and nothing + // else — which is what says the box was narrowed rather than forgotten. + d := shadedPage(t, "q 0 0 60 60 re W n 40 40 60 60 re W n 0 0 100 100 re f Q", + func(w *reader.Writer) reader.Dict { return reader.Dict{} }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + // The overlap is x 40..60, y 40..60 in the page's own coordinates, which + // is the middle of the image with its y counted the other way. + wantColour(t, img, 50, 50, color.RGBA{A: 255}, 4) + for _, at := range [][2]int{{20, 80}, {80, 20}, {20, 20}, {80, 80}} { + wantWhite(t, img, at[0], at[1]) + } +} diff --git a/image.go b/image.go index 741403a..3921ee3 100644 --- a/image.go +++ b/image.go @@ -52,7 +52,7 @@ func (r *renderer) drawImage(g *gstate, s *sampled) { c := s.at(sx, sy) alpha := float64(c.A) / 255 * g.fillAlpha if g.clip != nil { - alpha *= float64(g.clip[y*r.img.W+x]) + alpha *= g.clip.at(x, y) } if g.softMask != nil { alpha *= maskLevel(g.softMask[y*r.img.W+x]) diff --git a/pattern.go b/pattern.go index a00596a..a5a9a9d 100644 --- a/pattern.go +++ b/pattern.go @@ -106,7 +106,7 @@ func (r *renderer) paintShading(g *gstate, sh *shading, m geometry.Matrix, cov [ for x := 0; x < w; x++ { a := cov[y*w+x] * alpha if g.clip != nil { - a *= float64(g.clip[(oy+y)*r.img.W+(ox+x)]) + a *= g.clip.at(ox+x, oy+y) } if g.softMask != nil { a *= maskLevel(g.softMask[(oy+y)*r.img.W+(ox+x)]) diff --git a/state.go b/state.go index 7790e35..4302443 100644 --- a/state.go +++ b/state.go @@ -5,6 +5,7 @@ import ( "github.com/go-gfx/gfx/raster" "github.com/go-gfx/gfx/vector" "github.com/go-pdfkit/reader" + "image" "image/color" ) @@ -43,9 +44,9 @@ type gstate struct { // text is everything the show operators read besides the string. text textState - // clip is the coverage every mark is multiplied by, one value per pixel of - // the image, or nil when nothing is clipped away. - clip []float32 + // clip is the shape everything is drawn through, or nil when nothing is + // clipped away. + clip *clip // softMask is a second such grid, and multiplies alongside the clip: a // clip is a shape and a soft mask is a picture, but both come down to how @@ -151,7 +152,7 @@ func (r *renderer) mask(g *gstate, cov []float64, ox, oy, w, h int, alpha float6 i := y*w + x v := out[i] * alpha if g.clip != nil { - v *= float64(g.clip[(oy+y)*r.img.W+(ox+x)]) + v *= g.clip.at(ox+x, oy+y) } if g.softMask != nil { v *= maskLevel(g.softMask[(oy+y)*r.img.W+(ox+x)]) @@ -165,20 +166,67 @@ func (r *renderer) mask(g *gstate, cov []float64, ox, oy, w, h int, alpha float6 // maskLevel turns one of a mask's bytes into how much it lets through. func maskLevel(v uint8) float64 { return float64(v) / 255 } +// A clip is what a page is allowed to draw on: the box it has narrowed itself +// down to, and how much of each pixel inside that box is allowed through. +// +// It is kept as a box rather than as a value for every pixel of the page +// because a page narrows its clip over and over — a real government form does +// it two thousand one hundred and forty times, and an arXiv figure six +// thousand seven hundred — and a full page of floats each time is four +// kilobytes per hundred pixels of paper, whether or not the shape covers any +// of it. Measured before this: that form allocated 4 226 MB, and one figure +// asked for 97 333 MB to draw a single page. +type clip struct { + ox, oy, w, h int + // cov is one value a pixel over the box, and nothing outside it. A clip + // whose box is empty lets nothing through at all, which is what a page + // asks for when it clips to a shape that covers no pixel. + cov []float32 +} + +// at is how much of one pixel of the image the clip lets through. Everything +// outside the box is outside the clip. +func (c *clip) at(x, y int) float64 { + if c == nil { + return 1 + } + if x < c.ox || y < c.oy || x >= c.ox+c.w || y >= c.oy+c.h { + return 0 + } + return float64(c.cov[(y-c.oy)*c.w+(x-c.ox)]) +} + // narrow intersects the clip with one coverage grid: what was already hidden // stays hidden, and everything outside the new shape joins it. func (r *renderer) narrow(g *gstate, cov []float64, ox, oy, w, h int, ok bool) { - next := make([]float32, r.img.W*r.img.H) - if ok { - for y := 0; y < h; y++ { - for x := 0; x < w; x++ { - next[(oy+y)*r.img.W+(ox+x)] = float32(cov[y*w+x]) - } - } + if !ok { + // The shape covered no pixel, so nothing may be drawn from here on. + g.clip = &clip{} + return } + // What is left is what both shapes allow, so the new box need be no + // larger than the smaller of the two — which is what keeps a page that + // narrows itself repeatedly from paying for the whole sheet each time. + box := image.Rect(ox, oy, ox+w, oy+h) if g.clip != nil { - for i := range next { - next[i] *= g.clip[i] + box = box.Intersect(image.Rect(g.clip.ox, g.clip.oy, + g.clip.ox+g.clip.w, g.clip.oy+g.clip.h)) + } + box = box.Intersect(image.Rect(0, 0, r.img.W, r.img.H)) + if box.Empty() { + g.clip = &clip{} + return + } + next := &clip{ox: box.Min.X, oy: box.Min.Y, w: box.Dx(), h: box.Dy()} + next.cov = make([]float32, next.w*next.h) + for y := 0; y < next.h; y++ { + for x := 0; x < next.w; x++ { + px, py := next.ox+x, next.oy+y + v := float32(cov[(py-oy)*w+(px-ox)]) + if g.clip != nil { + v *= float32(g.clip.at(px, py)) + } + next.cov[y*next.w+x] = v } } g.clip = next