diff --git a/README.md b/README.md index 7a3aea8..4ab05bf 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,21 @@ the format has — a sampled grid, an exponential curve, several of them stitched end to end, and a little PostScript program — which are also what a Separation or DeviceN colour space's tint transform is written in. -The mesh shadings and the standard fourteen faces are the waves that -follow. The corpus holds 371 mesh shadings of the fourth kind and 198 of -the seventh, against 19 923 axial and 5 589 radial ones. +And the **mesh shadings**, all four of them, which is how a plotting tool +writes a surface: free-form and lattice-form triangles, each corner its own +colour, and **Coons** and **tensor** patches, whose four sides are curves. +A patch is drawn by cutting it into a grid of little quadrilaterals — the +inside of a Coons patch follows from its twelve boundary points, and a +tensor patch says four more — and every triangle is filled by mixing the +colours at its corners across it. A patch may carry on from the one before +it, sharing an edge and two of its colours, which is how a surface is +written without repeating a single point. + +A pattern is placed in the space of the content that names it. That is the +page's own space at the top level, and inside a **form** it is the form's: +its matrix and the transform that drew it both count. Nearly every figure +a plotting tool writes is a form, so a pattern that stayed at the page's +origin would miss the shape it was asked to fill by the width of a page. ## How it is checked diff --git a/mesh.go b/mesh.go new file mode 100644 index 0000000..2001729 --- /dev/null +++ b/mesh.go @@ -0,0 +1,346 @@ +package render + +import ( + "image/color" + "math" + + "github.com/go-gfx/gfx/geometry" + "github.com/go-pdfkit/reader" +) + +// The four mesh shadings are the ones that carry their colours in a stream +// rather than in a function: a page says where the corners are and what +// colour each is, and the shape between them is filled in. +// +// Types 4 and 5 are triangles — free-form, where every vertex says how it +// joins the ones before it, and lattice-form, where they come in rows of a +// stated length. Types 6 and 7 are patches with curved sides: a Coons patch, +// whose inside follows from its twelve boundary points, and a tensor patch, +// which says four more points for the inside as well. This draws all four, +// and draws a patch by cutting it into small quadrilaterals, which is what +// makes a curved-sided patch a thing that can be filled at all. +type mesh struct { + triangles []triangle +} + +// A triangle is three corners, each with a colour of its own, in the +// shading's own space. +type triangle struct { + x, y [3]float64 + c [3]color.RGBA +} + +// A vertex is one corner as the stream holds it. +type vertex struct { + x, y float64 + c color.RGBA +} + +// maxMeshTriangles bounds how much a file may ask to be drawn, so that a +// stream naming a million patches cannot ask for the afternoon. +const maxMeshTriangles = 1 << 20 + +// patchSteps is how finely a curved-sided patch is cut up. Sixteen along each +// side is past the point where a step shows at any size a page is looked at, +// and is 512 triangles a patch. +const patchSteps = 16 + +// readMesh reads the vertices or patches a mesh shading's stream holds. +func (r *renderer) readMesh(sh *shading, stream *reader.Stream) *mesh { + data, img, err := r.doc.DecodeStream(stream) + if err != nil || img != "" { + return nil + } + if sh.fn != nil && sh.fn.outputs() != sh.space.components { + return nil + } + bits := r.meshBits(stream.Dict, sh) + if bits == nil { + return nil + } + rd := &meshReader{data: data, bits: *bits, sh: sh} + m := &mesh{} + switch sh.kind { + case 4: + rd.freeTriangles(m) + case 5: + rd.latticeTriangles(m, bits.perRow) + case 6, 7: + rd.patches(m, sh.kind) + } + if len(m.triangles) == 0 { + return nil + } + return m +} + +// meshBits is how wide each number in the stream is, and what range each maps +// onto. +type meshBits struct { + coord, comp, flag int + decode []float64 + // components is how many numbers a colour takes in the stream: one when + // the shading names a function, which turns that one into a colour, and + // otherwise as many as its space has. + components int + perRow int +} + +// meshBits reads the widths and the decode array a mesh stream is written +// with, and refuses one that says something it cannot mean. +func (r *renderer) meshBits(dict reader.Dict, sh *shading) *meshBits { + b := &meshBits{components: sh.space.components} + if sh.fn != nil { + b.components = 1 + } + b.coord = int(intOr(resolve(r.doc, dict.Get("BitsPerCoordinate")), 0)) + switch b.coord { + case 1, 2, 4, 8, 12, 16, 24, 32: + default: + return nil + } + b.comp = int(intOr(resolve(r.doc, dict.Get("BitsPerComponent")), 0)) + switch b.comp { + case 1, 2, 4, 8, 12, 16: + default: + return nil + } + if sh.kind != 5 { + b.flag = int(intOr(resolve(r.doc, dict.Get("BitsPerFlag")), 0)) + switch b.flag { + case 2, 4, 8: + default: + return nil + } + } else { + b.perRow = int(intOr(resolve(r.doc, dict.Get("VerticesPerRow")), 0)) + if b.perRow < 2 || b.perRow > 1<<16 { + return nil + } + } + b.decode = r.floatArray(dict.Get("Decode")) + if len(b.decode) < 4+2*b.components { + return nil + } + return b +} + +// A meshReader walks the packed stream a bit at a time. +type meshReader struct { + data []byte + at int // in bits + bits meshBits + sh *shading + bad bool +} + +// done reports whether there is nothing left worth reading. +func (r *meshReader) done() bool { return r.bad || r.at >= len(r.data)*8 } + +// read takes one number of the given width. +func (r *meshReader) read(width int) uint64 { + if r.at+width > len(r.data)*8 { + r.bad = true + return 0 + } + var v uint64 + for k := 0; k < width; k++ { + i := r.at + k + v = v<<1 | uint64(r.data[i/8]>>(7-i%8)&1) + } + r.at += width + return v +} + +// align moves to the next byte, which is where every vertex and every patch +// begins. +func (r *meshReader) align() { + if r.at%8 != 0 { + r.at += 8 - r.at%8 + } +} + +// coordinate reads one packed number and maps it onto what the decode array +// says it means. +func (r *meshReader) coordinate(i int) float64 { + raw := r.read(r.bits.coord) + max := float64(uint64(1)<= 3 { + have = 0 + } + switch have { + case 0: + a, have = v, 1 + case 1: + b, have = v, 2 + default: + c, have = v, 3 + m.add(a, b, c) + } + case flag == 1: + a, b, c = b, c, v + m.add(a, b, c) + default: + b, c = c, v + m.add(a, b, c) + } + } +} + +// latticeTriangles reads a type 5 mesh: rows of a stated length, with two +// triangles between every pair of neighbours in consecutive rows. +func (r *meshReader) latticeTriangles(m *mesh, perRow int) { + var previous []vertex + for !r.done() && len(m.triangles) < maxMeshTriangles { + row := make([]vertex, 0, perRow) + for i := 0; i < perRow; i++ { + row = append(row, r.vertex()) + } + if r.bad { + return + } + if previous != nil { + for i := 0; i+1 < perRow; i++ { + m.add(previous[i], previous[i+1], row[i]) + m.add(previous[i+1], row[i+1], row[i]) + } + } + previous = row + } +} + +// add puts one triangle in the mesh. +func (m *mesh) add(a, b, c vertex) { + m.triangles = append(m.triangles, triangle{ + x: [3]float64{a.x, b.x, c.x}, + y: [3]float64{a.y, b.y, c.y}, + c: [3]color.RGBA{a.c, b.c, c.c}, + }) +} + +// A meshRaster is a mesh drawn into device pixels: the colour of every pixel +// the mesh covers, and nothing where it covers none. A mesh is the one kind of +// shading that cannot be asked what colour a point is without first working +// out which triangle the point is in, so it is drawn once and then read. +type meshRaster struct { + ox, oy, w, h int + // col holds one colour a pixel, with a zero alpha where the mesh does not + // reach; every colour it does set is opaque. + col []color.RGBA +} + +// at is the colour of one device pixel, and false where the mesh covers none. +func (m *meshRaster) at(x, y int) (color.RGBA, bool) { + i := (y-m.oy)*m.w + (x - m.ox) + if i < 0 || i >= len(m.col) || m.col[i].A == 0 { + return color.RGBA{}, false + } + return m.col[i], true +} + +// rasterise draws every triangle of the mesh into a patch of device pixels. +// Neighbouring triangles agree along the edge they share, so where two of them +// both claim a pixel it does not matter which one wins. +func (m *mesh) rasterise(t geometry.Matrix, ox, oy, w, h int) *meshRaster { + out := &meshRaster{ox: ox, oy: oy, w: w, h: h, col: make([]color.RGBA, w*h)} + for i := range m.triangles { + out.draw(&m.triangles[i], t) + } + return out +} + +// draw puts one triangle down, taking each pixel's colour from where it sits +// between the three corners. +func (r *meshRaster) draw(t *triangle, m geometry.Matrix) { + var px, py [3]float64 + for i := 0; i < 3; i++ { + p := m.TransformPoint(geometry.Point{X: t.x[i], Y: t.y[i]}) + if math.IsNaN(p.X) || math.IsNaN(p.Y) || math.IsInf(p.X, 0) || math.IsInf(p.Y, 0) { + return + } + px[i], py[i] = p.X, p.Y + } + area := (px[1]-px[0])*(py[2]-py[0]) - (px[2]-px[0])*(py[1]-py[0]) + if area == 0 { + return // a triangle with no inside covers nothing + } + loX := max(r.ox, int(math.Floor(min3(px)))) + hiX := min(r.ox+r.w, int(math.Ceil(max3(px)))+1) + loY := max(r.oy, int(math.Floor(min3(py)))) + hiY := min(r.oy+r.h, int(math.Ceil(max3(py)))+1) + // A pixel exactly on a shared edge belongs to both triangles that meet + // there; letting it in on both sides is what keeps a seam from showing. + const inside = -1e-9 + for y := loY; y < hiY; y++ { + for x := loX; x < hiX; x++ { + cx, cy := float64(x)+0.5, float64(y)+0.5 + w0 := ((px[1]-cx)*(py[2]-cy) - (px[2]-cx)*(py[1]-cy)) / area + w1 := ((px[2]-cx)*(py[0]-cy) - (px[0]-cx)*(py[2]-cy)) / area + w2 := 1 - w0 - w1 + if w0 < inside || w1 < inside || w2 < inside { + continue + } + r.col[(y-r.oy)*r.w+(x-r.ox)] = mixThree(t.c, w0, w1, w2) + } + } +} + +// mixThree is the colour a point takes from the three corners around it. +func mixThree(c [3]color.RGBA, w0, w1, w2 float64) color.RGBA { + part := func(get func(color.RGBA) uint8) uint8 { + v := w0*float64(get(c[0])) + w1*float64(get(c[1])) + w2*float64(get(c[2])) + return byteOf(v / 255) + } + return color.RGBA{ + R: part(func(c color.RGBA) uint8 { return c.R }), + G: part(func(c color.RGBA) uint8 { return c.G }), + B: part(func(c color.RGBA) uint8 { return c.B }), + A: 255, + } +} + +// min3 and max3 are the ends of a triangle's reach along one axis. +func min3(v [3]float64) float64 { return math.Min(v[0], math.Min(v[1], v[2])) } +func max3(v [3]float64) float64 { return math.Max(v[0], math.Max(v[1], v[2])) } diff --git a/mesh_test.go b/mesh_test.go new file mode 100644 index 0000000..8ba7b42 --- /dev/null +++ b/mesh_test.go @@ -0,0 +1,524 @@ +package render + +import ( + "image/color" + "math" + + "github.com/go-gfx/gfx/raster" + "testing" + + "github.com/go-pdfkit/reader" +) + +// meshBytes packs the numbers a mesh stream is made of, a bit at a time, since +// the widths a file may name are not all whole bytes. +type meshBytes struct { + b []byte + n int // bits written +} + +// bits writes one number of the given width. +func (m *meshBytes) bits(v uint64, w int) *meshBytes { + for k := w - 1; k >= 0; k-- { + if m.n%8 == 0 { + m.b = append(m.b, 0) + } + if v>>uint(k)&1 == 1 { + m.b[len(m.b)-1] |= 1 << (7 - m.n%8) + } + m.n++ + } + return m +} + +// coord writes one 32-bit coordinate, mapped onto the nought-to-a-hundred +// decode range the tests below all use. +func (m *meshBytes) coord(v float64) *meshBytes { + return m.bits(uint64(math.Round(v/100*float64(^uint32(0)))), 32) +} + +// point writes a place on the page. +func (m *meshBytes) point(x, y float64) *meshBytes { return m.coord(x).coord(y) } + +// rgb writes one colour, three components eight bits wide. +func (m *meshBytes) rgb(c color.RGBA) *meshBytes { + return m.bits(uint64(c.R), 8).bits(uint64(c.G), 8).bits(uint64(c.B), 8) +} + +// flag writes the byte that says how a vertex or a patch joins the last one. +func (m *meshBytes) flag(v byte) *meshBytes { return m.bits(uint64(v), 8) } + +// one writes a single component, for a mesh whose colours go through a +// function. +func (m *meshBytes) one(v byte) *meshBytes { return m.bits(uint64(v), 8) } + +var ( + meshRed = color.RGBA{R: 255, A: 255} + meshGreen = color.RGBA{G: 255, A: 255} + meshBlue = color.RGBA{B: 255, A: 255} + meshWhite = color.RGBA{R: 255, G: 255, B: 255, A: 255} +) + +// meshDecode is the decode array every test here writes its numbers against: +// a hundred points each way, and colour components from nought to one. +func meshDecode(components int) reader.Array { + v := []float64{0, 100, 0, 100} + for i := 0; i < components; i++ { + v = append(v, 0, 1) + } + return nums(v...) +} + +// meshShading builds a page that paints one mesh over the whole of it. +func meshShading(t *testing.T, kind int, data []byte, extra reader.Dict) *reader.Document { + t.Helper() + return shadedPage(t, "/S1 sh", func(w *reader.Writer) reader.Dict { + dict := reader.Dict{ + "ShadingType": reader.Integer(int64(kind)), + "ColorSpace": reader.Name("DeviceRGB"), + "BitsPerCoordinate": reader.Integer(32), + "BitsPerComponent": reader.Integer(8), + "BitsPerFlag": reader.Integer(8), + "Decode": meshDecode(3), + } + for k, v := range extra { + dict[k] = v + } + return reader.Dict{"Shading": reader.Dict{ + "S1": w.Add(&reader.Stream{Dict: dict, Raw: data}), + }} + }) +} + +// renderMesh draws such a page. +func renderMesh(t *testing.T, kind int, data []byte, extra reader.Dict) *raster.Image { + t.Helper() + img, err := Page(meshShading(t, kind, data, extra), 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + return img +} + +// nearly says whether two colours are close enough that a rounding either way +// does not fail the test. +func nearly(a, b color.RGBA, tol int) bool { + d := func(x, y uint8) int { + if x > y { + return int(x - y) + } + return int(y - x) + } + return d(a.R, b.R) <= tol && d(a.G, b.G) <= tol && d(a.B, b.B) <= tol +} + +// wantMeshColour fails unless the pixel is what it should be. +func wantMeshColour(t *testing.T, img *raster.Image, x, y int, want color.RGBA, why string) { + t.Helper() + if got := img.At(x, y); !nearly(got, want, 15) { + t.Errorf("%s: pixel (%d,%d) is %v, wanted %v", why, x, y, got, want) + } +} + +// freeTriangle is one triangle written the way a type 4 stream writes it. +func freeTriangle() []byte { + m := &meshBytes{} + m.flag(0).point(0, 0).rgb(meshRed) + m.flag(0).point(100, 0).rgb(meshGreen) + m.flag(0).point(0, 100).rgb(meshBlue) + return m.b +} + +func TestAFreeFormTriangleMeshIsDrawn(t *testing.T) { + // The three corners take their own colours and the half of the page the + // triangle does not reach is left as paper. + img := renderMesh(t, 4, freeTriangle(), nil) + wantMeshColour(t, img, 2, 97, meshRed, "the corner at the origin") + wantMeshColour(t, img, 97, 97, meshGreen, "the corner along the bottom") + wantMeshColour(t, img, 2, 2, meshBlue, "the corner up the side") + wantMeshColour(t, img, 90, 10, meshWhite, "outside the triangle") + // The middle of the hypotenuse is halfway between two of the corners. + wantMeshColour(t, img, 50, 50, color.RGBA{G: 128, B: 128, A: 255}, "the middle of the long side") +} + +func TestAFreeFormMeshCarriesTwoVerticesOn(t *testing.T) { + // A flag of one keeps the last two vertices, a flag of two keeps the + // first and the last: a strip written without repeating anything. + m := &meshBytes{} + m.flag(0).point(0, 0).rgb(meshRed) + m.flag(0).point(100, 0).rgb(meshRed) + m.flag(0).point(0, 100).rgb(meshRed) + m.flag(1).point(100, 100).rgb(meshBlue) + img := renderMesh(t, 4, m.b, nil) + wantMeshColour(t, img, 2, 97, meshRed, "the first triangle") + wantMeshColour(t, img, 97, 2, meshBlue, "the corner the second triangle added") + + m = &meshBytes{} + m.flag(0).point(0, 0).rgb(meshRed) + m.flag(0).point(100, 0).rgb(meshRed) + m.flag(0).point(0, 100).rgb(meshRed) + m.flag(2).point(100, 100).rgb(meshBlue) + img = renderMesh(t, 4, m.b, nil) + wantMeshColour(t, img, 97, 2, meshBlue, "the corner the flag of two added") +} + +func TestAFreeFormMeshStartsAgainOnAZeroFlag(t *testing.T) { + // A zero flag after a whole triangle throws the three away and begins + // another, which is the only way a stream says two separate shapes. + m := &meshBytes{} + m.flag(0).point(0, 0).rgb(meshRed) + m.flag(0).point(40, 0).rgb(meshRed) + m.flag(0).point(0, 40).rgb(meshRed) + m.flag(0).point(60, 60).rgb(meshBlue) + m.flag(0).point(100, 60).rgb(meshBlue) + m.flag(0).point(60, 100).rgb(meshBlue) + img := renderMesh(t, 4, m.b, nil) + wantMeshColour(t, img, 2, 97, meshRed, "the first triangle") + wantMeshColour(t, img, 65, 35, meshBlue, "the second triangle") + wantMeshColour(t, img, 90, 10, meshWhite, "between the two") +} + +func TestALatticeFormMeshIsDrawn(t *testing.T) { + // Two rows of two: the four corners of the page, each its own colour, + // with the square between them filled in. + m := &meshBytes{} + m.point(0, 0).rgb(meshRed) + m.point(100, 0).rgb(meshGreen) + m.point(0, 100).rgb(meshBlue) + m.point(100, 100).rgb(meshWhite) + img := renderMesh(t, 5, m.b, reader.Dict{"VerticesPerRow": reader.Integer(2)}) + wantMeshColour(t, img, 2, 97, meshRed, "the corner at the origin") + wantMeshColour(t, img, 97, 97, meshGreen, "the corner along the bottom") + wantMeshColour(t, img, 2, 2, meshBlue, "the corner up the side") + wantMeshColour(t, img, 97, 2, meshWhite, "the far corner") +} + +// flatPatch writes the twelve boundary points of a patch that covers the whole +// page and is not curved at all, so that what comes out can be read off. +func flatPatch(m *meshBytes, from int) *meshBytes { + net := func(i, j int) (float64, float64) { + return float64(i) * 100 / 3, float64(j) * 100 / 3 + } + order := [12][2]int{{0, 0}, {0, 1}, {0, 2}, {0, 3}, {1, 3}, {2, 3}, + {3, 3}, {3, 2}, {3, 1}, {3, 0}, {2, 0}, {1, 0}} + for i := from; i < 12; i++ { + m.point(net(order[i][0], order[i][1])) + } + return m +} + +func TestACoonsPatchIsDrawn(t *testing.T) { + // A flat patch with a different colour at each corner comes out as the + // four colours mixed across it, which is what says both the surface and + // the way round the corners go were read right. + m := &meshBytes{} + m.flag(0) + flatPatch(m, 0) + m.rgb(meshRed).rgb(meshGreen).rgb(meshBlue).rgb(meshWhite) + img := renderMesh(t, 6, m.b, nil) + wantMeshColour(t, img, 2, 97, meshRed, "the corner the patch starts at") + wantMeshColour(t, img, 2, 2, meshGreen, "the corner along the first side") + wantMeshColour(t, img, 97, 2, meshBlue, "the corner across the patch") + wantMeshColour(t, img, 97, 97, meshWhite, "the last corner") + wantMeshColour(t, img, 50, 50, color.RGBA{R: 128, G: 128, B: 128, A: 255}, "the middle") +} + +func TestATensorPatchWithACoonsInsideDrawsTheSame(t *testing.T) { + // A tensor patch says four more points; giving it the ones a Coons patch + // would have worked out for itself must draw the very same thing. + coons := &meshBytes{} + coons.flag(0) + flatPatch(coons, 0) + coons.rgb(meshRed).rgb(meshGreen).rgb(meshBlue).rgb(meshWhite) + + tensor := &meshBytes{} + tensor.flag(0) + flatPatch(tensor, 0) + for _, at := range [4][2]int{{1, 1}, {1, 2}, {2, 2}, {2, 1}} { + tensor.point(float64(at[0])*100/3, float64(at[1])*100/3) + } + tensor.rgb(meshRed).rgb(meshGreen).rgb(meshBlue).rgb(meshWhite) + + a := renderMesh(t, 6, coons.b, nil) + b := renderMesh(t, 7, tensor.b, nil) + for y := 0; y < 100; y += 7 { + for x := 0; x < 100; x += 7 { + if a.At(x, y) != b.At(x, y) { + t.Fatalf("pixel (%d,%d): the Coons patch is %v and the tensor patch %v", + x, y, a.At(x, y), b.At(x, y)) + } + } + } +} + +func TestAPatchCarriesTheEdgeOfTheOneBefore(t *testing.T) { + // Each of the three continuing flags shares a different edge. Whichever + // it is, the new patch keeps two colours as well as four points, so the + // seam between the two is the same colour on both sides. + for _, flag := range []byte{1, 2, 3} { + m := &meshBytes{} + m.flag(0) + flatPatch(m, 0) + m.rgb(meshRed).rgb(meshGreen).rgb(meshBlue).rgb(meshWhite) + m.flag(flag) + flatPatch(m, 4) + m.rgb(meshRed).rgb(meshRed) + img := renderMesh(t, 6, m.b, nil) + // The second patch reuses the first patch's own points, so it covers + // the page again; what matters is that it was read without running + // off the end and that something was drawn. + if img.At(50, 50) == meshWhite { + t.Errorf("flag %d: the middle of the page was left as paper", flag) + } + } +} + +func TestAPatchThatCarriesOnFromNothingIsRefused(t *testing.T) { + // A stream whose first patch says it shares an edge has nothing to share + // it with, so nothing is drawn rather than something made up. + m := &meshBytes{} + m.flag(1) + flatPatch(m, 4) + m.rgb(meshRed).rgb(meshGreen) + img := renderMesh(t, 6, m.b, nil) + wantMeshColour(t, img, 50, 50, meshWhite, "a patch with no patch before it") +} + +func TestAMeshColoursThroughAFunction(t *testing.T) { + // A mesh may write one number a vertex and name a function that turns it + // into a colour, which is how a gradient triangle is written small. + m := &meshBytes{} + m.flag(0).point(0, 0).one(0) + m.flag(0).point(100, 0).one(255) + m.flag(0).point(0, 100).one(0) + d := shadedPage(t, "/S1 sh", func(w *reader.Writer) reader.Dict { + return reader.Dict{"Shading": reader.Dict{"S1": w.Add(&reader.Stream{ + Dict: reader.Dict{ + "ShadingType": reader.Integer(4), "ColorSpace": reader.Name("DeviceRGB"), + "BitsPerCoordinate": reader.Integer(32), "BitsPerComponent": reader.Integer(8), + "BitsPerFlag": reader.Integer(8), "Decode": meshDecode(1), + "Function": rampFunction(w), + }, Raw: m.b})}} + }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + wantMeshColour(t, img, 2, 97, meshRed, "where the function was given nought") + wantMeshColour(t, img, 95, 97, meshBlue, "where it was given one") +} + +func TestAMeshWhoseFunctionGivesTheWrongNumberOfComponentsIsRefused(t *testing.T) { + m := &meshBytes{} + m.flag(0).point(0, 0).one(0) + m.flag(0).point(100, 0).one(255) + m.flag(0).point(0, 100).one(0) + d := shadedPage(t, "/S1 sh", func(w *reader.Writer) reader.Dict { + return reader.Dict{"Shading": reader.Dict{"S1": w.Add(&reader.Stream{ + Dict: reader.Dict{ + "ShadingType": reader.Integer(4), "ColorSpace": reader.Name("DeviceGray"), + "BitsPerCoordinate": reader.Integer(32), "BitsPerComponent": reader.Integer(8), + "BitsPerFlag": reader.Integer(8), "Decode": meshDecode(1), + "Function": rampFunction(w), // three out, one wanted + }, Raw: m.b})}} + }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + wantMeshColour(t, img, 2, 97, meshWhite, "a mesh whose function does not fit its space") +} + +func TestAMeshPaintsItsBackgroundWhereItReachesNothing(t *testing.T) { + img := renderMesh(t, 4, freeTriangle(), reader.Dict{"Background": nums(0, 1, 0)}) + wantMeshColour(t, img, 90, 10, meshGreen, "outside the triangle") + wantMeshColour(t, img, 2, 97, meshRed, "inside it") +} + +func TestAMeshKeepsToItsBoundingBox(t *testing.T) { + img := renderMesh(t, 4, freeTriangle(), reader.Dict{"BBox": nums(0, 50, 50, 100)}) + wantMeshColour(t, img, 2, 2, meshBlue, "inside the box") + wantMeshColour(t, img, 2, 97, meshWhite, "below the box") +} + +func TestAMeshIsUsedAsAPattern(t *testing.T) { + // A mesh may be a shading pattern rather than painted on its own, and a + // pattern is used to fill a shape. Two fills in a row read the drawing + // once and then again from what was kept. + d := shadedPage(t, "/Pattern cs /P1 scn 0 0 50 100 re f 50 0 50 100 re f", + func(w *reader.Writer) reader.Dict { + shading := w.Add(&reader.Stream{Dict: reader.Dict{ + "ShadingType": reader.Integer(4), "ColorSpace": reader.Name("DeviceRGB"), + "BitsPerCoordinate": reader.Integer(32), "BitsPerComponent": reader.Integer(8), + "BitsPerFlag": reader.Integer(8), "Decode": meshDecode(3), + }, Raw: freeTriangle()}) + return reader.Dict{"Pattern": reader.Dict{"P1": w.Add(reader.Dict{ + "PatternType": reader.Integer(2), "Shading": shading, + })}} + }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + wantMeshColour(t, img, 2, 97, meshRed, "the first fill") + wantMeshColour(t, img, 55, 97, color.RGBA{R: 114, G: 140, A: 255}, "the second fill") +} + +func TestAMeshThatSaysSomethingItCannotMeanIsRefused(t *testing.T) { + // Every width a mesh stream names has a short list of values it may take, + // and the decode array has to be long enough for what it describes. A + // stream that breaks one of those is not drawn at all. + for _, c := range []struct { + why string + kind int + extra reader.Dict + }{ + {"a coordinate width that is not one of the eight", 4, + reader.Dict{"BitsPerCoordinate": reader.Integer(7)}}, + {"a component width that is not one of the six", 4, + reader.Dict{"BitsPerComponent": reader.Integer(32)}}, + {"a flag width that is not two, four or eight", 4, + reader.Dict{"BitsPerFlag": reader.Integer(3)}}, + {"a decode array with too few numbers in it", 4, + reader.Dict{"Decode": nums(0, 100, 0, 100)}}, + {"a row length of one", 5, reader.Dict{"VerticesPerRow": reader.Integer(1)}}, + {"a row length past any sense", 5, reader.Dict{"VerticesPerRow": reader.Integer(1 << 20)}}, + } { + img := renderMesh(t, c.kind, freeTriangle(), c.extra) + wantMeshColour(t, img, 2, 97, meshWhite, c.why) + } +} + +func TestAMeshShadingThatIsNotAStreamIsRefused(t *testing.T) { + // The four mesh kinds carry their vertices in a stream; one written as a + // plain dictionary has nowhere to have put them. + d := shadedPage(t, "/S1 sh", func(w *reader.Writer) reader.Dict { + return reader.Dict{"Shading": reader.Dict{"S1": w.Add(reader.Dict{ + "ShadingType": reader.Integer(4), "ColorSpace": reader.Name("DeviceRGB"), + "BitsPerCoordinate": reader.Integer(32), "BitsPerComponent": reader.Integer(8), + "BitsPerFlag": reader.Integer(8), "Decode": meshDecode(3), + })}} + }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + wantMeshColour(t, img, 50, 50, meshWhite, "a mesh shading with no stream") +} + +func TestAMeshStreamThatStopsPartWayIsDrawnAsFarAsItGoes(t *testing.T) { + // Files are cut short. What was read whole is drawn; the half vertex at + // the end is not guessed at. + full := freeTriangle() + for _, n := range []int{0, 5, len(full) - 1} { + img := renderMesh(t, 4, full[:n], nil) + wantMeshColour(t, img, 2, 97, meshWhite, "a stream cut short before a triangle was whole") + } + // A fourth vertex begun and not finished leaves the first triangle. + m := &meshBytes{} + for _, b := range full { + m.bits(uint64(b), 8) + } + m.flag(1).coord(50) + img := renderMesh(t, 4, m.b, nil) + wantMeshColour(t, img, 2, 97, meshRed, "the triangle that was written whole") +} + +func TestAPatchStreamThatStopsPartWayIsRefused(t *testing.T) { + m := &meshBytes{} + m.flag(0) + flatPatch(m, 0) + m.rgb(meshRed) // three colours short + img := renderMesh(t, 6, m.b, nil) + wantMeshColour(t, img, 50, 50, meshWhite, "a patch whose colours were cut off") + + short := &meshBytes{} + short.flag(0) + short.point(0, 0) + img = renderMesh(t, 6, short.b, nil) + wantMeshColour(t, img, 50, 50, meshWhite, "a patch whose points were cut off") +} + +func TestATriangleWithNoInsideCoversNothing(t *testing.T) { + // Three vertices in a line make a triangle with no area, which is drawn + // by drawing nothing rather than by dividing by nought. + m := &meshBytes{} + m.flag(0).point(0, 0).rgb(meshRed) + m.flag(0).point(50, 50).rgb(meshRed) + m.flag(0).point(100, 100).rgb(meshRed) + img := renderMesh(t, 4, m.b, nil) + wantMeshColour(t, img, 20, 79, meshWhite, "a triangle with no inside") +} + +func TestAMeshDrawnThroughAnImpossibleTransformIsNotDrawn(t *testing.T) { + // A transform that squashes the page to a line cannot be undone, and a + // shading painted through one is left alone. + d := shadedPage(t, "q 0 0 0 0 0 0 cm /S1 sh Q", func(w *reader.Writer) reader.Dict { + return reader.Dict{"Shading": reader.Dict{"S1": w.Add(&reader.Stream{Dict: reader.Dict{ + "ShadingType": reader.Integer(4), "ColorSpace": reader.Name("DeviceRGB"), + "BitsPerCoordinate": reader.Integer(32), "BitsPerComponent": reader.Integer(8), + "BitsPerFlag": reader.Integer(8), "Decode": meshDecode(3), + }, Raw: freeTriangle()})}} + }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + wantMeshColour(t, img, 50, 50, meshWhite, "a mesh under a transform with no inverse") +} + +func TestAMeshStreamThatCannotBeDecodedIsRefused(t *testing.T) { + // A stream whose filter is one that gives back an image rather than + // bytes has no vertices in it to read. + img := renderMesh(t, 4, freeTriangle(), reader.Dict{"Filter": reader.Name("DCTDecode")}) + wantMeshColour(t, img, 50, 50, meshWhite, "a mesh stream that is a picture") +} + +func TestALatticeMeshWhoseLastRowIsCutShortStopsThere(t *testing.T) { + // Two whole rows and the beginning of a third: what was written whole is + // drawn and the part row is left. + m := &meshBytes{} + for _, v := range [][2]float64{{0, 0}, {100, 0}, {0, 100}, {100, 100}} { + m.point(v[0], v[1]).rgb(meshRed) + } + m.point(0, 100).rgb(meshBlue) // one vertex of a row of two + img := renderMesh(t, 5, m.b, reader.Dict{"VerticesPerRow": reader.Integer(2)}) + wantMeshColour(t, img, 50, 50, meshRed, "the rows that were written whole") +} + +func TestAPatchWhoseFlagRunsOffTheEndIsRefused(t *testing.T) { + // With a two-bit flag a patch is not a whole number of bytes long, so the + // next one begins part way through the last byte of the stream and there + // is nothing there to read. + m := &meshBytes{} + m.bits(0, 2) + flatPatch(m, 0) + m.rgb(meshRed).rgb(meshGreen).rgb(meshBlue).rgb(meshWhite) + if m.n%8 == 0 { + t.Fatalf("the patch came to %d bits, which is a whole number of bytes", m.n) + } + img := renderMesh(t, 6, m.b, reader.Dict{"BitsPerFlag": reader.Integer(2)}) + wantMeshColour(t, img, 50, 50, color.RGBA{R: 128, G: 128, B: 128, A: 255}, + "the patch that was written whole") +} + +func TestAMeshWhoseCoordinatesAreNotNumbersIsNotDrawn(t *testing.T) { + // A decode array wide enough to overflow gives coordinates that are not + // anywhere, and a triangle at no place covers no pixel. + huge := nums(-math.MaxFloat64, math.MaxFloat64, -math.MaxFloat64, math.MaxFloat64, 0, 1, 0, 1, 0, 1) + img := renderMesh(t, 4, freeTriangle(), reader.Dict{"Decode": huge}) + wantMeshColour(t, img, 50, 50, meshWhite, "a triangle whose corners are nowhere") +} + +func TestAShadingOfAKindThatDoesNotExistIsRefused(t *testing.T) { + d := shadedPage(t, "/S1 sh", func(w *reader.Writer) reader.Dict { + return reader.Dict{"Shading": reader.Dict{"S1": w.Add(reader.Dict{ + "ShadingType": reader.Integer(8), "ColorSpace": reader.Name("DeviceRGB"), + })}} + }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + wantMeshColour(t, img, 50, 50, meshWhite, "a shading of the eighth kind") +} diff --git a/patch.go b/patch.go new file mode 100644 index 0000000..b64085c --- /dev/null +++ b/patch.go @@ -0,0 +1,184 @@ +package render + +import "image/color" + +// A patch is the sixth and seventh kinds of shading: four curved sides and a +// colour at each corner. The sides are cubic Bézier curves, so a patch is +// twelve control points round the edge — and, for the seventh kind, four more +// inside that let the surface bulge where the edges alone would not. +// +// Nothing can fill a curved-sided patch directly, so it is cut into a grid of +// small quadrilaterals, each of them two triangles, and every corner takes its +// colour from where it sits in the patch. +type patch struct { + // p is the control net: p[i][j], where i runs with one parameter of the + // surface and j with the other. + p [4][4]point2 + // c is the colour at each corner, going round the patch from p[0][0] in + // the order the format writes them. + c [4]color.RGBA +} + +// A point2 is a place in the shading's own space. +type point2 struct{ x, y float64 } + +// The order the twelve boundary points arrive in, as positions in the control +// net: round the patch, starting at one corner. +var boundaryOrder = [12][2]int{ + {0, 0}, {0, 1}, {0, 2}, {0, 3}, + {1, 3}, {2, 3}, {3, 3}, + {3, 2}, {3, 1}, {3, 0}, + {2, 0}, {1, 0}, +} + +// The four inner points a tensor patch adds, in the order they arrive. +var innerOrder = [4][2]int{{1, 1}, {1, 2}, {2, 2}, {2, 1}} + +// The edge of the previous patch that a flag says this one shares, as the four +// control points it stands in for. A flag of one means the previous patch's +// far edge, two the one after that, three the one after that again; those +// become this patch's first four boundary points. +var sharedEdge = [4][4][2]int{ + 1: {{0, 3}, {1, 3}, {2, 3}, {3, 3}}, + 2: {{3, 3}, {3, 2}, {3, 1}, {3, 0}}, + 3: {{3, 0}, {2, 0}, {1, 0}, {0, 0}}, +} + +// The two colours that edge brings with it, as positions in the previous +// patch's colour list. +var sharedColours = [4][2]int{1: {1, 2}, 2: {2, 3}, 3: {3, 0}} + +// patches reads a type 6 or type 7 mesh and cuts every patch into triangles. +func (r *meshReader) patches(m *mesh, kind int) { + tensor := kind == 7 + var previous *patch + for !r.done() && len(m.triangles) < maxMeshTriangles { + r.align() + flag := int(r.read(r.bits.flag)) & 3 + if r.bad { + return + } + if flag != 0 && previous == nil { + // A patch that carries on from one that is not there. + return + } + p := &patch{} + start := 0 + if flag != 0 { + for i, at := range sharedEdge[flag] { + side := boundaryOrder[i] + p.p[side[0]][side[1]] = previous.p[at[0]][at[1]] + } + p.c[0] = previous.c[sharedColours[flag][0]] + p.c[1] = previous.c[sharedColours[flag][1]] + start = 4 + } + for i := start; i < 12; i++ { + at := boundaryOrder[i] + p.p[at[0]][at[1]] = point2{r.coordinate(0), r.coordinate(1)} + } + if tensor { + for _, at := range innerOrder { + p.p[at[0]][at[1]] = point2{r.coordinate(0), r.coordinate(1)} + } + } else { + p.fillCoonsInside() + } + for i := start / 2; i < 4; i++ { + p.c[i] = r.colour() + } + if r.bad { + return + } + p.cutUp(m) + previous = p + } +} + +// fillCoonsInside works out the four inner control points a Coons patch does +// not carry: its inside follows from its edges, and this is the combination +// that says how. The same shape serves all four, with the indices turned +// round, because a patch is symmetric in both directions. +func (p *patch) fillCoonsInside() { + p.p[1][1] = p.coonsInner([2]int{0, 0}, [2]int{0, 1}, [2]int{1, 0}, [2]int{0, 3}, [2]int{3, 0}, [2]int{3, 1}, [2]int{1, 3}, [2]int{3, 3}) + p.p[1][2] = p.coonsInner([2]int{0, 3}, [2]int{0, 2}, [2]int{1, 3}, [2]int{0, 0}, [2]int{3, 3}, [2]int{3, 2}, [2]int{1, 0}, [2]int{3, 0}) + p.p[2][1] = p.coonsInner([2]int{3, 0}, [2]int{3, 1}, [2]int{2, 0}, [2]int{3, 3}, [2]int{0, 0}, [2]int{0, 1}, [2]int{2, 3}, [2]int{0, 3}) + p.p[2][2] = p.coonsInner([2]int{3, 3}, [2]int{3, 2}, [2]int{2, 3}, [2]int{3, 0}, [2]int{0, 3}, [2]int{0, 2}, [2]int{2, 0}, [2]int{0, 0}) +} + +// coonsInner is the inner point nearest one corner: the corner itself, the two +// boundary points beside it, the two corners along from it, the two boundary +// points nearest those on the far edges, and the opposite corner. +func (p *patch) coonsInner(corner, a, b, along1, along2, far1, far2, opposite [2]int) point2 { + at := func(i [2]int) point2 { return p.p[i[0]][i[1]] } + mix := func(get func(point2) float64) float64 { + return (-4*get(at(corner)) + + 6*(get(at(a))+get(at(b))) - + 2*(get(at(along1))+get(at(along2))) + + 3*(get(at(far1))+get(at(far2))) - + get(at(opposite))) / 9 + } + return point2{ + x: mix(func(q point2) float64 { return q.x }), + y: mix(func(q point2) float64 { return q.y }), + } +} + +// cutUp turns the patch into triangles: a grid of small quadrilaterals, each +// of them two, with every corner coloured by where it sits. +func (p *patch) cutUp(m *mesh) { + var grid [patchSteps + 1][patchSteps + 1]vertex + for i := 0; i <= patchSteps; i++ { + u := float64(i) / patchSteps + for j := 0; j <= patchSteps; j++ { + v := float64(j) / patchSteps + at := p.surface(u, v) + grid[i][j] = vertex{x: at.x, y: at.y, c: p.colourAt(u, v)} + } + } + for i := 0; i < patchSteps; i++ { + for j := 0; j < patchSteps; j++ { + m.add(grid[i][j], grid[i+1][j], grid[i][j+1]) + m.add(grid[i+1][j], grid[i+1][j+1], grid[i][j+1]) + } + } +} + +// surface is where the point (u,v) of the patch lands: the control net read as +// a cubic Bézier surface. +func (p *patch) surface(u, v float64) point2 { + bu := bernstein(u) + bv := bernstein(v) + var out point2 + for i := 0; i < 4; i++ { + for j := 0; j < 4; j++ { + w := bu[i] * bv[j] + out.x += w * p.p[i][j].x + out.y += w * p.p[i][j].y + } + } + return out +} + +// bernstein is the four weights a cubic curve gives at a parameter. +func bernstein(t float64) [4]float64 { + s := 1 - t + return [4]float64{s * s * s, 3 * s * s * t, 3 * s * t * t, t * t * t} +} + +// colourAt mixes the four corner colours by where the point sits. The corners +// go round the patch rather than across it, so the two along one side are the +// first and the last. +func (p *patch) colourAt(u, v float64) color.RGBA { + mix := func(get func(color.RGBA) uint8) uint8 { + near := float64(get(p.c[0]))*(1-v) + float64(get(p.c[1]))*v + far := float64(get(p.c[3]))*(1-v) + float64(get(p.c[2]))*v + return byteOf((near*(1-u) + far*u) / 255) + } + return color.RGBA{ + R: mix(func(c color.RGBA) uint8 { return c.R }), + G: mix(func(c color.RGBA) uint8 { return c.G }), + B: mix(func(c color.RGBA) uint8 { return c.B }), + A: 255, + } +} diff --git a/pattern.go b/pattern.go index fd73df7..37aec45 100644 --- a/pattern.go +++ b/pattern.go @@ -98,6 +98,10 @@ func (r *renderer) paintShading(g *gstate, sh *shading, m geometry.Matrix, cov [ if !ok || alpha <= 0 { return } + var drawn *meshRaster + if sh.mesh != nil { + drawn = sh.mesh.rasterise(m, ox, oy, w, h) + } for y := 0; y < h; y++ { for x := 0; x < w; x++ { a := cov[y*w+x] * alpha @@ -108,7 +112,15 @@ func (r *renderer) paintShading(g *gstate, sh *shading, m geometry.Matrix, cov [ continue } p := inv.TransformPoint(geometry.Point{X: float64(ox+x) + 0.5, Y: float64(oy+y) + 0.5}) - c, ok := sh.at(p.X, p.Y) + var c color.RGBA + var ok bool + if drawn != nil { + if c, ok = drawn.at(ox+x, oy+y); !ok || !sh.insideBBox(p.X, p.Y) { + c, ok = sh.away() + } + } else { + c, ok = sh.at(p.X, p.Y) + } if !ok { continue } diff --git a/pattern_test.go b/pattern_test.go index 6ec4043..a4e3fda 100644 --- a/pattern_test.go +++ b/pattern_test.go @@ -1,6 +1,7 @@ package render import ( + "image/color" "testing" "github.com/go-gfx/gfx/geometry" @@ -279,3 +280,38 @@ func TestATilingPatternFilteredAsAnImage(t *testing.T) { t.Fatal(err) } } + +func TestAPatternInsideAFormIsPlacedInTheFormsSpace(t *testing.T) { + // A pattern is placed in the space the page is in, and a form's own + // matrix and the transform that drew it are both part of that space. A + // gradient used inside a form that has been moved must move with it: + // files written by plotting tools put every figure in a form and fill its + // panels with patterns, and a pattern that stayed at the page's origin + // would miss the shape it was asked to fill. + d := shadedPage(t, "q 1 0 0 1 50 50 cm /F1 Do Q", func(w *reader.Writer) reader.Dict { + shading := w.Add(reader.Dict{ + "ShadingType": reader.Integer(2), "ColorSpace": reader.Name("DeviceRGB"), + "Coords": nums(0, 0, 40, 0), + "Function": rampFunction(w), + "Extend": reader.Array{reader.Bool(true), reader.Bool(true)}, + }) + pattern := w.Add(reader.Dict{"PatternType": reader.Integer(2), "Shading": shading}) + form := w.Add(&reader.Stream{ + Dict: reader.Dict{ + "Type": reader.Name("XObject"), "Subtype": reader.Name("Form"), + "BBox": reader.Array{reader.Integer(0), reader.Integer(0), reader.Integer(40), reader.Integer(40)}, + "Resources": reader.Dict{"Pattern": reader.Dict{"P1": pattern}}, + }, + Raw: []byte("/Pattern cs /P1 scn 0 0 40 40 re f"), + }) + return reader.Dict{"XObject": reader.Dict{"F1": form}} + }) + img, err := Page(d, 1, Options{Scale: 1}) + if err != nil { + t.Fatal(err) + } + // The form sits from 50 to 90 across the page, and the gradient runs from + // red to blue over exactly that width. + wantColour(t, img, 52, 30, color.RGBA{R: 242, B: 12, A: 255}, 24) + wantColour(t, img, 87, 30, color.RGBA{R: 12, B: 242, A: 255}, 24) +} diff --git a/shading.go b/shading.go index 2fd1443..43bd688 100644 --- a/shading.go +++ b/shading.go @@ -27,6 +27,8 @@ type shading struct { // background is what is painted where the shading says nothing, when the // shading names one. background *color.RGBA + // mesh is set for the four kinds that carry their colours in a stream. + mesh *mesh } // readShading reads a shading dictionary — or the dictionary of a shading @@ -70,14 +72,17 @@ func (r *renderer) readShading(o reader.Object, resources reader.Dict) *shading c := sh.space.convert(bg) sh.background = &c } + if stream, isStream := reader.ToStream(resolved); isStream && sh.kind >= 4 && sh.kind <= 7 { + sh.mesh = r.readMesh(sh, stream) + } if !sh.usable() { return nil } return sh } -// usable reports whether the shading has what its kind needs to be drawn. The -// mesh kinds are not drawn yet and say so here rather than by drawing wrong. +// usable reports whether the shading has what its kind needs to be drawn. One +// that has not says so here rather than by drawing something wrong. func (s *shading) usable() bool { switch s.kind { case 1: @@ -86,6 +91,8 @@ func (s *shading) usable() bool { return s.fn != nil && len(s.coords) >= 4 && s.fn.outputs() == s.space.components case 3: return s.fn != nil && len(s.coords) >= 6 && s.fn.outputs() == s.space.components + case 4, 5, 6, 7: + return s.mesh != nil } return false } @@ -102,8 +109,7 @@ func matrixOf(v []float64) (geometry.Matrix, bool) { // at is the colour of the shading at a point of its own space, and false where // the shading covers nothing. func (s *shading) at(x, y float64) (color.RGBA, bool) { - if len(s.bbox) >= 4 && (x < math.Min(s.bbox[0], s.bbox[2]) || x > math.Max(s.bbox[0], s.bbox[2]) || - y < math.Min(s.bbox[1], s.bbox[3]) || y > math.Max(s.bbox[1], s.bbox[3])) { + if !s.insideBBox(x, y) { return s.away() } // Only the three kinds usable reports on ever get here. @@ -116,6 +122,16 @@ func (s *shading) at(x, y float64) (color.RGBA, bool) { return s.functionAt(x, y) } +// insideBBox reports whether a point of the shading's own space is within the +// box the shading says it keeps to, when it says. +func (s *shading) insideBBox(x, y float64) bool { + if len(s.bbox) < 4 { + return true + } + return x >= math.Min(s.bbox[0], s.bbox[2]) && x <= math.Max(s.bbox[0], s.bbox[2]) && + y >= math.Min(s.bbox[1], s.bbox[3]) && y <= math.Max(s.bbox[1], s.bbox[3]) +} + // away is what is drawn where the shading itself paints nothing. func (s *shading) away() (color.RGBA, bool) { if s.background != nil { diff --git a/state.go b/state.go index dab28c6..e8bc20f 100644 --- a/state.go +++ b/state.go @@ -69,8 +69,9 @@ type renderer struct { // which is what the specification says. tm, tlm geometry.Matrix - // base is the transform the page started in, which is the space a - // pattern is placed in however the transform has changed since. + // base is the space a pattern is placed in, however the transform has + // changed since: the transform the page started in, or — inside a form — + // the one the form's content started in. base geometry.Matrix // fonts are the ones already read, by the object they were read from. diff --git a/xobject.go b/xobject.go index 497f698..c50e21e 100644 --- a/xobject.go +++ b/xobject.go @@ -62,9 +62,16 @@ func (r *renderer) drawForm(g *gstate, stream *reader.Stream, parent reader.Dict if !ok { resources = parent } + // A pattern named inside a form is placed in the form's own space, not + // the page's: the form's matrix and the transform that drew it both count. + // Without this a pattern used in a figure lands wherever the page's origin + // happens to be, which is nearly always off the shape it was meant to fill. + was := r.base + r.base = inner.ctm r.depth++ r.run(content, resources, inner) r.depth-- + r.base = was } // clipToBox narrows the clip to a rectangle in the current user space.