github.com/unidoc/unidoc@v2.2.0+incompatible/pdf/contentstream/draw/shapes.go (about)

     1  package draw
     2  
     3  import (
     4  	"math"
     5  
     6  	pdfcontent "github.com/unidoc/unidoc/pdf/contentstream"
     7  	pdfcore "github.com/unidoc/unidoc/pdf/core"
     8  	pdf "github.com/unidoc/unidoc/pdf/model"
     9  )
    10  
    11  type Circle struct {
    12  	X             float64
    13  	Y             float64
    14  	Width         float64
    15  	Height        float64
    16  	FillEnabled   bool // Show fill?
    17  	FillColor     *pdf.PdfColorDeviceRGB
    18  	BorderEnabled bool // Show border?
    19  	BorderWidth   float64
    20  	BorderColor   *pdf.PdfColorDeviceRGB
    21  	Opacity       float64 // Alpha value (0-1).
    22  }
    23  
    24  // Draw a circle. Can specify a graphics state (gsName) for setting opacity etc.  Otherwise leave empty ("").
    25  // Returns the content stream as a byte array, the bounding box and an error on failure.
    26  func (c Circle) Draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
    27  	xRad := c.Width / 2
    28  	yRad := c.Height / 2
    29  	if c.BorderEnabled {
    30  		xRad -= c.BorderWidth / 2
    31  		yRad -= c.BorderWidth / 2
    32  	}
    33  
    34  	magic := 0.551784
    35  	xMagic := xRad * magic
    36  	yMagic := yRad * magic
    37  
    38  	bpath := NewCubicBezierPath()
    39  	bpath = bpath.AppendCurve(NewCubicBezierCurve(-xRad, 0, -xRad, yMagic, -xMagic, yRad, 0, yRad))
    40  	bpath = bpath.AppendCurve(NewCubicBezierCurve(0, yRad, xMagic, yRad, xRad, yMagic, xRad, 0))
    41  	bpath = bpath.AppendCurve(NewCubicBezierCurve(xRad, 0, xRad, -yMagic, xMagic, -yRad, 0, -yRad))
    42  	bpath = bpath.AppendCurve(NewCubicBezierCurve(0, -yRad, -xMagic, -yRad, -xRad, -yMagic, -xRad, 0))
    43  	bpath = bpath.Offset(xRad, yRad)
    44  	if c.BorderEnabled {
    45  		bpath = bpath.Offset(c.BorderWidth/2, c.BorderWidth/2)
    46  	}
    47  	if c.X != 0 || c.Y != 0 {
    48  		bpath = bpath.Offset(c.X, c.Y)
    49  	}
    50  
    51  	creator := pdfcontent.NewContentCreator()
    52  
    53  	creator.Add_q()
    54  
    55  	if c.FillEnabled {
    56  		creator.Add_rg(c.FillColor.R(), c.FillColor.G(), c.FillColor.B())
    57  	}
    58  	if c.BorderEnabled {
    59  		creator.Add_RG(c.BorderColor.R(), c.BorderColor.G(), c.BorderColor.B())
    60  		creator.Add_w(c.BorderWidth)
    61  	}
    62  	if len(gsName) > 1 {
    63  		// If a graphics state is provided, use it. (Used for transparency settings here).
    64  		creator.Add_gs(pdfcore.PdfObjectName(gsName))
    65  	}
    66  
    67  	DrawBezierPathWithCreator(bpath, creator)
    68  	creator.Add_h() // Close the path.
    69  
    70  	if c.FillEnabled && c.BorderEnabled {
    71  		creator.Add_B() // fill and stroke.
    72  	} else if c.FillEnabled {
    73  		creator.Add_f() // Fill.
    74  	} else if c.BorderEnabled {
    75  		creator.Add_S() // Stroke.
    76  	}
    77  	creator.Add_Q()
    78  
    79  	// Get bounding box.
    80  	pathBbox := bpath.GetBoundingBox()
    81  	if c.BorderEnabled {
    82  		// Account for stroke width.
    83  		pathBbox.Height += c.BorderWidth
    84  		pathBbox.Width += c.BorderWidth
    85  		pathBbox.X -= c.BorderWidth / 2
    86  		pathBbox.Y -= c.BorderWidth / 2
    87  	}
    88  
    89  	// Bounding box - global coordinate system.
    90  	bbox := &pdf.PdfRectangle{}
    91  	bbox.Llx = pathBbox.X
    92  	bbox.Lly = pathBbox.Y
    93  	bbox.Urx = pathBbox.X + pathBbox.Width
    94  	bbox.Ury = pathBbox.Y + pathBbox.Height
    95  
    96  	return creator.Bytes(), bbox, nil
    97  }
    98  
    99  // A rectangle defined with a specified Width and Height and a lower left corner at (X,Y).  The rectangle can
   100  // optionally have a border and a filling color.
   101  // The Width/Height includes the border (if any specified), i.e. is positioned inside.
   102  type Rectangle struct {
   103  	X             float64
   104  	Y             float64
   105  	Width         float64
   106  	Height        float64
   107  	FillEnabled   bool // Show fill?
   108  	FillColor     *pdf.PdfColorDeviceRGB
   109  	BorderEnabled bool // Show border?
   110  	BorderWidth   float64
   111  	BorderColor   *pdf.PdfColorDeviceRGB
   112  	Opacity       float64 // Alpha value (0-1).
   113  }
   114  
   115  // Draw the circle. Can specify a graphics state (gsName) for setting opacity etc.  Otherwise leave empty ("").
   116  // Returns the content stream as a byte array, bounding box and an error on failure.
   117  func (rect Rectangle) Draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
   118  	path := NewPath()
   119  
   120  	path = path.AppendPoint(NewPoint(0, 0))
   121  	path = path.AppendPoint(NewPoint(0, rect.Height))
   122  	path = path.AppendPoint(NewPoint(rect.Width, rect.Height))
   123  	path = path.AppendPoint(NewPoint(rect.Width, 0))
   124  	path = path.AppendPoint(NewPoint(0, 0))
   125  
   126  	if rect.X != 0 || rect.Y != 0 {
   127  		path = path.Offset(rect.X, rect.Y)
   128  	}
   129  
   130  	creator := pdfcontent.NewContentCreator()
   131  
   132  	creator.Add_q()
   133  	if rect.FillEnabled {
   134  		creator.Add_rg(rect.FillColor.R(), rect.FillColor.G(), rect.FillColor.B())
   135  	}
   136  	if rect.BorderEnabled {
   137  		creator.Add_RG(rect.BorderColor.R(), rect.BorderColor.G(), rect.BorderColor.B())
   138  		creator.Add_w(rect.BorderWidth)
   139  	}
   140  	if len(gsName) > 1 {
   141  		// If a graphics state is provided, use it. (Used for transparency settings here).
   142  		creator.Add_gs(pdfcore.PdfObjectName(gsName))
   143  	}
   144  	DrawPathWithCreator(path, creator)
   145  	creator.Add_h() // Close the path.
   146  
   147  	if rect.FillEnabled && rect.BorderEnabled {
   148  		creator.Add_B() // fill and stroke.
   149  	} else if rect.FillEnabled {
   150  		creator.Add_f() // Fill.
   151  	} else if rect.BorderEnabled {
   152  		creator.Add_S() // Stroke.
   153  	}
   154  	creator.Add_Q()
   155  
   156  	// Get bounding box.
   157  	pathBbox := path.GetBoundingBox()
   158  
   159  	// Bounding box - global coordinate system.
   160  	bbox := &pdf.PdfRectangle{}
   161  	bbox.Llx = pathBbox.X
   162  	bbox.Lly = pathBbox.Y
   163  	bbox.Urx = pathBbox.X + pathBbox.Width
   164  	bbox.Ury = pathBbox.Y + pathBbox.Height
   165  
   166  	return creator.Bytes(), bbox, nil
   167  }
   168  
   169  // The currently supported line ending styles are None, Arrow (ClosedArrow) and Butt.
   170  type LineEndingStyle int
   171  
   172  const (
   173  	LineEndingStyleNone  LineEndingStyle = 0
   174  	LineEndingStyleArrow LineEndingStyle = 1
   175  	LineEndingStyleButt  LineEndingStyle = 2
   176  )
   177  
   178  // Defines a line between point 1 (X1,Y1) and point 2 (X2,Y2).  The line ending styles can be none (regular line),
   179  // or arrows at either end.  The line also has a specified width, color and opacity.
   180  type Line struct {
   181  	X1               float64
   182  	Y1               float64
   183  	X2               float64
   184  	Y2               float64
   185  	LineColor        *pdf.PdfColorDeviceRGB
   186  	Opacity          float64 // Alpha value (0-1).
   187  	LineWidth        float64
   188  	LineEndingStyle1 LineEndingStyle // Line ending style of point 1.
   189  	LineEndingStyle2 LineEndingStyle // Line ending style of point 2.
   190  }
   191  
   192  // Draw a line in PDF.  Generates the content stream which can be used in page contents or appearance stream of annotation.
   193  // Returns the stream content, XForm bounding box (local), bounding box and an error if one occurred.
   194  func (line Line) Draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
   195  	x1, x2 := line.X1, line.X2
   196  	y1, y2 := line.Y1, line.Y2
   197  
   198  	dy := y2 - y1
   199  	dx := x2 - x1
   200  	theta := math.Atan2(dy, dx)
   201  
   202  	L := math.Sqrt(math.Pow(dx, 2.0) + math.Pow(dy, 2.0))
   203  	w := line.LineWidth
   204  
   205  	pi := math.Pi
   206  
   207  	mul := 1.0
   208  	if dx < 0 {
   209  		mul *= -1.0
   210  	}
   211  	if dy < 0 {
   212  		mul *= -1.0
   213  	}
   214  
   215  	// Vs.
   216  	VsX := mul * (-w / 2 * math.Cos(theta+pi/2))
   217  	VsY := mul * (-w/2*math.Sin(theta+pi/2) + w*math.Sin(theta+pi/2))
   218  
   219  	// V1.
   220  	V1X := VsX + w/2*math.Cos(theta+pi/2)
   221  	V1Y := VsY + w/2*math.Sin(theta+pi/2)
   222  
   223  	// P2.
   224  	V2X := VsX + w/2*math.Cos(theta+pi/2) + L*math.Cos(theta)
   225  	V2Y := VsY + w/2*math.Sin(theta+pi/2) + L*math.Sin(theta)
   226  
   227  	// P3.
   228  	V3X := VsX + w/2*math.Cos(theta+pi/2) + L*math.Cos(theta) + w*math.Cos(theta-pi/2)
   229  	V3Y := VsY + w/2*math.Sin(theta+pi/2) + L*math.Sin(theta) + w*math.Sin(theta-pi/2)
   230  
   231  	// P4.
   232  	V4X := VsX + w/2*math.Cos(theta-pi/2)
   233  	V4Y := VsY + w/2*math.Sin(theta-pi/2)
   234  
   235  	path := NewPath()
   236  	path = path.AppendPoint(NewPoint(V1X, V1Y))
   237  	path = path.AppendPoint(NewPoint(V2X, V2Y))
   238  	path = path.AppendPoint(NewPoint(V3X, V3Y))
   239  	path = path.AppendPoint(NewPoint(V4X, V4Y))
   240  
   241  	lineEnding1 := line.LineEndingStyle1
   242  	lineEnding2 := line.LineEndingStyle2
   243  
   244  	// TODO: Allow custom height/widths.
   245  	arrowHeight := 3 * w
   246  	arrowWidth := 3 * w
   247  	arrowExtruding := (arrowWidth - w) / 2
   248  
   249  	if lineEnding2 == LineEndingStyleArrow {
   250  		// Convert P2, P3
   251  		p2 := path.GetPointNumber(2)
   252  
   253  		va1 := NewVectorPolar(arrowHeight, theta+pi)
   254  		pa1 := p2.AddVector(va1)
   255  
   256  		bVec := NewVectorPolar(arrowWidth/2, theta+pi/2)
   257  		aVec := NewVectorPolar(arrowHeight, theta)
   258  
   259  		va2 := NewVectorPolar(arrowExtruding, theta+pi/2)
   260  		pa2 := pa1.AddVector(va2)
   261  
   262  		va3 := aVec.Add(bVec.Flip())
   263  		pa3 := pa2.AddVector(va3)
   264  
   265  		va4 := bVec.Scale(2).Flip().Add(va3.Flip())
   266  		pa4 := pa3.AddVector(va4)
   267  
   268  		pa5 := pa1.AddVector(NewVectorPolar(w, theta-pi/2))
   269  
   270  		newpath := NewPath()
   271  		newpath = newpath.AppendPoint(path.GetPointNumber(1))
   272  		newpath = newpath.AppendPoint(pa1)
   273  		newpath = newpath.AppendPoint(pa2)
   274  		newpath = newpath.AppendPoint(pa3)
   275  		newpath = newpath.AppendPoint(pa4)
   276  		newpath = newpath.AppendPoint(pa5)
   277  		newpath = newpath.AppendPoint(path.GetPointNumber(4))
   278  
   279  		path = newpath
   280  	}
   281  	if lineEnding1 == LineEndingStyleArrow {
   282  		// Get the first and last points.
   283  		p1 := path.GetPointNumber(1)
   284  		pn := path.GetPointNumber(path.Length())
   285  
   286  		// First three points on arrow.
   287  		v1 := NewVectorPolar(w/2, theta+pi+pi/2)
   288  		pa1 := p1.AddVector(v1)
   289  
   290  		v2 := NewVectorPolar(arrowHeight, theta).Add(NewVectorPolar(arrowWidth/2, theta+pi/2))
   291  		pa2 := pa1.AddVector(v2)
   292  
   293  		v3 := NewVectorPolar(arrowExtruding, theta-pi/2)
   294  		pa3 := pa2.AddVector(v3)
   295  
   296  		// Last three points
   297  		v5 := NewVectorPolar(arrowHeight, theta)
   298  		pa5 := pn.AddVector(v5)
   299  
   300  		v6 := NewVectorPolar(arrowExtruding, theta+pi+pi/2)
   301  		pa6 := pa5.AddVector(v6)
   302  
   303  		pa7 := pa1
   304  
   305  		newpath := NewPath()
   306  		newpath = newpath.AppendPoint(pa1)
   307  		newpath = newpath.AppendPoint(pa2)
   308  		newpath = newpath.AppendPoint(pa3)
   309  		for _, p := range path.Points[1 : len(path.Points)-1] {
   310  			newpath = newpath.AppendPoint(p)
   311  		}
   312  		newpath = newpath.AppendPoint(pa5)
   313  		newpath = newpath.AppendPoint(pa6)
   314  		newpath = newpath.AppendPoint(pa7)
   315  
   316  		path = newpath
   317  	}
   318  
   319  	creator := pdfcontent.NewContentCreator()
   320  
   321  	// Draw line with arrow
   322  	creator.
   323  		Add_q().
   324  		Add_rg(line.LineColor.R(), line.LineColor.G(), line.LineColor.B())
   325  	if len(gsName) > 1 {
   326  		// If a graphics state is provided, use it. (Used for transparency settings here).
   327  		creator.Add_gs(pdfcore.PdfObjectName(gsName))
   328  	}
   329  
   330  	path = path.Offset(line.X1, line.Y1)
   331  
   332  	pathBbox := path.GetBoundingBox()
   333  
   334  	DrawPathWithCreator(path, creator)
   335  	creator.Add_f().
   336  		//creator.Add_S().
   337  		Add_Q()
   338  
   339  	/*
   340  		// Offsets (needed for placement of annotations bbox).
   341  		offX := x1 - VsX
   342  		offY := y1 - VsY
   343  	*/
   344  
   345  	// Bounding box - global coordinate system.
   346  	bbox := &pdf.PdfRectangle{}
   347  	bbox.Llx = pathBbox.X
   348  	bbox.Lly = pathBbox.Y
   349  	bbox.Urx = pathBbox.X + pathBbox.Width
   350  	bbox.Ury = pathBbox.Y + pathBbox.Height
   351  
   352  	return creator.Bytes(), bbox, nil
   353  }