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  }