Merge branch 'v2' of https://github.com/unidoc/unidoc into up_v2_dev

This commit is contained in:
Peter Williams 2017-04-18 16:14:38 +10:00
commit a9c2a01404
14 changed files with 1131 additions and 24 deletions

180
pdf/annotator/annotator.go Normal file
View File

@ -0,0 +1,180 @@
/*
* 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 (
pdfcore "github.com/unidoc/unidoc/pdf/core"
pdf "github.com/unidoc/unidoc/pdf/model"
)
// The currently supported line ending styles are None, Arrow (ClosedArrow) and Butt.
type LineEndingStyle int
const (
LineEndingStyleNone LineEndingStyle = 0
LineEndingStyleArrow LineEndingStyle = 1
LineEndingStyleButt LineEndingStyle = 2
)
// Defines a line 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 LineAnnotationDef 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.
}
// Creates a line annotation object that can be added to page PDF annotations.
func CreateLineAnnotation(lineDef LineAnnotationDef) (*pdf.PdfAnnotation, error) {
// Line annotation.
lineAnnotation := pdf.NewPdfAnnotationLine()
// Line endpoint locations.
lineAnnotation.L = pdfcore.MakeArrayFromFloats([]float64{lineDef.X1, lineDef.Y1, lineDef.X2, lineDef.Y2})
// Line endings.
le1 := pdfcore.MakeName("None")
if lineDef.LineEndingStyle1 == LineEndingStyleArrow {
le1 = pdfcore.MakeName("ClosedArrow")
}
le2 := pdfcore.MakeName("None")
if lineDef.LineEndingStyle2 == LineEndingStyleArrow {
le2 = pdfcore.MakeName("ClosedArrow")
}
lineAnnotation.LE = pdfcore.MakeArray(le1, le2)
// Opacity.
if lineDef.Opacity < 1.0 {
lineAnnotation.CA = pdfcore.MakeFloat(lineDef.Opacity)
}
r, g, b := lineDef.LineColor.R(), lineDef.LineColor.G(), lineDef.LineColor.B()
lineAnnotation.IC = pdfcore.MakeArrayFromFloats([]float64{r, g, b}) // fill color of line endings, rgb 0-1.
lineAnnotation.C = pdfcore.MakeArrayFromFloats([]float64{r, g, b}) // line color, rgb 0-1.
bs := pdf.NewBorderStyle()
bs.SetBorderWidth(lineDef.LineWidth) // Line width: 3 points.
lineAnnotation.BS = bs.ToPdfObject()
// Make the appearance stream (for uniform appearance).
apDict, bbox, err := makeLineAnnotationAppearanceStream(lineDef)
if err != nil {
return nil, err
}
lineAnnotation.AP = apDict
// The rect specifies the location and dimensions of the annotation. Technically if the annotation could not
// be displayed if it goes outside these bounds, although rarely enforced.
lineAnnotation.Rect = pdfcore.MakeArrayFromFloats([]float64{bbox.Llx, bbox.Lly, bbox.Urx, bbox.Ury})
return lineAnnotation.PdfAnnotation, nil
}
// A rectangle defined with a specified Width and Height and a lower left corner at (X,Y). The rectangle can
// optionally have a border and a filling color.
// The Width/Height includes the border (if any specified).
type RectangleAnnotationDef 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 rectangle annotation object that can be added to page PDF annotations.
func CreateRectangleAnnotation(rectDef RectangleAnnotationDef) (*pdf.PdfAnnotation, error) {
rectAnnotation := pdf.NewPdfAnnotationSquare()
if rectDef.BorderEnabled {
r, g, b := rectDef.BorderColor.R(), rectDef.BorderColor.G(), rectDef.BorderColor.B()
rectAnnotation.C = pdfcore.MakeArrayFromFloats([]float64{r, g, b})
bs := pdf.NewBorderStyle()
bs.SetBorderWidth(rectDef.BorderWidth)
rectAnnotation.BS = bs.ToPdfObject()
}
if rectDef.FillEnabled {
r, g, b := rectDef.FillColor.R(), rectDef.FillColor.G(), rectDef.FillColor.B()
rectAnnotation.IC = pdfcore.MakeArrayFromFloats([]float64{r, g, b})
} else {
rectAnnotation.IC = pdfcore.MakeArrayFromIntegers([]int{}) // No fill.
}
if rectDef.Opacity < 1.0 {
rectAnnotation.CA = pdfcore.MakeFloat(rectDef.Opacity)
}
// Make the appearance stream (for uniform appearance).
apDict, bbox, err := makeRectangleAnnotationAppearanceStream(rectDef)
if err != nil {
return nil, err
}
rectAnnotation.AP = apDict
rectAnnotation.Rect = pdfcore.MakeArrayFromFloats([]float64{bbox.Llx, bbox.Lly, bbox.Urx, bbox.Ury})
return rectAnnotation.PdfAnnotation, nil
}
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
}

143
pdf/annotator/circle.go Normal file
View File

@ -0,0 +1,143 @@
/*
* 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)
if circDef.FillEnabled && circDef.BorderEnabled {
creator.Add_B() // fill and stroke.
} else if circDef.FillEnabled {
creator.Add_f() // Fill.
} else if circDef.BorderEnabled {
creator.Add_S() // 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
}

10
pdf/annotator/doc.go Normal file
View File

@ -0,0 +1,10 @@
/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/
// The annotator package contains an annotator with a convenient interface for creating annotations with appearance
// streams. It goes beyond the models package which includes definitions of basic annotation models, in that it
// can create the appearance streams which specify the exact appearance as needed by many pdf viewers for consistent
// appearance of the annotations.
package annotator

229
pdf/annotator/line.go Normal file
View File

@ -0,0 +1,229 @@
/*
* 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 (
"math"
"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 path with the content creator.
func drawPathWithCreator(path draw.Path, creator *pdfcontent.ContentCreator) {
for idx, p := range path.Points {
if idx == 0 {
creator.Add_m(p.X, p.Y)
} else {
creator.Add_l(p.X, p.Y)
}
}
}
func makeLineAnnotationAppearanceStream(lineDef LineAnnotationDef) (*pdfcore.PdfObjectDictionary, *pdf.PdfRectangle, error) {
form := pdf.NewXObjectForm()
form.Resources = pdf.NewPdfPageResources()
gsName := ""
if lineDef.Opacity < 1.0 {
// Create graphics state with right opacity.
gsState := &pdfcore.PdfObjectDictionary{}
gsState.Set("ca", pdfcore.MakeFloat(lineDef.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 := drawPdfLine(lineDef, 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
}
// Draw a line in 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), Rect bounding box (global/page) and an error if one occurred.
func drawPdfLine(lineDef LineAnnotationDef, gsName string) ([]byte, *pdf.PdfRectangle, *pdf.PdfRectangle, error) {
x1, x2 := lineDef.X1, lineDef.X2
y1, y2 := lineDef.Y1, lineDef.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 := lineDef.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 := draw.NewPath()
path = path.AppendPoint(draw.NewPoint(V1X, V1Y))
path = path.AppendPoint(draw.NewPoint(V2X, V2Y))
path = path.AppendPoint(draw.NewPoint(V3X, V3Y))
path = path.AppendPoint(draw.NewPoint(V4X, V4Y))
lineEnding1 := lineDef.LineEndingStyle1
lineEnding2 := lineDef.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 := draw.NewVectorPolar(arrowHeight, theta+pi)
pa1 := p2.AddVector(va1)
bVec := draw.NewVectorPolar(arrowWidth/2, theta+pi/2)
aVec := draw.NewVectorPolar(arrowHeight, theta)
va2 := draw.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(draw.NewVectorPolar(w, theta-pi/2))
newpath := draw.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 := draw.NewVectorPolar(w/2, theta+pi+pi/2)
pa1 := p1.AddVector(v1)
v2 := draw.NewVectorPolar(arrowHeight, theta).Add(draw.NewVectorPolar(arrowWidth/2, theta+pi/2))
pa2 := pa1.AddVector(v2)
v3 := draw.NewVectorPolar(arrowExtruding, theta-pi/2)
pa3 := pa2.AddVector(v3)
// Last three points
v5 := draw.NewVectorPolar(arrowHeight, theta)
pa5 := pn.AddVector(v5)
v6 := draw.NewVectorPolar(arrowExtruding, theta+pi+pi/2)
pa6 := pa5.AddVector(v6)
pa7 := pa1
newpath := draw.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
}
pathBbox := path.GetBoundingBox()
creator := pdfcontent.NewContentCreator()
// Draw line with arrow
creator.
Add_q().
Add_rg(lineDef.LineColor.R(), lineDef.LineColor.G(), lineDef.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))
}
drawPathWithCreator(path, creator)
creator.Add_f().
//creator.Add_S().
Add_Q()
// Offsets (needed for placement of annotations bbox).
offX := x1 - VsX
offY := y1 - VsY
// 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
}

111
pdf/annotator/rectangle.go Normal file
View File

@ -0,0 +1,111 @@
/*
* 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"
)
func makeRectangleAnnotationAppearanceStream(rectDef RectangleAnnotationDef) (*pdfcore.PdfObjectDictionary, *pdf.PdfRectangle, error) {
form := pdf.NewXObjectForm()
form.Resources = pdf.NewPdfPageResources()
gsName := ""
if rectDef.Opacity < 1.0 {
// Create graphics state with right opacity.
gsState := &pdfcore.PdfObjectDictionary{}
gsState.Set("ca", pdfcore.MakeFloat(rectDef.Opacity))
gsState.Set("CA", pdfcore.MakeFloat(rectDef.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 := drawPdfRectangle(rectDef, 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 drawPdfRectangle(rectDef RectangleAnnotationDef, gsName string) ([]byte, *pdf.PdfRectangle, *pdf.PdfRectangle, error) {
path := draw.NewPath()
path = path.AppendPoint(draw.NewPoint(0, 0))
path = path.AppendPoint(draw.NewPoint(0, rectDef.Height))
path = path.AppendPoint(draw.NewPoint(rectDef.Width, rectDef.Height))
path = path.AppendPoint(draw.NewPoint(rectDef.Width, 0))
path = path.AppendPoint(draw.NewPoint(0, 0))
creator := pdfcontent.NewContentCreator()
creator.Add_q()
if rectDef.FillEnabled {
creator.Add_rg(rectDef.FillColor.R(), rectDef.FillColor.G(), rectDef.FillColor.B())
}
if rectDef.BorderEnabled {
creator.Add_RG(rectDef.BorderColor.R(), rectDef.BorderColor.G(), rectDef.BorderColor.B())
creator.Add_w(rectDef.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)
if rectDef.FillEnabled && rectDef.BorderEnabled {
creator.Add_B() // fill and stroke.
} else if rectDef.FillEnabled {
creator.Add_f() // Fill.
} else if rectDef.BorderEnabled {
creator.Add_S() // Stroke.
}
creator.Add_Q()
// Offsets (needed for placement of annotations bbox).
offX := rectDef.X
offY := rectDef.Y
// Get bounding box.
pathBbox := path.GetBoundingBox()
// 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
}

View 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
}

View File

@ -0,0 +1,9 @@
/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/
// The draw package has handy features for defining paths which can be used to draw content on a PDF page. Handles
// defining paths as points, vector calculations and conversion to PDF content stream data which can be used in
// page content streams and XObject forms and thus also in annotation appearance streams.
package draw

View File

@ -0,0 +1,103 @@
/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/
package draw
// A path consists of straight line connections between each point defined in an array of points.
type Path struct {
Points []Point
}
func NewPath() Path {
path := Path{}
path.Points = []Point{}
return path
}
func (this Path) AppendPoint(point Point) Path {
this.Points = append(this.Points, point)
return this
}
func (this Path) RemovePoint(number int) Path {
if number < 1 || number > len(this.Points) {
return this
}
idx := number - 1
this.Points = append(this.Points[:idx], this.Points[idx+1:]...)
return this
}
func (this Path) Length() int {
return len(this.Points)
}
func (this Path) GetPointNumber(number int) Point {
if number < 1 || number > len(this.Points) {
return Point{}
}
return this.Points[number-1]
}
func (path Path) Copy() Path {
pathcopy := Path{}
pathcopy.Points = []Point{}
for _, p := range path.Points {
pathcopy.Points = append(pathcopy.Points, p)
}
return pathcopy
}
func (path Path) Offset(offX, offY float64) Path {
for i, p := range path.Points {
path.Points[i] = p.Add(offX, offY)
}
return path
}
func (path Path) GetBoundingBox() Rectangle {
bbox := Rectangle{}
minX := 0.0
maxX := 0.0
minY := 0.0
maxY := 0.0
for idx, p := range path.Points {
if idx == 0 {
minX = p.X
maxX = p.X
minY = p.Y
maxY = p.Y
continue
}
if p.X < minX {
minX = p.X
}
if p.X > maxX {
maxX = p.X
}
if p.Y < minY {
minY = p.Y
}
if p.Y > maxY {
maxY = p.Y
}
}
bbox.X = minX
bbox.Y = minY
bbox.Width = maxX - minX
bbox.Height = maxY - minY
return bbox
}
type Rectangle struct {
X float64
Y float64
Width float64
Height float64
}

View 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)
}

View File

@ -0,0 +1,85 @@
/*
* 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"
type Vector struct {
Dx float64
Dy float64
}
func NewVector(dx, dy float64) Vector {
v := Vector{}
v.Dx = dx
v.Dy = dy
return v
}
func NewVectorBetween(a Point, b Point) Vector {
v := Vector{}
v.Dx = b.X - a.X
v.Dy = b.Y - a.Y
return v
}
func NewVectorPolar(length float64, theta float64) Vector {
v := Vector{}
v.Dx = length * math.Cos(theta)
v.Dy = length * math.Sin(theta)
return v
}
func (v Vector) Add(other Vector) Vector {
v.Dx += other.Dx
v.Dy += other.Dy
return v
}
func (v Vector) Rotate(phi float64) Vector {
mag := v.Magnitude()
angle := v.GetPolarAngle()
return NewVectorPolar(mag, angle+phi)
}
// Change the sign of the vector: -vector.
func (this Vector) Flip() Vector {
mag := this.Magnitude()
theta := this.GetPolarAngle()
this.Dx = mag * math.Cos(theta+math.Pi)
this.Dy = mag * math.Sin(theta+math.Pi)
return this
}
func (v Vector) FlipY() Vector {
v.Dy = -v.Dy
return v
}
func (v Vector) FlipX() Vector {
v.Dx = -v.Dx
return v
}
func (this Vector) Scale(factor float64) Vector {
mag := this.Magnitude()
theta := this.GetPolarAngle()
this.Dx = factor * mag * math.Cos(theta)
this.Dy = factor * mag * math.Sin(theta)
return this
}
func (this Vector) Magnitude() float64 {
return math.Sqrt(math.Pow(this.Dx, 2.0) + math.Pow(this.Dy, 2.0))
}
func (this Vector) GetPolarAngle() float64 {
return math.Atan2(this.Dy, this.Dx)
}

View File

@ -33,17 +33,20 @@ type ContentStreamInlineImage struct {
// Make a new content stream inline image object from an image.
func NewInlineImageFromImage(img Image, encoder StreamEncoder) (*ContentStreamInlineImage, error) {
filterName := ""
if encoder == nil {
encoder = NewRawEncoder()
} else {
filterName = encoder.GetFilterName()
}
inlineImage := ContentStreamInlineImage{}
if img.ColorComponents == 1 {
inlineImage.ColorSpace = MakeName("DeviceGray")
inlineImage.ColorSpace = MakeName("G") // G short for DeviceGray
} else if img.ColorComponents == 3 {
inlineImage.ColorSpace = MakeName("DeviceRGB")
inlineImage.ColorSpace = MakeName("RGB") // RGB short for DeviceRGB
} else if img.ColorComponents == 4 {
inlineImage.ColorSpace = MakeName("DeviceCMYK")
inlineImage.ColorSpace = MakeName("CMYK") // CMYK short for DeviceCMYK
} else {
common.Log.Debug("Invalid number of color components for inline image: %d", img.ColorComponents)
return nil, errors.New("Invalid number of color components")
@ -58,7 +61,6 @@ func NewInlineImageFromImage(img Image, encoder StreamEncoder) (*ContentStreamIn
}
inlineImage.stream = encoded
filterName := encoder.GetFilterName()
if len(filterName) > 0 {
inlineImage.Filter = MakeName(filterName)
}
@ -110,40 +112,40 @@ func (this *ContentStreamInlineImage) DefaultWriteString() string {
s := ""
if this.BitsPerComponent != nil {
s += "BPC " + this.BitsPerComponent.DefaultWriteString() + "\n"
s += "/BPC " + this.BitsPerComponent.DefaultWriteString() + "\n"
}
if this.ColorSpace != nil {
s += "CS " + this.ColorSpace.DefaultWriteString() + "\n"
s += "/CS " + this.ColorSpace.DefaultWriteString() + "\n"
}
if this.Decode != nil {
s += "D " + this.Decode.DefaultWriteString() + "\n"
s += "/D " + this.Decode.DefaultWriteString() + "\n"
}
if this.DecodeParms != nil {
s += "DP " + this.DecodeParms.DefaultWriteString() + "\n"
s += "/DP " + this.DecodeParms.DefaultWriteString() + "\n"
}
if this.Filter != nil {
s += "F " + this.Filter.DefaultWriteString() + "\n"
s += "/F " + this.Filter.DefaultWriteString() + "\n"
}
if this.Height != nil {
s += "H " + this.Height.DefaultWriteString() + "\n"
s += "/H " + this.Height.DefaultWriteString() + "\n"
}
if this.ImageMask != nil {
s += "IM " + this.ImageMask.DefaultWriteString() + "\n"
s += "/IM " + this.ImageMask.DefaultWriteString() + "\n"
}
if this.Intent != nil {
s += "Intent " + this.Intent.DefaultWriteString() + "\n"
s += "/Intent " + this.Intent.DefaultWriteString() + "\n"
}
if this.Interpolate != nil {
s += "I " + this.Interpolate.DefaultWriteString() + "\n"
s += "/I " + this.Interpolate.DefaultWriteString() + "\n"
}
if this.Width != nil {
s += "W " + this.Width.DefaultWriteString() + "\n"
s += "/W " + this.Width.DefaultWriteString() + "\n"
}
output.WriteString(s)
output.WriteString("ID ")
output.Write(this.stream)
output.WriteString("\n")
output.WriteString("\nEI\n")
return output.String()
}
@ -170,6 +172,12 @@ func (this *ContentStreamInlineImage) GetColorSpace(resources *PdfPageResources)
} else if *name == "I" {
return nil, errors.New("Unsupported Index colorspace")
} else {
if resources.ColorSpace == nil {
// Can also refer to a name in the PDF page resources...
common.Log.Debug("Error, unsupported inline image colorspace: %s", *name)
return nil, errors.New("Unknown colorspace")
}
cs, has := resources.ColorSpace.Colorspaces[string(*name)]
if !has {
// Can also refer to a name in the PDF page resources...
@ -213,6 +221,7 @@ func (this *ContentStreamInlineImage) ToImage(resources *PdfPageResources) (*Ima
return nil, err
}
common.Log.Trace("encoder: %+v %T", encoder, encoder)
common.Log.Trace("inline image: %+v", this)
decoded, err := encoder.DecodeBytes(this.stream)
if err != nil {
@ -302,6 +311,7 @@ func (this *ContentStreamParser) ParseInlineImage() (*ContentStreamInlineImage,
// Not an operand.. Read key value properties..
param, ok := obj.(*PdfObjectName)
if !ok {
common.Log.Debug("Invalid inline image property (expecting name) - %T", obj)
return nil, fmt.Errorf("Invalid inline image property (expecting name) - %T", obj)
}
@ -390,6 +400,7 @@ func (this *ContentStreamParser) ParseInlineImage() (*ContentStreamInlineImage,
state = 2
} else {
im.stream = append(im.stream, skipBytes...)
skipBytes = []byte{} // Clear.
// Need an extra check to decide if we fall back to state 0 or 1.
if IsWhiteSpace(c) {
state = 1
@ -403,6 +414,7 @@ func (this *ContentStreamParser) ParseInlineImage() (*ContentStreamInlineImage,
state = 3
} else {
im.stream = append(im.stream, skipBytes...)
skipBytes = []byte{} // Clear.
state = 0
}
} else if state == 3 {
@ -419,6 +431,7 @@ func (this *ContentStreamParser) ParseInlineImage() (*ContentStreamInlineImage,
} else {
// Seems like "<ws>EI" was part of the data.
im.stream = append(im.stream, skipBytes...)
skipBytes = []byte{} // Clear.
state = 0
}
}

View File

@ -901,6 +901,24 @@ func (r *PdfPageResources) ToPdfObject() PdfObject {
return d
}
// Add External Graphics State (GState). The gsDict can be specified either directly as a dictionary or an indirect
// object containing a dictionary.
func (r *PdfPageResources) AddExtGState(gsName PdfObjectName, gsDict PdfObject) error {
if r.ExtGState == nil {
r.ExtGState = &PdfObjectDictionary{}
}
obj := r.ExtGState
dict, ok := TraceToDirectObject(obj).(*PdfObjectDictionary)
if !ok {
common.Log.Debug("ExtGState type error (got %T/%T)", obj, TraceToDirectObject(obj))
return ErrTypeError
}
(*dict)[gsName] = gsDict
return nil
}
// Get the shading specified by keyName. Returns nil if not existing. The bool flag indicated whether it was found
// or not.
func (r *PdfPageResources) GetShadingByName(keyName string) (*PdfShading, bool) {
@ -1049,8 +1067,10 @@ func (r *PdfPageResources) setXObjectByName(keyName string, stream *PdfObjectStr
r.XObject = &PdfObjectDictionary{}
}
xresDict, has := r.XObject.(*PdfObjectDictionary)
obj := TraceToDirectObject(r.XObject)
xresDict, has := obj.(*PdfObjectDictionary)
if !has {
common.Log.Debug("Invalid XObject, got %T/%T", r.XObject, obj)
return errors.New("Type check error")
}

View File

@ -29,7 +29,7 @@ type PdfShading struct {
AntiAlias *PdfObjectBool
context PdfModel // The sub shading type entry (types 1-7). Represented by PdfShadingType1-7.
container PdfObject // The container.
container PdfObject // The container. Can be stream, indirect object, or dictionary.
}
func (this *PdfShading) GetContainingPdfObject() PdfObject {
@ -58,6 +58,8 @@ func (this *PdfShading) getShadingDict() (*PdfObjectDictionary, error) {
return d, nil
} else if streamObj, isStream := obj.(*PdfObjectStream); isStream {
return streamObj.PdfObjectDictionary, nil
} else if d, isDict := obj.(*PdfObjectDictionary); isDict {
return d, nil
} else {
common.Log.Debug("Unable to access shading dictionary")
return nil, ErrTypeError
@ -150,6 +152,9 @@ func newPdfShadingFromPdfObject(obj PdfObject) (*PdfShading, error) {
} else if streamObj, isStream := obj.(*PdfObjectStream); isStream {
shading.container = streamObj
dict = streamObj.PdfObjectDictionary
} else if d, isDict := obj.(*PdfObjectDictionary); isDict {
shading.container = d
dict = d
} else {
common.Log.Debug("Object type unexpected (%T)", obj)
return nil, ErrTypeError

View File

@ -145,17 +145,25 @@ func (xform *XObjectForm) GetContentStream() ([]byte, error) {
return decoded, nil
}
// Update the content stream, encode if needed.
func (xform *XObjectForm) SetContentStream(content []byte) error {
// Update the content stream with specified encoding. If encoding is null, will use the xform.Filter object
// or Raw encoding if not set.
func (xform *XObjectForm) SetContentStream(content []byte, encoder StreamEncoder) error {
encoded := content
if xform.Filter != nil {
enc, err := xform.Filter.EncodeBytes(encoded)
if err != nil {
return err
if encoder == nil {
if xform.Filter != nil {
encoder = xform.Filter
} else {
encoder = NewRawEncoder()
}
encoded = enc
}
enc, err := encoder.EncodeBytes(encoded)
if err != nil {
return err
}
encoded = enc
xform.Stream = encoded
return nil