From 80c33cc810fdc7e6673b7e12c08df84be14e1acf Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Wed, 12 Apr 2017 11:21:32 +0000 Subject: [PATCH 1/4] Fixes in inline image parsing and outputting. --- pdf/contentstream/inline-image.go | 43 ++++++++++++++++++++----------- pdf/model/page.go | 4 ++- 2 files changed, 31 insertions(+), 16 deletions(-) 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 c0f7cad9..83609e03 100644 --- a/pdf/model/page.go +++ b/pdf/model/page.go @@ -1053,8 +1053,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") } From 329700417413596cbf6ce5f8730f5b4d8a4728d4 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Wed, 12 Apr 2017 12:31:05 +0000 Subject: [PATCH 2/4] Create rectangle annotation with appearance stream --- pdf/annotator/annotator.go | 64 ++++++++++++++++++++++-- pdf/annotator/rectangle.go | 99 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 3 deletions(-) create mode 100644 pdf/annotator/rectangle.go diff --git a/pdf/annotator/annotator.go b/pdf/annotator/annotator.go index 83603f55..30e91684 100644 --- a/pdf/annotator/annotator.go +++ b/pdf/annotator/annotator.go @@ -5,6 +5,7 @@ import ( pdf "github.com/unidoc/unidoc/pdf/model" ) +// The currently supported line ending styles are None, Arrow (ClosedArrow) and Butt. type LineEndingStyle int const ( @@ -13,16 +14,18 @@ const ( 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 + Opacity float64 // Alpha value (0-1). LineWidth float64 - LineEndingStyle1 LineEndingStyle - LineEndingStyle2 LineEndingStyle + 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. @@ -69,3 +72,58 @@ func CreateLineAnnotation(lineDef LineAnnotationDef) (*pdf.PdfAnnotation, error) 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 { +} diff --git a/pdf/annotator/rectangle.go b/pdf/annotator/rectangle.go new file mode 100644 index 00000000..1f275f2e --- /dev/null +++ b/pdf/annotator/rectangle.go @@ -0,0 +1,99 @@ +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) + creator.Add_B() // fill and 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 +} From 89b36f338b37f37f1ba12e857833698e2d8cef1b Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Wed, 12 Apr 2017 21:37:20 +0000 Subject: [PATCH 3/4] Annotations support for rectangle and ellipses with appearance stream in annotator. --- pdf/annotator/annotator.go | 51 ++++++++ pdf/annotator/circle.go | 137 ++++++++++++++++++++++ pdf/annotator/line.go | 5 + pdf/annotator/rectangle.go | 5 + pdf/contentstream/draw/bezier_curve.go | 154 +++++++++++++++++++++++++ pdf/contentstream/draw/path.go | 37 +----- pdf/contentstream/draw/point.go | 37 ++++++ pdf/contentstream/draw/vector.go | 5 + 8 files changed, 400 insertions(+), 31 deletions(-) create mode 100644 pdf/annotator/circle.go create mode 100644 pdf/contentstream/draw/bezier_curve.go create mode 100644 pdf/contentstream/draw/point.go diff --git a/pdf/annotator/annotator.go b/pdf/annotator/annotator.go index 30e91684..ea429539 100644 --- a/pdf/annotator/annotator.go +++ b/pdf/annotator/annotator.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package annotator import ( @@ -126,4 +131,50 @@ func CreateRectangleAnnotation(rectDef RectangleAnnotationDef) (*pdf.PdfAnnotati } type CircleAnnotationDef struct { + X float64 + Y float64 + Width float64 + Height float64 + FillEnabled bool // Show fill? + FillColor *pdf.PdfColorDeviceRGB + BorderEnabled bool // Show border? + BorderWidth float64 + BorderColor *pdf.PdfColorDeviceRGB + Opacity float64 // Alpha value (0-1). +} + +// Creates a circle/ellipse annotation object with appearance stream that can be added to page PDF annotations. +func CreateCircleAnnotation(circDef CircleAnnotationDef) (*pdf.PdfAnnotation, error) { + circAnnotation := pdf.NewPdfAnnotationCircle() + + if circDef.BorderEnabled { + r, g, b := circDef.BorderColor.R(), circDef.BorderColor.G(), circDef.BorderColor.B() + circAnnotation.C = pdfcore.MakeArrayFromFloats([]float64{r, g, b}) + bs := pdf.NewBorderStyle() + bs.SetBorderWidth(circDef.BorderWidth) + circAnnotation.BS = bs.ToPdfObject() + } + + if circDef.FillEnabled { + r, g, b := circDef.FillColor.R(), circDef.FillColor.G(), circDef.FillColor.B() + circAnnotation.IC = pdfcore.MakeArrayFromFloats([]float64{r, g, b}) + } else { + circAnnotation.IC = pdfcore.MakeArrayFromIntegers([]int{}) // No fill. + } + + if circDef.Opacity < 1.0 { + circAnnotation.CA = pdfcore.MakeFloat(circDef.Opacity) + } + + // Make the appearance stream (for uniform appearance). + apDict, bbox, err := makeCircleAnnotationAppearanceStream(circDef) + if err != nil { + return nil, err + } + + circAnnotation.AP = apDict + circAnnotation.Rect = pdfcore.MakeArrayFromFloats([]float64{bbox.Llx, bbox.Lly, bbox.Urx, bbox.Ury}) + + return circAnnotation.PdfAnnotation, nil + } diff --git a/pdf/annotator/circle.go b/pdf/annotator/circle.go new file mode 100644 index 00000000..3b893d80 --- /dev/null +++ b/pdf/annotator/circle.go @@ -0,0 +1,137 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package annotator + +import ( + "github.com/unidoc/unidoc/common" + + pdfcontent "github.com/unidoc/unidoc/pdf/contentstream" + "github.com/unidoc/unidoc/pdf/contentstream/draw" + pdfcore "github.com/unidoc/unidoc/pdf/core" + pdf "github.com/unidoc/unidoc/pdf/model" +) + +// Make the bezier path with the content creator. +func drawBezierPathWithCreator(bpath draw.CubicBezierPath, creator *pdfcontent.ContentCreator) { + for idx, c := range bpath.Curves { + if idx == 0 { + creator.Add_m(c.P0.X, c.P0.Y) + } + creator.Add_c(c.P1.X, c.P1.Y, c.P2.X, c.P2.Y, c.P3.X, c.P3.Y) + } +} + +func makeCircleAnnotationAppearanceStream(circDef CircleAnnotationDef) (*pdfcore.PdfObjectDictionary, *pdf.PdfRectangle, error) { + form := pdf.NewXObjectForm() + form.Resources = pdf.NewPdfPageResources() + + gsName := "" + if circDef.Opacity < 1.0 { + // Create graphics state with right opacity. + gsState := &pdfcore.PdfObjectDictionary{} + gsState.Set("ca", pdfcore.MakeFloat(circDef.Opacity)) + gsState.Set("CA", pdfcore.MakeFloat(circDef.Opacity)) + err := form.Resources.AddExtGState("gs1", gsState) + if err != nil { + common.Log.Debug("Unable to add extgstate gs1") + return nil, nil, err + } + + gsName = "gs1" + } + + content, localBbox, globalBbox, err := drawPdfCircle(circDef, gsName) + if err != nil { + return nil, nil, err + } + + err = form.SetContentStream(content, nil) + if err != nil { + return nil, nil, err + } + + // Local bounding box for the XObject Form. + form.BBox = localBbox.ToPdfObject() + + apDict := &pdfcore.PdfObjectDictionary{} + apDict.Set("N", form.ToPdfObject()) + + return apDict, globalBbox, nil +} + +func drawPdfCircle(circDef CircleAnnotationDef, gsName string) ([]byte, *pdf.PdfRectangle, *pdf.PdfRectangle, error) { + xRad := circDef.Width / 2 + yRad := circDef.Height / 2 + if circDef.BorderEnabled { + xRad -= circDef.BorderWidth / 2 + yRad -= circDef.BorderWidth / 2 + } + + magic := 0.551784 + xMagic := xRad * magic + yMagic := yRad * magic + + bpath := draw.NewCubicBezierPath() + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(-xRad, 0, -xRad, yMagic, -xMagic, yRad, 0, yRad)) + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(0, yRad, xMagic, yRad, xRad, yMagic, xRad, 0)) + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(xRad, 0, xRad, -yMagic, xMagic, -yRad, 0, -yRad)) + bpath = bpath.AppendCurve(draw.NewCubicBezierCurve(0, -yRad, -xMagic, -yRad, -xRad, -yMagic, -xRad, 0)) + bpath = bpath.Offset(xRad, yRad) + if circDef.BorderEnabled { + bpath = bpath.Offset(circDef.BorderWidth/2, circDef.BorderWidth/2) + } + + creator := pdfcontent.NewContentCreator() + + creator.Add_q() + + if circDef.FillEnabled { + creator.Add_rg(circDef.FillColor.R(), circDef.FillColor.G(), circDef.FillColor.B()) + } + if circDef.BorderEnabled { + creator.Add_RG(circDef.BorderColor.R(), circDef.BorderColor.G(), circDef.BorderColor.B()) + creator.Add_w(circDef.BorderWidth) + } + if len(gsName) > 1 { + // If a graphics state is provided, use it. (Used for transparency settings here). + creator.Add_gs(pdfcore.PdfObjectName(gsName)) + } + + drawBezierPathWithCreator(bpath, creator) + + creator.Add_B() // fill and stroke. + creator.Add_Q() + + // Offsets (needed for placement of annotations bbox). + offX := circDef.X + offY := circDef.Y + + // Get bounding box. + pathBbox := bpath.GetBoundingBox() + if circDef.BorderEnabled { + // Account for stroke width. + pathBbox.Height += circDef.BorderWidth + pathBbox.Width += circDef.BorderWidth + pathBbox.X -= circDef.BorderWidth / 2 + pathBbox.Y -= circDef.BorderWidth / 2 + } + + // Bounding box - local coordinate system (without offset). + localBbox := &pdf.PdfRectangle{} + localBbox.Llx = pathBbox.X + localBbox.Lly = pathBbox.Y + localBbox.Urx = pathBbox.X + pathBbox.Width + localBbox.Ury = pathBbox.Y + pathBbox.Height + + // Bounding box - global page coordinate system (with offset). + globalBbox := &pdf.PdfRectangle{} + globalBbox.Llx = offX + pathBbox.X + globalBbox.Lly = offY + pathBbox.Y + globalBbox.Urx = offX + pathBbox.X + pathBbox.Width + globalBbox.Ury = offY + pathBbox.Y + pathBbox.Height + + return creator.Bytes(), localBbox, globalBbox, nil +} diff --git a/pdf/annotator/line.go b/pdf/annotator/line.go index 888537b9..fc9a9b77 100644 --- a/pdf/annotator/line.go +++ b/pdf/annotator/line.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package annotator import ( diff --git a/pdf/annotator/rectangle.go b/pdf/annotator/rectangle.go index 1f275f2e..e23e4224 100644 --- a/pdf/annotator/rectangle.go +++ b/pdf/annotator/rectangle.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package annotator import ( diff --git a/pdf/contentstream/draw/bezier_curve.go b/pdf/contentstream/draw/bezier_curve.go new file mode 100644 index 00000000..a32f89d6 --- /dev/null +++ b/pdf/contentstream/draw/bezier_curve.go @@ -0,0 +1,154 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package draw + +import ( + "math" + + "github.com/unidoc/unidoc/pdf/model" +) + +// Cubic bezier curves are defined by: +// R(t) = P0*(1-t)^3 + P1*3*t*(1-t)^2 + P2*3*t^2*(1-t) + P3*t^3 +// where P0 is the current point, P1, P2 control points and P3 the final point. +type CubicBezierCurve struct { + P0 Point // Starting point. + P1 Point // Control point 1. + P2 Point // Control point 2. + P3 Point // Final point. +} + +func NewCubicBezierCurve(x0, y0, x1, y1, x2, y2, x3, y3 float64) CubicBezierCurve { + curve := CubicBezierCurve{} + curve.P0 = NewPoint(x0, y0) + curve.P1 = NewPoint(x1, y1) + curve.P2 = NewPoint(x2, y2) + curve.P3 = NewPoint(x3, y3) + return curve +} + +// Add X,Y offset to all points on a curve. +func (curve CubicBezierCurve) AddOffsetXY(offX, offY float64) CubicBezierCurve { + curve.P0.X += offX + curve.P1.X += offX + curve.P2.X += offX + curve.P3.X += offX + + curve.P0.Y += offY + curve.P1.Y += offY + curve.P2.Y += offY + curve.P3.Y += offY + + return curve +} + +func (curve CubicBezierCurve) GetBounds() model.PdfRectangle { + minX := curve.P0.X + maxX := curve.P0.X + minY := curve.P0.Y + maxY := curve.P0.Y + + // 1000 points. + for t := 0.0; t <= 1.0; t += 0.001 { + Rx := curve.P0.X*math.Pow(1-t, 3) + + curve.P1.X*3*t*math.Pow(1-t, 2) + + curve.P2.X*3*math.Pow(t, 2)*(1-t) + + curve.P3.X*math.Pow(t, 3) + Ry := curve.P0.Y*math.Pow(1-t, 3) + + curve.P1.Y*3*t*math.Pow(1-t, 2) + + curve.P2.Y*3*math.Pow(t, 2)*(1-t) + + curve.P3.Y*math.Pow(t, 3) + + if Rx < minX { + minX = Rx + } + if Rx > maxX { + maxX = Rx + } + if Ry < minY { + minY = Ry + } + if Ry > maxY { + maxY = Ry + } + } + + bounds := model.PdfRectangle{} + bounds.Llx = minX + bounds.Lly = minY + bounds.Urx = maxX + bounds.Ury = maxY + return bounds +} + +type CubicBezierPath struct { + Curves []CubicBezierCurve +} + +func NewCubicBezierPath() CubicBezierPath { + bpath := CubicBezierPath{} + bpath.Curves = []CubicBezierCurve{} + return bpath +} + +func (this CubicBezierPath) AppendCurve(curve CubicBezierCurve) CubicBezierPath { + this.Curves = append(this.Curves, curve) + return this +} + +func (bpath CubicBezierPath) Copy() CubicBezierPath { + bpathcopy := CubicBezierPath{} + bpathcopy.Curves = []CubicBezierCurve{} + for _, c := range bpath.Curves { + bpathcopy.Curves = append(bpathcopy.Curves, c) + } + return bpathcopy +} + +func (bpath CubicBezierPath) Offset(offX, offY float64) CubicBezierPath { + for i, c := range bpath.Curves { + bpath.Curves[i] = c.AddOffsetXY(offX, offY) + } + return bpath +} + +func (bpath CubicBezierPath) GetBoundingBox() Rectangle { + bbox := Rectangle{} + + minX := 0.0 + maxX := 0.0 + minY := 0.0 + maxY := 0.0 + for idx, c := range bpath.Curves { + curveBounds := c.GetBounds() + if idx == 0 { + minX = curveBounds.Llx + maxX = curveBounds.Urx + minY = curveBounds.Lly + maxY = curveBounds.Ury + continue + } + + if curveBounds.Llx < minX { + minX = curveBounds.Llx + } + if curveBounds.Urx > maxX { + maxX = curveBounds.Urx + } + if curveBounds.Lly < minY { + minY = curveBounds.Lly + } + if curveBounds.Ury > maxY { + maxY = curveBounds.Ury + } + } + + bbox.X = minX + bbox.Y = minY + bbox.Width = maxX - minX + bbox.Height = maxY - minY + return bbox +} diff --git a/pdf/contentstream/draw/path.go b/pdf/contentstream/draw/path.go index 553ba72e..1246b6e8 100644 --- a/pdf/contentstream/draw/path.go +++ b/pdf/contentstream/draw/path.go @@ -1,29 +1,11 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package draw -import "fmt" - -type Point struct { - X float64 - Y float64 -} - -func (p Point) Add(dx, dy float64) Point { - p.X += dx - p.Y += dy - return p -} - -// Add vector to a point. -func (this Point) AddVector(v Vector) Point { - this.X += v.Dx - this.Y += v.Dy - return this -} - -func (p Point) String() string { - return fmt.Sprintf("(%.1f,%.1f)", p.X, p.Y) -} - +// A path consists of straight line connections between each point defined in an array of points. type Path struct { Points []Point } @@ -34,13 +16,6 @@ func NewPath() Path { return path } -func NewPoint(x, y float64) Point { - point := Point{} - point.X = x - point.Y = y - return point -} - func (this Path) AppendPoint(point Point) Path { this.Points = append(this.Points, point) return this diff --git a/pdf/contentstream/draw/point.go b/pdf/contentstream/draw/point.go new file mode 100644 index 00000000..eb5265c3 --- /dev/null +++ b/pdf/contentstream/draw/point.go @@ -0,0 +1,37 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package draw + +import "fmt" + +type Point struct { + X float64 + Y float64 +} + +func NewPoint(x, y float64) Point { + point := Point{} + point.X = x + point.Y = y + return point +} + +func (p Point) Add(dx, dy float64) Point { + p.X += dx + p.Y += dy + return p +} + +// Add vector to a point. +func (this Point) AddVector(v Vector) Point { + this.X += v.Dx + this.Y += v.Dy + return this +} + +func (p Point) String() string { + return fmt.Sprintf("(%.1f,%.1f)", p.X, p.Y) +} diff --git a/pdf/contentstream/draw/vector.go b/pdf/contentstream/draw/vector.go index 5d05561b..b8afc699 100644 --- a/pdf/contentstream/draw/vector.go +++ b/pdf/contentstream/draw/vector.go @@ -1,3 +1,8 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + package draw import "math" From 82d91844d1ecb3bcde2c088f6738c29979c0f0f9 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Wed, 12 Apr 2017 22:26:12 +0000 Subject: [PATCH 4/4] annotation fixes for circle, rectangle (fill, stroke) --- pdf/annotator/circle.go | 8 +++++++- pdf/annotator/rectangle.go | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pdf/annotator/circle.go b/pdf/annotator/circle.go index 3b893d80..53863311 100644 --- a/pdf/annotator/circle.go +++ b/pdf/annotator/circle.go @@ -102,7 +102,13 @@ func drawPdfCircle(circDef CircleAnnotationDef, gsName string) ([]byte, *pdf.Pdf drawBezierPathWithCreator(bpath, creator) - creator.Add_B() // fill and stroke. + 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). diff --git a/pdf/annotator/rectangle.go b/pdf/annotator/rectangle.go index e23e4224..e7aee4f2 100644 --- a/pdf/annotator/rectangle.go +++ b/pdf/annotator/rectangle.go @@ -76,7 +76,14 @@ func drawPdfRectangle(rectDef RectangleAnnotationDef, gsName string) ([]byte, *p creator.Add_gs(pdfcore.PdfObjectName(gsName)) } drawPathWithCreator(path, creator) - creator.Add_B() // fill and stroke. + + 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).