diff --git a/pdf/annotator/annotator.go b/pdf/annotator/annotator.go new file mode 100644 index 00000000..ea429539 --- /dev/null +++ b/pdf/annotator/annotator.go @@ -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 + +} diff --git a/pdf/annotator/circle.go b/pdf/annotator/circle.go new file mode 100644 index 00000000..53863311 --- /dev/null +++ b/pdf/annotator/circle.go @@ -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 +} diff --git a/pdf/annotator/doc.go b/pdf/annotator/doc.go new file mode 100644 index 00000000..7164964a --- /dev/null +++ b/pdf/annotator/doc.go @@ -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 diff --git a/pdf/annotator/line.go b/pdf/annotator/line.go new file mode 100644 index 00000000..fc9a9b77 --- /dev/null +++ b/pdf/annotator/line.go @@ -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 +} diff --git a/pdf/annotator/rectangle.go b/pdf/annotator/rectangle.go new file mode 100644 index 00000000..e7aee4f2 --- /dev/null +++ b/pdf/annotator/rectangle.go @@ -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 +} 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/doc.go b/pdf/contentstream/draw/doc.go new file mode 100644 index 00000000..81d4a950 --- /dev/null +++ b/pdf/contentstream/draw/doc.go @@ -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 diff --git a/pdf/contentstream/draw/path.go b/pdf/contentstream/draw/path.go new file mode 100644 index 00000000..1246b6e8 --- /dev/null +++ b/pdf/contentstream/draw/path.go @@ -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 +} 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 new file mode 100644 index 00000000..b8afc699 --- /dev/null +++ b/pdf/contentstream/draw/vector.go @@ -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) +} diff --git a/pdf/contentstream/inline-image.go b/pdf/contentstream/inline-image.go index b3adf9df..4ec74d28 100644 --- a/pdf/contentstream/inline-image.go +++ b/pdf/contentstream/inline-image.go @@ -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 "EI" was part of the data. im.stream = append(im.stream, skipBytes...) + skipBytes = []byte{} // Clear. state = 0 } } diff --git a/pdf/model/page.go b/pdf/model/page.go index 104ff907..0f1264ac 100644 --- a/pdf/model/page.go +++ b/pdf/model/page.go @@ -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") } diff --git a/pdf/model/shading.go b/pdf/model/shading.go index 55e01456..53478bca 100644 --- a/pdf/model/shading.go +++ b/pdf/model/shading.go @@ -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 diff --git a/pdf/model/xobject.go b/pdf/model/xobject.go index 92196694..44b40b63 100644 --- a/pdf/model/xobject.go +++ b/pdf/model/xobject.go @@ -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