From 729edb193d393bff89db9476cac22ac76005e03e Mon Sep 17 00:00:00 2001 From: David Delavennat Date: Wed, 26 Aug 2026 01:30:34 +0200 Subject: [PATCH] Draw the mesh shadings, and place a pattern in the form that names it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four kinds of shading that carry their colours in a stream rather than in a function: free-form and lattice-form triangles, each corner its own colour, and Coons and tensor patches, whose four sides are cubic curves. Nothing can fill a curved-sided patch directly, so a patch is cut into a grid of little quadrilaterals — sixteen a side, which is past the point where a step shows — and every triangle is filled by mixing the colours at its corners across it. The inside of a Coons patch follows from its twelve boundary points and is worked out here; a tensor patch says four more. 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 point. The corpus was asked first: 184 of its pages carry a mesh — 114 of the fourth kind, 17 of the fifth, 53 of the seventh, and none of the sixth, which is implemented all the same because it is the tensor patch with its inside left out. Putting those pages beside macOS's own renderer found a second thing, and a larger one: a pattern is placed in the space of the content that names it, and inside a form XObject that is the form's matrix and the transform that drew it, not the page's origin. Every pgfplots surface in the corpus was landing in the bottom-left corner of the page instead of on the panel it was meant to fill. That is fixed here, with a test that fails without it, and it puts every shading and tiling pattern used inside a figure where it belongs — not only the mesh ones. A mesh whose function gives back a different number of components than its colour space takes is now refused rather than converted through a space that cannot take it. --- README.md | 18 +- mesh.go | 346 ++++++++++++++++++++++++++++++++ mesh_test.go | 524 ++++++++++++++++++++++++++++++++++++++++++++++++ patch.go | 184 +++++++++++++++++ pattern.go | 14 +- pattern_test.go | 36 ++++ shading.go | 24 ++- state.go | 5 +- xobject.go | 7 + 9 files changed, 1148 insertions(+), 10 deletions(-) create mode 100644 mesh.go create mode 100644 mesh_test.go create mode 100644 patch.go 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.