github.com/andybalholm/giopdf@v0.0.0-20220317170119-aad9a095ad48/canvas.go (about) 1 package giopdf 2 3 import ( 4 "image" 5 "image/color" 6 7 "gioui.org/f32" 8 "gioui.org/op" 9 "gioui.org/op/clip" 10 "gioui.org/op/paint" 11 "github.com/andybalholm/stroke" 12 ) 13 14 // A Canvas implements the PDF imaging model, drawing to a Gio operations list. 15 // Most of its methods correspond directly to PDF page description operators. 16 type Canvas struct { 17 PathBuilder 18 graphicsState 19 20 stateStack []graphicsState 21 setClippingPath bool 22 23 ops *op.Ops 24 } 25 26 func NewCanvas(ops *op.Ops) *Canvas { 27 return &Canvas{ 28 ops: ops, 29 graphicsState: graphicsState{ 30 fillColor: color.NRGBA{0, 0, 0, 255}, 31 strokeColor: color.NRGBA{0, 0, 0, 255}, 32 lineWidth: 1, 33 miterLimit: 10, 34 hScale: 100, 35 }, 36 } 37 } 38 39 func (c *Canvas) fill() { 40 ps := toPathSpec(c.ops, c.Path, true) 41 paint.FillShape(c.ops, c.fillColor, clip.Outline{ps}.Op()) 42 } 43 44 func (c *Canvas) stroke() { 45 var p [][]stroke.Segment 46 var contour []stroke.Segment 47 var pos, lastMove f32.Point 48 for _, e := range c.Path { 49 switch e.Op { 50 case 'm': 51 lastMove = e.End 52 pos = e.End 53 if len(contour) > 0 { 54 p = append(p, contour) 55 contour = nil 56 } 57 case 'l': 58 contour = append(contour, stroke.LinearSegment(stroke.Point(pos), stroke.Point(e.End))) 59 pos = e.End 60 case 'c': 61 contour = append(contour, stroke.Segment{stroke.Point(pos), stroke.Point(e.CP1), stroke.Point(e.CP2), stroke.Point(e.End)}) 62 pos = e.End 63 case 'h': 64 if pos != lastMove { 65 contour = append(contour, stroke.LinearSegment(stroke.Point(pos), stroke.Point(lastMove))) 66 pos = lastMove 67 } 68 } 69 } 70 if len(contour) > 0 { 71 p = append(p, contour) 72 contour = nil 73 } 74 75 if len(c.dashes) > 0 { 76 p = stroke.Dash(p, c.dashes, c.dashPhase) 77 } 78 79 outline := stroke.Stroke(p, stroke.Options{ 80 Width: c.lineWidth, 81 Cap: stroke.CapStyle(c.lineCap), 82 Join: stroke.JoinStyle(c.lineJoin), 83 MiterLimit: c.miterLimit, 84 }) 85 86 ps := segmentsToPathSpec(c.ops, outline) 87 paint.FillShape(c.ops, c.strokeColor, clip.Outline{ps}.Op()) 88 } 89 90 func segmentsToPathSpec(ops *op.Ops, outline [][]stroke.Segment) clip.PathSpec { 91 var path clip.Path 92 path.Begin(ops) 93 94 for _, contour := range outline { 95 path.MoveTo(f32.Point(contour[0].Start)) 96 for i, s := range contour { 97 if i > 0 && s.Start != contour[i-1].End { 98 path.LineTo(f32.Point(s.Start)) 99 } 100 path.CubeTo(f32.Point(s.CP1), f32.Point(s.CP2), f32.Point(s.End)) 101 } 102 } 103 return path.End() 104 } 105 106 func (c *Canvas) finishPath() { 107 if c.setClippingPath { 108 ps := toPathSpec(c.ops, c.Path, true) 109 cs := clip.Outline{ps}.Op().Push(c.ops) 110 c.clippingPaths = append(c.clippingPaths, cs) 111 } 112 c.setClippingPath = false 113 c.Path = c.Path[:0] 114 } 115 116 // Fill fills the current path. 117 func (c *Canvas) Fill() { 118 c.fill() 119 c.finishPath() 120 } 121 122 // Stroke strokes (outlines) the current path. 123 func (c *Canvas) Stroke() { 124 c.stroke() 125 c.finishPath() 126 } 127 128 // CloseAndStroke closes the current path before stroking it it. 129 func (c *Canvas) CloseAndStroke() { 130 c.ClosePath() 131 c.stroke() 132 c.finishPath() 133 } 134 135 // FillAndStroke fills the current path and then strokes (outlines) it. 136 func (c *Canvas) FillAndStroke() { 137 c.fill() 138 c.stroke() 139 c.finishPath() 140 } 141 142 // NoOpPaint finishes the current path without filling or stroking it. 143 // It is normally used to apply a clipping path after calling Clip. 144 func (c *Canvas) NoOpPaint() { 145 c.finishPath() 146 } 147 148 // Clip causes the current path to be added to the clipping path after it is 149 // painted. 150 func (c *Canvas) Clip() { 151 c.setClippingPath = true 152 } 153 154 // CloseFillAndStroke closes the current path before filling and stroking it. 155 func (c *Canvas) CloseFillAndStroke() { 156 c.ClosePath() 157 c.fill() 158 c.stroke() 159 c.finishPath() 160 } 161 162 // Save pushes a copy of the current graphics state onto the state stack. 163 func (c *Canvas) Save() { 164 c.stateStack = append(c.stateStack, c.graphicsState) 165 c.transforms = nil 166 c.clippingPaths = nil 167 } 168 169 // Restore restores the graphics state, popping it off the stack. 170 func (c *Canvas) Restore() { 171 // First pop off the TransformStack and clip.Stack entries that were saved since the last Save call. 172 for i := len(c.transforms) - 1; i >= 0; i-- { 173 c.transforms[i].Pop() 174 } 175 for i := len(c.clippingPaths) - 1; i >= 0; i-- { 176 c.clippingPaths[i].Pop() 177 } 178 179 n := len(c.stateStack) - 1 180 c.graphicsState = c.stateStack[n] 181 c.stateStack = c.stateStack[:n] 182 } 183 184 // Transform changes the coordinate system according to the transformation 185 // matrix specified. 186 func (ca *Canvas) Transform(a, b, c, d, e, f float32) { 187 m := f32.NewAffine2D(a, c, e, b, d, f) 188 s := op.Affine(m).Push(ca.ops) 189 ca.transforms = append(ca.transforms, s) 190 } 191 192 // Image draws an image. The image is placed in the unit square of the user 193 // coordinate system. 194 func (c *Canvas) Image(img image.Image) { 195 io := paint.NewImageOp(img) 196 size := io.Size() 197 c.Save() 198 c.Transform(1/float32(size.X), 0, 0, -1/float32(size.Y), 0, 1) 199 io.Add(c.ops) 200 paint.PaintOp{}.Add(c.ops) 201 c.Restore() 202 }