mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-27 13:48:51 +08:00
417 lines
12 KiB
Go
417 lines
12 KiB
Go
package draw
|
|
|
|
import (
|
|
"math"
|
|
|
|
pdfcontent "github.com/unidoc/unipdf/v3/contentstream"
|
|
pdfcore "github.com/unidoc/unipdf/v3/core"
|
|
pdf "github.com/unidoc/unipdf/v3/model"
|
|
)
|
|
|
|
// Circle represents a circle shape with fill and border properties that can be drawn to a PDF content stream.
|
|
type Circle struct {
|
|
X float64
|
|
Y float64
|
|
Width float64
|
|
Height float64
|
|
FillEnabled bool // Show fill?
|
|
FillColor *pdf.PdfColorDeviceRGB
|
|
BorderEnabled bool // Show border?
|
|
BorderWidth float64
|
|
BorderColor *pdf.PdfColorDeviceRGB
|
|
Opacity float64 // Alpha value (0-1).
|
|
}
|
|
|
|
// Draw draws the circle. Can specify a graphics state (gsName) for setting opacity etc. Otherwise leave empty ("").
|
|
// Returns the content stream as a byte array, the bounding box and an error on failure.
|
|
func (c Circle) Draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
|
|
xRad := c.Width / 2
|
|
yRad := c.Height / 2
|
|
if c.BorderEnabled {
|
|
xRad -= c.BorderWidth / 2
|
|
yRad -= c.BorderWidth / 2
|
|
}
|
|
|
|
magic := 0.551784
|
|
xMagic := xRad * magic
|
|
yMagic := yRad * magic
|
|
|
|
bpath := NewCubicBezierPath()
|
|
bpath = bpath.AppendCurve(NewCubicBezierCurve(-xRad, 0, -xRad, yMagic, -xMagic, yRad, 0, yRad))
|
|
bpath = bpath.AppendCurve(NewCubicBezierCurve(0, yRad, xMagic, yRad, xRad, yMagic, xRad, 0))
|
|
bpath = bpath.AppendCurve(NewCubicBezierCurve(xRad, 0, xRad, -yMagic, xMagic, -yRad, 0, -yRad))
|
|
bpath = bpath.AppendCurve(NewCubicBezierCurve(0, -yRad, -xMagic, -yRad, -xRad, -yMagic, -xRad, 0))
|
|
bpath = bpath.Offset(xRad, yRad)
|
|
if c.BorderEnabled {
|
|
bpath = bpath.Offset(c.BorderWidth/2, c.BorderWidth/2)
|
|
}
|
|
if c.X != 0 || c.Y != 0 {
|
|
bpath = bpath.Offset(c.X, c.Y)
|
|
}
|
|
|
|
creator := pdfcontent.NewContentCreator()
|
|
|
|
creator.Add_q()
|
|
|
|
if c.FillEnabled {
|
|
creator.Add_rg(c.FillColor.R(), c.FillColor.G(), c.FillColor.B())
|
|
}
|
|
if c.BorderEnabled {
|
|
creator.Add_RG(c.BorderColor.R(), c.BorderColor.G(), c.BorderColor.B())
|
|
creator.Add_w(c.BorderWidth)
|
|
}
|
|
if len(gsName) > 1 {
|
|
// If a graphics state is provided, use it. (Used for transparency settings here).
|
|
creator.Add_gs(pdfcore.PdfObjectName(gsName))
|
|
}
|
|
|
|
DrawBezierPathWithCreator(bpath, creator)
|
|
creator.Add_h() // Close the path.
|
|
|
|
if c.FillEnabled && c.BorderEnabled {
|
|
creator.Add_B() // fill and stroke.
|
|
} else if c.FillEnabled {
|
|
creator.Add_f() // Fill.
|
|
} else if c.BorderEnabled {
|
|
creator.Add_S() // Stroke.
|
|
}
|
|
creator.Add_Q()
|
|
|
|
// Get bounding box.
|
|
pathBbox := bpath.GetBoundingBox()
|
|
if c.BorderEnabled {
|
|
// Account for stroke width.
|
|
pathBbox.Height += c.BorderWidth
|
|
pathBbox.Width += c.BorderWidth
|
|
pathBbox.X -= c.BorderWidth / 2
|
|
pathBbox.Y -= c.BorderWidth / 2
|
|
}
|
|
|
|
// Bounding box - global coordinate system.
|
|
bbox := &pdf.PdfRectangle{}
|
|
bbox.Llx = pathBbox.X
|
|
bbox.Lly = pathBbox.Y
|
|
bbox.Urx = pathBbox.X + pathBbox.Width
|
|
bbox.Ury = pathBbox.Y + pathBbox.Height
|
|
|
|
return creator.Bytes(), bbox, nil
|
|
}
|
|
|
|
// Rectangle is a shape with a specified Width and Height and a lower left corner at (X,Y) that can be
|
|
// drawn to a PDF content stream. The rectangle can optionally have a border and a filling color.
|
|
// The Width/Height includes the border (if any specified), i.e. is positioned inside.
|
|
type Rectangle struct {
|
|
X float64
|
|
Y float64
|
|
Width float64
|
|
Height float64
|
|
FillEnabled bool // Show fill?
|
|
FillColor *pdf.PdfColorDeviceRGB
|
|
BorderEnabled bool // Show border?
|
|
BorderWidth float64
|
|
BorderColor *pdf.PdfColorDeviceRGB
|
|
Opacity float64 // Alpha value (0-1).
|
|
}
|
|
|
|
// Draw draws the rectangle. Can specify a graphics state (gsName) for setting opacity etc.
|
|
// Otherwise leave empty (""). Returns the content stream as a byte array, bounding box and an error on failure.
|
|
func (rect Rectangle) Draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
|
|
path := NewPath()
|
|
|
|
path = path.AppendPoint(NewPoint(0, 0))
|
|
path = path.AppendPoint(NewPoint(0, rect.Height))
|
|
path = path.AppendPoint(NewPoint(rect.Width, rect.Height))
|
|
path = path.AppendPoint(NewPoint(rect.Width, 0))
|
|
path = path.AppendPoint(NewPoint(0, 0))
|
|
|
|
if rect.X != 0 || rect.Y != 0 {
|
|
path = path.Offset(rect.X, rect.Y)
|
|
}
|
|
|
|
creator := pdfcontent.NewContentCreator()
|
|
|
|
creator.Add_q()
|
|
if rect.FillEnabled {
|
|
creator.Add_rg(rect.FillColor.R(), rect.FillColor.G(), rect.FillColor.B())
|
|
}
|
|
if rect.BorderEnabled {
|
|
creator.Add_RG(rect.BorderColor.R(), rect.BorderColor.G(), rect.BorderColor.B())
|
|
creator.Add_w(rect.BorderWidth)
|
|
}
|
|
if len(gsName) > 1 {
|
|
// If a graphics state is provided, use it. (Used for transparency settings here).
|
|
creator.Add_gs(pdfcore.PdfObjectName(gsName))
|
|
}
|
|
DrawPathWithCreator(path, creator)
|
|
creator.Add_h() // Close the path.
|
|
|
|
if rect.FillEnabled && rect.BorderEnabled {
|
|
creator.Add_B() // fill and stroke.
|
|
} else if rect.FillEnabled {
|
|
creator.Add_f() // Fill.
|
|
} else if rect.BorderEnabled {
|
|
creator.Add_S() // Stroke.
|
|
}
|
|
creator.Add_Q()
|
|
|
|
// Get bounding box.
|
|
pathBbox := path.GetBoundingBox()
|
|
|
|
// Bounding box - global coordinate system.
|
|
bbox := &pdf.PdfRectangle{}
|
|
bbox.Llx = pathBbox.X
|
|
bbox.Lly = pathBbox.Y
|
|
bbox.Urx = pathBbox.X + pathBbox.Width
|
|
bbox.Ury = pathBbox.Y + pathBbox.Height
|
|
|
|
return creator.Bytes(), bbox, nil
|
|
}
|
|
|
|
// LineEndingStyle defines the line ending style for lines.
|
|
// The currently supported line ending styles are None, Arrow (ClosedArrow) and Butt.
|
|
type LineEndingStyle int
|
|
|
|
// Line ending styles.
|
|
const (
|
|
LineEndingStyleNone LineEndingStyle = 0
|
|
LineEndingStyleArrow LineEndingStyle = 1
|
|
LineEndingStyleButt LineEndingStyle = 2
|
|
)
|
|
|
|
// LineStyle refers to how the line will be created.
|
|
type LineStyle int
|
|
|
|
// Line styles.
|
|
const (
|
|
LineStyleSolid LineStyle = 0
|
|
LineStyleDashed LineStyle = 1
|
|
)
|
|
|
|
// Line defines a line shape between point 1 (X1,Y1) and point 2 (X2,Y2). The line ending styles can be none (regular line),
|
|
// or arrows at either end. The line also has a specified width, color and opacity.
|
|
type Line struct {
|
|
X1 float64
|
|
Y1 float64
|
|
X2 float64
|
|
Y2 float64
|
|
LineColor *pdf.PdfColorDeviceRGB
|
|
Opacity float64 // Alpha value (0-1).
|
|
LineWidth float64
|
|
LineEndingStyle1 LineEndingStyle // Line ending style of point 1.
|
|
LineEndingStyle2 LineEndingStyle // Line ending style of point 2.
|
|
LineStyle LineStyle
|
|
}
|
|
|
|
// Draw draws the line to PDF contentstream. Generates the content stream which can be used in page contents or
|
|
// appearance stream of annotation. Returns the stream content, XForm bounding box (local), bounding box and an error
|
|
// if one occurred.
|
|
func (line Line) Draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
|
|
x1, x2 := line.X1, line.X2
|
|
y1, y2 := line.Y1, line.Y2
|
|
|
|
dy := y2 - y1
|
|
dx := x2 - x1
|
|
theta := math.Atan2(dy, dx)
|
|
|
|
L := math.Sqrt(math.Pow(dx, 2.0) + math.Pow(dy, 2.0))
|
|
w := line.LineWidth
|
|
|
|
pi := math.Pi
|
|
|
|
mul := 1.0
|
|
if dx < 0 {
|
|
mul *= -1.0
|
|
}
|
|
if dy < 0 {
|
|
mul *= -1.0
|
|
}
|
|
|
|
// Vs.
|
|
VsX := mul * (-w / 2 * math.Cos(theta+pi/2))
|
|
VsY := mul * (-w/2*math.Sin(theta+pi/2) + w*math.Sin(theta+pi/2))
|
|
|
|
// V1.
|
|
V1X := VsX + w/2*math.Cos(theta+pi/2)
|
|
V1Y := VsY + w/2*math.Sin(theta+pi/2)
|
|
|
|
// P2.
|
|
V2X := VsX + w/2*math.Cos(theta+pi/2) + L*math.Cos(theta)
|
|
V2Y := VsY + w/2*math.Sin(theta+pi/2) + L*math.Sin(theta)
|
|
|
|
// P3.
|
|
V3X := VsX + w/2*math.Cos(theta+pi/2) + L*math.Cos(theta) + w*math.Cos(theta-pi/2)
|
|
V3Y := VsY + w/2*math.Sin(theta+pi/2) + L*math.Sin(theta) + w*math.Sin(theta-pi/2)
|
|
|
|
// P4.
|
|
V4X := VsX + w/2*math.Cos(theta-pi/2)
|
|
V4Y := VsY + w/2*math.Sin(theta-pi/2)
|
|
|
|
path := NewPath()
|
|
path = path.AppendPoint(NewPoint(V1X, V1Y))
|
|
path = path.AppendPoint(NewPoint(V2X, V2Y))
|
|
path = path.AppendPoint(NewPoint(V3X, V3Y))
|
|
path = path.AppendPoint(NewPoint(V4X, V4Y))
|
|
|
|
lineEnding1 := line.LineEndingStyle1
|
|
lineEnding2 := line.LineEndingStyle2
|
|
|
|
// TODO: Allow custom height/widths.
|
|
arrowHeight := 3 * w
|
|
arrowWidth := 3 * w
|
|
arrowExtruding := (arrowWidth - w) / 2
|
|
|
|
if lineEnding2 == LineEndingStyleArrow {
|
|
// Convert P2, P3
|
|
p2 := path.GetPointNumber(2)
|
|
|
|
va1 := NewVectorPolar(arrowHeight, theta+pi)
|
|
pa1 := p2.AddVector(va1)
|
|
|
|
bVec := NewVectorPolar(arrowWidth/2, theta+pi/2)
|
|
aVec := NewVectorPolar(arrowHeight, theta)
|
|
|
|
va2 := NewVectorPolar(arrowExtruding, theta+pi/2)
|
|
pa2 := pa1.AddVector(va2)
|
|
|
|
va3 := aVec.Add(bVec.Flip())
|
|
pa3 := pa2.AddVector(va3)
|
|
|
|
va4 := bVec.Scale(2).Flip().Add(va3.Flip())
|
|
pa4 := pa3.AddVector(va4)
|
|
|
|
pa5 := pa1.AddVector(NewVectorPolar(w, theta-pi/2))
|
|
|
|
newpath := NewPath()
|
|
newpath = newpath.AppendPoint(path.GetPointNumber(1))
|
|
newpath = newpath.AppendPoint(pa1)
|
|
newpath = newpath.AppendPoint(pa2)
|
|
newpath = newpath.AppendPoint(pa3)
|
|
newpath = newpath.AppendPoint(pa4)
|
|
newpath = newpath.AppendPoint(pa5)
|
|
newpath = newpath.AppendPoint(path.GetPointNumber(4))
|
|
|
|
path = newpath
|
|
}
|
|
if lineEnding1 == LineEndingStyleArrow {
|
|
// Get the first and last points.
|
|
p1 := path.GetPointNumber(1)
|
|
pn := path.GetPointNumber(path.Length())
|
|
|
|
// First three points on arrow.
|
|
v1 := NewVectorPolar(w/2, theta+pi+pi/2)
|
|
pa1 := p1.AddVector(v1)
|
|
|
|
v2 := NewVectorPolar(arrowHeight, theta).Add(NewVectorPolar(arrowWidth/2, theta+pi/2))
|
|
pa2 := pa1.AddVector(v2)
|
|
|
|
v3 := NewVectorPolar(arrowExtruding, theta-pi/2)
|
|
pa3 := pa2.AddVector(v3)
|
|
|
|
// Last three points
|
|
v5 := NewVectorPolar(arrowHeight, theta)
|
|
pa5 := pn.AddVector(v5)
|
|
|
|
v6 := NewVectorPolar(arrowExtruding, theta+pi+pi/2)
|
|
pa6 := pa5.AddVector(v6)
|
|
|
|
pa7 := pa1
|
|
|
|
newpath := NewPath()
|
|
newpath = newpath.AppendPoint(pa1)
|
|
newpath = newpath.AppendPoint(pa2)
|
|
newpath = newpath.AppendPoint(pa3)
|
|
for _, p := range path.Points[1 : len(path.Points)-1] {
|
|
newpath = newpath.AppendPoint(p)
|
|
}
|
|
newpath = newpath.AppendPoint(pa5)
|
|
newpath = newpath.AppendPoint(pa6)
|
|
newpath = newpath.AppendPoint(pa7)
|
|
|
|
path = newpath
|
|
}
|
|
|
|
creator := pdfcontent.NewContentCreator()
|
|
|
|
// Draw line with arrow
|
|
creator.
|
|
Add_q().
|
|
Add_rg(line.LineColor.R(), line.LineColor.G(), line.LineColor.B())
|
|
if len(gsName) > 1 {
|
|
// If a graphics state is provided, use it. (Used for transparency settings here).
|
|
creator.Add_gs(pdfcore.PdfObjectName(gsName))
|
|
}
|
|
|
|
path = path.Offset(line.X1, line.Y1)
|
|
|
|
pathBbox := path.GetBoundingBox()
|
|
|
|
DrawPathWithCreator(path, creator)
|
|
|
|
if line.LineStyle == LineStyleDashed {
|
|
creator.
|
|
Add_d([]int64{1, 1}, 0).
|
|
Add_S().
|
|
Add_f().
|
|
Add_Q()
|
|
} else {
|
|
creator.
|
|
Add_f().
|
|
Add_Q()
|
|
}
|
|
|
|
// Bounding box - global coordinate system.
|
|
bbox := &pdf.PdfRectangle{}
|
|
bbox.Llx = pathBbox.X
|
|
bbox.Lly = pathBbox.Y
|
|
bbox.Urx = pathBbox.X + pathBbox.Width
|
|
bbox.Ury = pathBbox.Y + pathBbox.Height
|
|
|
|
return creator.Bytes(), bbox, nil
|
|
}
|
|
|
|
// BasicLine defines a line between point 1 (X1,Y1) and point 2 (X2,Y2). The line has a specified width, color and opacity.
|
|
type BasicLine struct {
|
|
X1 float64
|
|
Y1 float64
|
|
X2 float64
|
|
Y2 float64
|
|
LineColor *pdf.PdfColorDeviceRGB
|
|
Opacity float64 // Alpha value (0-1).
|
|
LineWidth float64
|
|
LineStyle LineStyle
|
|
}
|
|
|
|
// Draw draws the basic line to PDF. Generates the content stream which can be used in page contents or appearance
|
|
// stream of annotation. Returns the stream content, XForm bounding box (local), bounding box and an error if
|
|
// one occurred.
|
|
func (line BasicLine) Draw(gsName string) ([]byte, *pdf.PdfRectangle, error) {
|
|
w := line.LineWidth
|
|
|
|
path := NewPath()
|
|
path = path.AppendPoint(NewPoint(line.X1, line.Y1))
|
|
path = path.AppendPoint(NewPoint(line.X2, line.Y2))
|
|
|
|
cc := pdfcontent.NewContentCreator()
|
|
|
|
pathBbox := path.GetBoundingBox()
|
|
|
|
DrawPathWithCreator(path, cc)
|
|
|
|
if line.LineStyle == LineStyleDashed {
|
|
cc.Add_d([]int64{1, 1}, 0)
|
|
}
|
|
cc.Add_RG(line.LineColor.R(), line.LineColor.G(), line.LineColor.B()).
|
|
Add_w(w).
|
|
Add_S().
|
|
Add_Q()
|
|
|
|
// Bounding box - global coordinate system.
|
|
bbox := &pdf.PdfRectangle{}
|
|
bbox.Llx = pathBbox.X
|
|
bbox.Lly = pathBbox.Y
|
|
bbox.Urx = pathBbox.X + pathBbox.Width
|
|
bbox.Ury = pathBbox.Y + pathBbox.Height
|
|
|
|
return cc.Bytes(), bbox, nil
|
|
}
|