Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions clip_test.go
Original file line number Diff line number Diff line change
@@ -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])
}
}
2 changes: 1 addition & 1 deletion image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
2 changes: 1 addition & 1 deletion pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)])
Expand Down
74 changes: 61 additions & 13 deletions state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)])
Expand All @@ -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
Expand Down
Loading