diff --git a/pdf/annotator/annotator.go b/pdf/annotator/annotator.go index 30e91684..ea429539 100644 --- a/pdf/annotator/annotator.go +++ b/pdf/annotator/annotator.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package annotator import ( @@ -126,4 +131,50 @@ func CreateRectangleAnnotation(rectDef RectangleAnnotationDef) (*pdf.PdfAnnotati } type CircleAnnotationDef 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). +} + +// Creates a circle/ellipse annotation object with appearance stream that can be added to page PDF annotations. +func CreateCircleAnnotation(circDef CircleAnnotationDef) (*pdf.PdfAnnotation, error) { + circAnnotation := pdf.NewPdfAnnotationCircle() + + if circDef.BorderEnabled { + r, g, b := circDef.BorderColor.R(), circDef.BorderColor.G(), circDef.BorderColor.B() + circAnnotation.C = pdfcore.MakeArrayFromFloats([]float64{r, g, b}) + bs := pdf.NewBorderStyle() + bs.SetBorderWidth(circDef.BorderWidth) + circAnnotation.BS = bs.ToPdfObject() + } + + if circDef.FillEnabled { + r, g, b := circDef.FillColor.R(), circDef.FillColor.G(), circDef.FillColor.B() + circAnnotation.IC = pdfcore.MakeArrayFromFloats([]float64{r, g, b}) + } else { + circAnnotation.IC = pdfcore.MakeArrayFromIntegers([]int{}) // No fill. + } + + if circDef.Opacity < 1.0 { + circAnnotation.CA = pdfcore.MakeFloat(circDef.Opacity) + } + + // Make the appearance stream (for uniform appearance). + apDict, bbox, err := makeCircleAnnotationAppearanceStream(circDef) + if err != nil { + return nil, err + } + + circAnnotation.AP = apDict + circAnnotation.Rect = pdfcore.MakeArrayFromFloats([]float64{bbox.Llx, bbox.Lly, bbox.Urx, bbox.Ury}) + + return circAnnotation.PdfAnnotation, nil + } diff --git a/pdf/annotator/circle.go b/pdf/annotator/circle.go new file mode 100644 index 00000000..3b893d80 --- /dev/null +++ b/pdf/annotator/circle.go @@ -0,0 +1,137 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package annotator + +import ( + "github.com/unidoc/unidoc/common" + + pdfcontent "github.com/unidoc/unidoc/pdf/contentstream" + "github.com/unidoc/unidoc/pdf/contentstream/draw" + pdfcore "github.com/unidoc/unidoc/pdf/core" + pdf "github.com/unidoc/unidoc/pdf/model" +) + +// Make the bezier path with the content creator. +func drawBezierPathWithCreator(bpath draw.CubicBezierPath, creator *pdfcontent.ContentCreator) { + for idx, c := range bpath.Curves { + if idx == 0 { + creator.Add_m(c.P0.X, c.P0.Y) + } + creator.Add_c(c.P1.X, c.P1.Y, c.P2.X, c.P2.Y, c.P3.X, c.P3.Y) + } +} + +func makeCircleAnnotationAppearanceStream(circDef CircleAnnotationDef) (*pdfcore.PdfObjectDictionary, *pdf.PdfRectangle, error) { + form := pdf.NewXObjectForm() + form.Resources = pdf.NewPdfPageResources() + + gsName := "" + if circDef.Opacity < 1.0 { + // Create graphics state with right opacity. + gsState := &pdfcore.PdfObjectDictionary{} + gsState.Set("ca", pdfcore.MakeFloat(circDef.Opacity)) + gsState.Set("CA", pdfcore.MakeFloat(circDef.Opacity)) + err := form.Resources.AddExtGState("gs1", gsState) + if err != nil { + common.Log.Debug("Unable to add extgstate gs1") + return nil, nil, err + } + + gsName = "gs1" + } + + content, localBbox, globalBbox, err := drawPdfCircle(circDef, gsName) + if err != nil { + return nil, nil, err + } + + err = form.SetContentStream(content, nil) + if err != nil { + return nil, nil, err + } + + // Local bounding box for the XObject Form. + form.BBox = localBbox.ToPdfObject() + + apDict := &pdfcore.PdfObjectDictionary{} + apDict.Set("N", form.ToPdfObject()) + + return apDict, globalBbox, nil +} + +func drawPdfCircle(circDef CircleAnnotationDef, gsName string) ([]byte, *pdf.PdfRectangle, *pdf.PdfRectangle, error) { + xRad := circDef.Width / 2 + yRad := circDef.Height / 2 + if circDef.BorderEnabled { + xRad -= circDef.BorderWidth / 2 + yRad -= circDef.BorderWidth / 2 + } + + magic := 0.551784 + xMagic := xRad * magic + yMagic := yRad * magic + + bpath := draw.NewCubicBezierPath() + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(-xRad, 0, -xRad, yMagic, -xMagic, yRad, 0, yRad)) + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(0, yRad, xMagic, yRad, xRad, yMagic, xRad, 0)) + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(xRad, 0, xRad, -yMagic, xMagic, -yRad, 0, -yRad)) + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(0, -yRad, -xMagic, -yRad, -xRad, -yMagic, -xRad, 0)) + bpath = bpath.Offset(xRad, yRad) + if circDef.BorderEnabled { + bpath = bpath.Offset(circDef.BorderWidth/2, circDef.BorderWidth/2) + } + + creator := pdfcontent.NewContentCreator() + + creator.Add_q() + + if circDef.FillEnabled { + creator.Add_rg(circDef.FillColor.R(), circDef.FillColor.G(), circDef.FillColor.B()) + } + if circDef.BorderEnabled { + creator.Add_RG(circDef.BorderColor.R(), circDef.BorderColor.G(), circDef.BorderColor.B()) + creator.Add_w(circDef.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_B() // fill and stroke. + creator.Add_Q() + + // Offsets (needed for placement of annotations bbox). + offX := circDef.X + offY := circDef.Y + + // Get bounding box. + pathBbox := bpath.GetBoundingBox() + if circDef.BorderEnabled { + // Account for stroke width. + pathBbox.Height += circDef.BorderWidth + pathBbox.Width += circDef.BorderWidth + pathBbox.X -= circDef.BorderWidth / 2 + pathBbox.Y -= circDef.BorderWidth / 2 + } + + // Bounding box - local coordinate system (without offset). + localBbox := &pdf.PdfRectangle{} + localBbox.Llx = pathBbox.X + localBbox.Lly = pathBbox.Y + localBbox.Urx = pathBbox.X + pathBbox.Width + localBbox.Ury = pathBbox.Y + pathBbox.Height + + // Bounding box - global page coordinate system (with offset). + globalBbox := &pdf.PdfRectangle{} + globalBbox.Llx = offX + pathBbox.X + globalBbox.Lly = offY + pathBbox.Y + globalBbox.Urx = offX + pathBbox.X + pathBbox.Width + globalBbox.Ury = offY + pathBbox.Y + pathBbox.Height + + return creator.Bytes(), localBbox, globalBbox, nil +} diff --git a/pdf/annotator/line.go b/pdf/annotator/line.go index 888537b9..fc9a9b77 100644 --- a/pdf/annotator/line.go +++ b/pdf/annotator/line.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package annotator import ( diff --git a/pdf/annotator/rectangle.go b/pdf/annotator/rectangle.go index 1f275f2e..e23e4224 100644 --- a/pdf/annotator/rectangle.go +++ b/pdf/annotator/rectangle.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package annotator import ( diff --git a/pdf/contentstream/draw/bezier_curve.go b/pdf/contentstream/draw/bezier_curve.go new file mode 100644 index 00000000..a32f89d6 --- /dev/null +++ b/pdf/contentstream/draw/bezier_curve.go @@ -0,0 +1,154 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package draw + +import ( + "math" + + "github.com/unidoc/unidoc/pdf/model" +) + +// Cubic bezier curves are defined by: +// R(t) = P0*(1-t)^3 + P1*3*t*(1-t)^2 + P2*3*t^2*(1-t) + P3*t^3 +// where P0 is the current point, P1, P2 control points and P3 the final point. +type CubicBezierCurve struct { + P0 Point // Starting point. + P1 Point // Control point 1. + P2 Point // Control point 2. + P3 Point // Final point. +} + +func NewCubicBezierCurve(x0, y0, x1, y1, x2, y2, x3, y3 float64) CubicBezierCurve { + curve := CubicBezierCurve{} + curve.P0 = NewPoint(x0, y0) + curve.P1 = NewPoint(x1, y1) + curve.P2 = NewPoint(x2, y2) + curve.P3 = NewPoint(x3, y3) + return curve +} + +// Add X,Y offset to all points on a curve. +func (curve CubicBezierCurve) AddOffsetXY(offX, offY float64) CubicBezierCurve { + curve.P0.X += offX + curve.P1.X += offX + curve.P2.X += offX + curve.P3.X += offX + + curve.P0.Y += offY + curve.P1.Y += offY + curve.P2.Y += offY + curve.P3.Y += offY + + return curve +} + +func (curve CubicBezierCurve) GetBounds() model.PdfRectangle { + minX := curve.P0.X + maxX := curve.P0.X + minY := curve.P0.Y + maxY := curve.P0.Y + + // 1000 points. + for t := 0.0; t <= 1.0; t += 0.001 { + Rx := curve.P0.X*math.Pow(1-t, 3) + + curve.P1.X*3*t*math.Pow(1-t, 2) + + curve.P2.X*3*math.Pow(t, 2)*(1-t) + + curve.P3.X*math.Pow(t, 3) + Ry := curve.P0.Y*math.Pow(1-t, 3) + + curve.P1.Y*3*t*math.Pow(1-t, 2) + + curve.P2.Y*3*math.Pow(t, 2)*(1-t) + + curve.P3.Y*math.Pow(t, 3) + + if Rx < minX { + minX = Rx + } + if Rx > maxX { + maxX = Rx + } + if Ry < minY { + minY = Ry + } + if Ry > maxY { + maxY = Ry + } + } + + bounds := model.PdfRectangle{} + bounds.Llx = minX + bounds.Lly = minY + bounds.Urx = maxX + bounds.Ury = maxY + return bounds +} + +type CubicBezierPath struct { + Curves []CubicBezierCurve +} + +func NewCubicBezierPath() CubicBezierPath { + bpath := CubicBezierPath{} + bpath.Curves = []CubicBezierCurve{} + return bpath +} + +func (this CubicBezierPath) AppendCurve(curve CubicBezierCurve) CubicBezierPath { + this.Curves = append(this.Curves, curve) + return this +} + +func (bpath CubicBezierPath) Copy() CubicBezierPath { + bpathcopy := CubicBezierPath{} + bpathcopy.Curves = []CubicBezierCurve{} + for _, c := range bpath.Curves { + bpathcopy.Curves = append(bpathcopy.Curves, c) + } + return bpathcopy +} + +func (bpath CubicBezierPath) Offset(offX, offY float64) CubicBezierPath { + for i, c := range bpath.Curves { + bpath.Curves[i] = c.AddOffsetXY(offX, offY) + } + return bpath +} + +func (bpath CubicBezierPath) GetBoundingBox() Rectangle { + bbox := Rectangle{} + + minX := 0.0 + maxX := 0.0 + minY := 0.0 + maxY := 0.0 + for idx, c := range bpath.Curves { + curveBounds := c.GetBounds() + if idx == 0 { + minX = curveBounds.Llx + maxX = curveBounds.Urx + minY = curveBounds.Lly + maxY = curveBounds.Ury + continue + } + + if curveBounds.Llx < minX { + minX = curveBounds.Llx + } + if curveBounds.Urx > maxX { + maxX = curveBounds.Urx + } + if curveBounds.Lly < minY { + minY = curveBounds.Lly + } + if curveBounds.Ury > maxY { + maxY = curveBounds.Ury + } + } + + bbox.X = minX + bbox.Y = minY + bbox.Width = maxX - minX + bbox.Height = maxY - minY + return bbox +} diff --git a/pdf/contentstream/draw/path.go b/pdf/contentstream/draw/path.go index 553ba72e..1246b6e8 100644 --- a/pdf/contentstream/draw/path.go +++ b/pdf/contentstream/draw/path.go @@ -1,29 +1,11 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package draw -import "fmt" - -type Point struct { - X float64 - Y float64 -} - -func (p Point) Add(dx, dy float64) Point { - p.X += dx - p.Y += dy - return p -} - -// Add vector to a point. -func (this Point) AddVector(v Vector) Point { - this.X += v.Dx - this.Y += v.Dy - return this -} - -func (p Point) String() string { - return fmt.Sprintf("(%.1f,%.1f)", p.X, p.Y) -} - +// A path consists of straight line connections between each point defined in an array of points. type Path struct { Points []Point } @@ -34,13 +16,6 @@ func NewPath() Path { return path } -func NewPoint(x, y float64) Point { - point := Point{} - point.X = x - point.Y = y - return point -} - func (this Path) AppendPoint(point Point) Path { this.Points = append(this.Points, point) return this diff --git a/pdf/contentstream/draw/point.go b/pdf/contentstream/draw/point.go new file mode 100644 index 00000000..eb5265c3 --- /dev/null +++ b/pdf/contentstream/draw/point.go @@ -0,0 +1,37 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package draw + +import "fmt" + +type Point struct { + X float64 + Y float64 +} + +func NewPoint(x, y float64) Point { + point := Point{} + point.X = x + point.Y = y + return point +} + +func (p Point) Add(dx, dy float64) Point { + p.X += dx + p.Y += dy + return p +} + +// Add vector to a point. +func (this Point) AddVector(v Vector) Point { + this.X += v.Dx + this.Y += v.Dy + return this +} + +func (p Point) String() string { + return fmt.Sprintf("(%.1f,%.1f)", p.X, p.Y) +} diff --git a/pdf/contentstream/draw/vector.go b/pdf/contentstream/draw/vector.go index 5d05561b..b8afc699 100644 --- a/pdf/contentstream/draw/vector.go +++ b/pdf/contentstream/draw/vector.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package draw import "math"