mirror of
https://github.com/unidoc/unipdf.git
synced 2025-05-05 19:30:30 +08:00
Annotations support for rectangle and ellipses with appearance stream in annotator.
This commit is contained in:
parent
3297004174
commit
89b36f338b
@ -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
|
package annotator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -126,4 +131,50 @@ func CreateRectangleAnnotation(rectDef RectangleAnnotationDef) (*pdf.PdfAnnotati
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CircleAnnotationDef struct {
|
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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
137
pdf/annotator/circle.go
Normal file
137
pdf/annotator/circle.go
Normal file
@ -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
|
||||||
|
}
|
@ -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
|
package annotator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -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
|
package annotator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
154
pdf/contentstream/draw/bezier_curve.go
Normal file
154
pdf/contentstream/draw/bezier_curve.go
Normal file
@ -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
|
||||||
|
}
|
@ -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
|
package draw
|
||||||
|
|
||||||
import "fmt"
|
// A path consists of straight line connections between each point defined in an array of points.
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Path struct {
|
type Path struct {
|
||||||
Points []Point
|
Points []Point
|
||||||
}
|
}
|
||||||
@ -34,13 +16,6 @@ func NewPath() Path {
|
|||||||
return 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 {
|
func (this Path) AppendPoint(point Point) Path {
|
||||||
this.Points = append(this.Points, point)
|
this.Points = append(this.Points, point)
|
||||||
return this
|
return this
|
||||||
|
37
pdf/contentstream/draw/point.go
Normal file
37
pdf/contentstream/draw/point.go
Normal file
@ -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)
|
||||||
|
}
|
@ -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
|
package draw
|
||||||
|
|
||||||
import "math"
|
import "math"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user