From a07f25550d120cf65e0df54d867eef869d3e23d2 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 13:15:58 +0000 Subject: [PATCH 01/17] go vet fix --- pdf/creator/creator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf/creator/creator.go b/pdf/creator/creator.go index 417f7492..ba8edbe3 100644 --- a/pdf/creator/creator.go +++ b/pdf/creator/creator.go @@ -189,7 +189,7 @@ func (c *Creator) newPage() *model.PdfPage { width := c.pagesize[0] height := c.pagesize[1] - bbox := model.PdfRectangle{0, 0, width, height} + bbox := model.PdfRectangle{Llx: 0, Lly: 0, Urx: width, Ury: height} page.MediaBox = &bbox c.pageWidth = width From 6cefdf91cc90ca13f83d914135a11235b5668b82 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 13:49:55 +0000 Subject: [PATCH 02/17] Added Division drawable which is a container that can wrap over pages --- pdf/creator/division.go | 136 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 pdf/creator/division.go diff --git a/pdf/creator/division.go b/pdf/creator/division.go new file mode 100644 index 00000000..b9a9691a --- /dev/null +++ b/pdf/creator/division.go @@ -0,0 +1,136 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package creator + +import ( + "errors" + + "github.com/unidoc/unidoc/common" +) + +// Division is a container component which can wrap across multiple pages (unlike Block). +// It can contain multiple Drawable components (currently supporting Paragraph and Image). +// +// The component stacking behavior is vertical, where the Drawables are drawn on top of each other. +// TODO: Add inline mode (horizontal stacking). +type Division struct { + components []VectorDrawable + + // Positioning: relative / absolute. + positioning positioning + + // Margins to be applied around the block when drawing on Page. + margins margins +} + +// NewDivision returns a new Division container component. +func NewDivision() *Division { + div := &Division{} + div.components = []VectorDrawable{} + return div +} + +// Add adds a VectorDrawable to the Division container. +// Currently supported VectorDrawables: *Paragraph, *Image. +func (div *Division) Add(d VectorDrawable) error { + supported := false + + switch d.(type) { + case *Paragraph: + supported = true + case *Image: + supported = true + } + + if !supported { + return errors.New("Unsupported type in Division") + } + + div.components = append(div.components, d) + + return nil +} + +// Height returns the height for the Division component assuming all stacked on top of each other. +func (div *Division) Height() float64 { + y := 0.0 + yMax := 0.0 + for _, component := range div.components { + compWidth, compHeight := component.Width(), component.Height() + switch t := component.(type) { + case *Paragraph: + p := t + compWidth += p.margins.left + p.margins.right + compHeight += p.margins.top + p.margins.bottom + } + + // Vertical stacking. + y += compHeight + yMax = y + } + + return yMax +} + +// Width is not used. Not used as a Division element is designed to fill into available width depending on +// context. Returns 0. +func (div *Division) Width() float64 { + return 0 +} + +// GeneratePageBlocks generates the page blocks for the Division component. +// Multiple blocks are generated if the contents wrap over multiple pages. +func (div *Division) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { + pageblocks := []*Block{} + + origCtx := ctx + + if div.positioning.isRelative() { + // Update context. + ctx.X += div.margins.left + ctx.Y += div.margins.top + ctx.Width -= div.margins.left + div.margins.right + ctx.Height -= div.margins.top + } + + // Draw. + for _, component := range div.components { + newblocks, updCtx, err := component.GeneratePageBlocks(ctx) + if err != nil { + common.Log.Debug("Error generating page blocks: %v", err) + return nil, ctx, err + } + + if len(newblocks) < 1 { + continue + } + + if len(pageblocks) > 0 { + // If there are pageblocks already in place. + // merge the first block in with current Block and append the rest. + pageblocks[len(pageblocks)-1].mergeBlocks(newblocks[0]) + pageblocks = append(pageblocks, newblocks[1:]...) + } else { + pageblocks = append(pageblocks, newblocks[0:]...) + } + + // Apply padding/margins. + updCtx.X = ctx.X + ctx = updCtx + } + + if div.positioning.isRelative() { + // Move back X to same start of line. + ctx.X = origCtx.X + } + + if div.positioning.isAbsolute() { + // If absolute: return original context. + return pageblocks, origCtx, nil + } + + return pageblocks, ctx, nil +} From 30f0960392d3b3e3885cc118ab6adc1241cf50c5 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 13:50:17 +0000 Subject: [PATCH 03/17] godoc enhancements --- pdf/creator/block.go | 17 +++++++++-------- pdf/creator/drawable.go | 6 +++++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pdf/creator/block.go b/pdf/creator/block.go index 53106818..125f5b14 100644 --- a/pdf/creator/block.go +++ b/pdf/creator/block.go @@ -96,7 +96,7 @@ func (blk *Block) SetAngle(angleDeg float64) { blk.angle = angleDeg } -// Duplicate the block with a new copy of the operations list. +// duplicate duplicates the block with a new copy of the operations list. func (blk *Block) duplicate() *Block { dup := &Block{} @@ -167,7 +167,7 @@ func (blk *Block) Width() float64 { return blk.width } -// Add contents to a block. Wrap both existing and new contents to ensure +// addContents adds contents to a block. Wrap both existing and new contents to ensure // independence of content operations. func (blk *Block) addContents(operations *contentstream.ContentStreamOperations) { blk.contents.WrapIfNeeded() @@ -175,7 +175,7 @@ func (blk *Block) addContents(operations *contentstream.ContentStreamOperations) *blk.contents = append(*blk.contents, *operations...) } -// Add contents to a block by contents string. +// addContentsByString adds contents to a block by contents string. func (blk *Block) addContentsByString(contents string) error { cc := contentstream.NewContentStreamParser(contents) operations, err := cc.Parse() @@ -235,7 +235,7 @@ func (blk *Block) ScaleToHeight(h float64) { blk.Scale(ratio, ratio) } -// Internal function to apply translation to the block, moving block contents on the PDF. +// translate translates the block, moving block contents on the PDF. For internal use. func (blk *Block) translate(tx, ty float64) { ops := contentstream.NewContentCreator(). Translate(tx, -ty). @@ -245,7 +245,8 @@ func (blk *Block) translate(tx, ty float64) { blk.contents.WrapIfNeeded() } -// Draw the block on a Page. +// drawToPage draws the block on a PdfPage. Generates the content streams and appends to the PdfPage's content +// stream and links needed resources. func (blk *Block) drawToPage(page *model.PdfPage) error { // Check if Page contents are wrapped - if not wrap it. content, err := page.GetAllContentStreams() @@ -279,7 +280,7 @@ func (blk *Block) drawToPage(page *model.PdfPage) error { return nil } -// Draw the drawable d on the block. +// Draw draws the drawable d on the block. // Note that the drawable must not wrap, i.e. only return one block. Otherwise an error is returned. func (blk *Block) Draw(d Drawable) error { ctx := DrawContext{} @@ -330,13 +331,13 @@ func (blk *Block) DrawWithContext(d Drawable, ctx DrawContext) error { return nil } -// Append another block onto the block. +// mergeBlocks appends another block onto the block. func (blk *Block) mergeBlocks(toAdd *Block) error { err := mergeContents(blk.contents, blk.resources, toAdd.contents, toAdd.resources) return err } -// Merge contents and content streams. +// mergeContents merges contents and content streams. // Active in the sense that it modified the input contents and resources. func mergeContents(contents *contentstream.ContentStreamOperations, resources *model.PdfPageResources, contentsToAdd *contentstream.ContentStreamOperations, resourcesToAdd *model.PdfPageResources) error { diff --git a/pdf/creator/drawable.go b/pdf/creator/drawable.go index fed0ec18..c556088e 100644 --- a/pdf/creator/drawable.go +++ b/pdf/creator/drawable.go @@ -16,7 +16,11 @@ type Drawable interface { // VectorDrawable is a Drawable with a specified width and height. type VectorDrawable interface { Drawable + + // Width returns the width of the Drawable. Width() float64 + + // Height returns the height of the Drawable. Height() float64 } @@ -30,7 +34,7 @@ type DrawContext struct { // Current position. In a relative positioning mode, a drawable will be placed at these coordinates. X, Y float64 - // Context dimensions. Available width and height. + // Context dimensions. Available width and height (on current page). Width, Height float64 // Page Margins. From 6a3348ebd76339a5c0a98c339641247422678acc Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 13:50:35 +0000 Subject: [PATCH 04/17] Fix wrapping for block in relative mode --- pdf/creator/block.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pdf/creator/block.go b/pdf/creator/block.go index 125f5b14..f6db7a38 100644 --- a/pdf/creator/block.go +++ b/pdf/creator/block.go @@ -130,6 +130,7 @@ func (blk *Block) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, er cc.Translate(0, -blk.Height()) } contents := append(*cc.Operations(), *dup.contents...) + contents.WrapIfNeeded() dup.contents = &contents blocks = append(blocks, dup) From 38505ef0202192d4aa66243bcecf8746a27530d5 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 13:51:41 +0000 Subject: [PATCH 05/17] Support for Division in table cells --- pdf/creator/table.go | 53 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/pdf/creator/table.go b/pdf/creator/table.go index 3aabb638..73c233a1 100644 --- a/pdf/creator/table.go +++ b/pdf/creator/table.go @@ -196,7 +196,9 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, } // For text: Calculate width, height, wrapping within available space if specified. - if p, isp := cell.content.(*Paragraph); isp { + switch t := cell.content.(type) { + case *Paragraph: + p := t if p.enableWrap { p.SetWidth(w - cell.indent) } @@ -208,7 +210,50 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, // Add diff to last row table.rowHeights[cell.row+cell.rowspan-2] += diffh } + case *Image: + img := t + newh := img.Height() + img.margins.top + img.margins.bottom + if newh > h { + diffh := newh - h + // Add diff to last row + table.rowHeights[cell.row+cell.rowspan-2] += diffh + } + case *Division: + div := t + + ctx := DrawContext{ + X: xrel, + Y: yrel, + Width: w, + } + + // Mock call to generate page blocks. + divBlocks, updCtx, err := div.GeneratePageBlocks(ctx) + if err != nil { + return nil, ctx, err + } + + if len(divBlocks) > 1 { + // Wraps across page, make cell reach all the way to bottom of current page. + newh := ctx.Height - h + if newh > h { + diffh := newh - h + // Add diff to last row + table.rowHeights[cell.row+cell.rowspan-2] += diffh + } + } + + newh := div.Height() + div.margins.top + div.margins.bottom + _ = updCtx + + // Get available width and height. + if newh > h { + diffh := newh - h + // Add diff to last row + table.rowHeights[cell.row+cell.rowspan-2] += diffh + } } + } // Draw cells. @@ -240,6 +285,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, } ctx.Height = origHeight - yrel + if h > ctx.Height { // Go to next page. blocks = append(blocks, block) @@ -545,7 +591,6 @@ func (cell *TableCell) Width(ctx DrawContext) float64 { // SetContent sets the cell's content. The content is a VectorDrawable, i.e. a Drawable with a known height and width. // The currently supported VectorDrawable is: *Paragraph. -// TODO: Add support for *Image, *Block. func (cell *TableCell) SetContent(vd VectorDrawable) error { switch t := vd.(type) { case *Paragraph: @@ -554,6 +599,10 @@ func (cell *TableCell) SetContent(vd VectorDrawable) error { t.enableWrap = false // No wrapping. } + cell.content = vd + case *Image: + cell.content = vd + case *Division: cell.content = vd default: common.Log.Debug("Error: unsupported cell content type %T\n", vd) From 9bafe454f6cca909aac637333334629d421a2b17 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 13:52:28 +0000 Subject: [PATCH 06/17] Test case for table cells with multiple paragraphs inside divs and image --- pdf/creator/table_test.go | 230 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 pdf/creator/table_test.go diff --git a/pdf/creator/table_test.go b/pdf/creator/table_test.go new file mode 100644 index 00000000..12c76b41 --- /dev/null +++ b/pdf/creator/table_test.go @@ -0,0 +1,230 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package creator + +import ( + "fmt" + "io/ioutil" + "testing" + + "github.com/unidoc/unidoc/pdf/model/fonts" +) + +func TestTableMultiParagraphWrapped(t *testing.T) { + c := New() + + pageHistoryTable := NewTable(4) + pageHistoryTable.SetColumnWidths(0.1, 0.6, 0.15, 0.15) + content := [][]string{ + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + } + fontHelvetica := fonts.NewFontHelvetica() + for _, rows := range content { + for _, txt := range rows { + p := NewParagraph(txt) + p.SetFontSize(12) + p.SetFont(fontHelvetica) + p.SetColor(ColorBlack) + p.SetEnableWrap(true) + + cell := pageHistoryTable.NewCell() + cell.SetBorder(CellBorderStyleBox, 1) + cell.SetContent(p) + } + } + + err := c.Draw(pageHistoryTable) + if err != nil { + t.Fatalf("Error drawing: %v", err) + } + + err = c.WriteToFile("/tmp/table_pagehist.pdf") + if err != nil { + t.Fatalf("Fail: %v\n", err) + } +} + +func TestTableWithImage(t *testing.T) { + c := New() + + pageHistoryTable := NewTable(4) + pageHistoryTable.SetColumnWidths(0.1, 0.6, 0.15, 0.15) + content := [][]string{ + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + } + fontHelvetica := fonts.NewFontHelvetica() + for _, rows := range content { + for _, txt := range rows { + p := NewParagraph(txt) + p.SetFontSize(12) + p.SetFont(fontHelvetica) + p.SetColor(ColorBlack) + p.SetEnableWrap(true) + + cell := pageHistoryTable.NewCell() + cell.SetBorder(CellBorderStyleBox, 1) + err := cell.SetContent(p) + if err != nil { + t.Fatalf("Error: %v", err) + } + } + } + + pageHistoryTable.SkipCells(1) + + // Add image + imgData, err := ioutil.ReadFile(testImageFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + img, err := NewImageFromData(imgData) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + img.margins.top = 2.0 + img.margins.bottom = 2.0 + img.margins.left = 2.0 + img.margins.bottom = 2.0 + img.ScaleToWidth(0.3 * c.Width()) + fmt.Printf("Scaling image to width: %v\n", 0.5*c.Width()) + + cell := pageHistoryTable.NewCell() + cell.SetContent(img) + cell.SetBorder(CellBorderStyleBox, 1) + + err = c.Draw(pageHistoryTable) + if err != nil { + t.Fatalf("Error drawing: %v", err) + } + + err = c.WriteToFile("/tmp/table_pagehist_with_img.pdf") + if err != nil { + t.Fatalf("Fail: %v\n", err) + } +} + +func TestTableWithDiv(t *testing.T) { + c := New() + + pageHistoryTable := NewTable(4) + pageHistoryTable.SetColumnWidths(0.1, 0.6, 0.15, 0.15) + + headings := []string{ + "", "Description", "Passing", "Total", + } + content := [][]string{ + {"1", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"2", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"3", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"3", "FullText Search Highlight the Term in Results. Going hunting in the winter can be fruitful, especially if it has not been too cold and the deer are well fed. \n\nissues 60", "120 90 30", "130 1"}, + {"4", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"5", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"6", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130 a b c d e f g"}, + {"7", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"8", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130 gogogoggogoogogo"}, + {"9", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"10", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"11", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"12", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"13", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"14", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"15", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"16", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"17", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"18", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"19", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + {"20", "FullText Search Highlight the Term in Results \n\nissues 60", "120", "130"}, + } + fontHelvetica := fonts.NewFontHelvetica() + fontHelveticaBold := fonts.NewFontHelveticaBold() + for _, rows := range content { + for colIdx, txt := range rows { + p := NewParagraph(txt) + p.SetFontSize(12) + p.SetFont(fontHelvetica) + p.SetColor(ColorBlack) + p.SetMargins(0, 5, 10.0, 10.0) + if len(txt) > 10 { + p.SetTextAlignment(TextAlignmentJustify) + } else { + p.SetTextAlignment(TextAlignmentCenter) + } + + // Place cell contents (header and text) inside a div. + div := NewDivision() + + if len(headings[colIdx]) > 0 { + heading := NewParagraph(headings[colIdx]) + heading.SetFontSize(14) + heading.SetFont(fontHelveticaBold) + heading.SetColor(ColorRed) + heading.SetTextAlignment(TextAlignmentCenter) + err := div.Add(heading) + if err != nil { + t.Fatalf("Error: %v", err) + } + } + + err := div.Add(p) + if err != nil { + t.Fatalf("Error: %v", err) + } + + cell := pageHistoryTable.NewCell() + cell.SetBorder(CellBorderStyleBox, 1) + err = cell.SetContent(div) + if err != nil { + t.Fatalf("Error: %v", err) + } + } + } + + pageHistoryTable.SkipCells(1) + + // Add image + imgData, err := ioutil.ReadFile(testImageFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + img, err := NewImageFromData(imgData) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + img.margins.top = 2.0 + img.margins.bottom = 2.0 + img.margins.left = 2.0 + img.margins.bottom = 2.0 + img.ScaleToWidth(0.2 * c.Width()) + fmt.Printf("Scaling image to width: %v\n", 0.5*c.Width()) + + cell := pageHistoryTable.NewCell() + cell.SetContent(img) + cell.SetBorder(CellBorderStyleBox, 1) + + err = c.Draw(pageHistoryTable) + if err != nil { + t.Fatalf("Error drawing: %v", err) + } + + err = c.WriteToFile("/tmp/table_pagehist_with_div.pdf") + if err != nil { + t.Fatalf("Fail: %v\n", err) + } +} From 446bf4fe8dc750a6309620add9165f0c469ca1af Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 13:52:35 +0000 Subject: [PATCH 07/17] godoc --- pdf/creator/paragraph.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf/creator/paragraph.go b/pdf/creator/paragraph.go index cb83674b..70ec4af8 100644 --- a/pdf/creator/paragraph.go +++ b/pdf/creator/paragraph.go @@ -209,7 +209,7 @@ func (p *Paragraph) Height() float64 { return h } -// Calculate the text width (if not wrapped). +// getTextWidth calculates the text width as if all in one line (not taking wrapping into account). func (p *Paragraph) getTextWidth() float64 { w := float64(0.0) From 2c6c5b8fc84d86985a09d0be14eb9b8c068f609d Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 14:22:05 +0000 Subject: [PATCH 08/17] Jenkinsfile fix --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index c8d9eb1c..93d0fba2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -10,7 +10,7 @@ node { sh 'go version' stage('Checkout') { - git url: 'https://github.com/unidoc/unidoc.git' + checkout scm } stage('Prepare') { From 749a238c776cdeb83efaf0fc065bcc3441326fd9 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 26 Jul 2018 14:57:57 +0000 Subject: [PATCH 09/17] golint --- pdf/creator/filled_curve.go | 48 ++++++++++++++++++------------------- pdf/creator/image.go | 5 ++-- pdf/creator/paragraph.go | 8 +++---- pdf/creator/table.go | 14 +++++------ 4 files changed, 36 insertions(+), 39 deletions(-) diff --git a/pdf/creator/filled_curve.go b/pdf/creator/filled_curve.go index 5714ff7e..1b5e1a25 100644 --- a/pdf/creator/filled_curve.go +++ b/pdf/creator/filled_curve.go @@ -30,38 +30,38 @@ func NewFilledCurve() *FilledCurve { } // AppendCurve appends a Bezier curve to the filled curve. -func (this *FilledCurve) AppendCurve(curve draw.CubicBezierCurve) *FilledCurve { - this.curves = append(this.curves, curve) - return this +func (fc *FilledCurve) AppendCurve(curve draw.CubicBezierCurve) *FilledCurve { + fc.curves = append(fc.curves, curve) + return fc } // SetFillColor sets the fill color for the path. -func (this *FilledCurve) SetFillColor(color Color) { - this.fillColor = pdf.NewPdfColorDeviceRGB(color.ToRGB()) +func (fc *FilledCurve) SetFillColor(color Color) { + fc.fillColor = pdf.NewPdfColorDeviceRGB(color.ToRGB()) } // SetBorderColor sets the border color for the path. -func (this *FilledCurve) SetBorderColor(color Color) { - this.borderColor = pdf.NewPdfColorDeviceRGB(color.ToRGB()) +func (fc *FilledCurve) SetBorderColor(color Color) { + fc.borderColor = pdf.NewPdfColorDeviceRGB(color.ToRGB()) } // draw draws the filled curve. Can specify a graphics state (gsName) for setting opacity etc. Otherwise leave empty (""). // Returns the content stream as a byte array, the bounding box and an error on failure. -func (this *FilledCurve) draw(gsName string) ([]byte, *pdf.PdfRectangle, error) { +func (fc *FilledCurve) draw(gsName string) ([]byte, *pdf.PdfRectangle, error) { bpath := draw.NewCubicBezierPath() - for _, c := range this.curves { + for _, c := range fc.curves { bpath = bpath.AppendCurve(c) } creator := pdfcontent.NewContentCreator() creator.Add_q() - if this.FillEnabled { - creator.Add_rg(this.fillColor.R(), this.fillColor.G(), this.fillColor.B()) + if fc.FillEnabled { + creator.Add_rg(fc.fillColor.R(), fc.fillColor.G(), fc.fillColor.B()) } - if this.BorderEnabled { - creator.Add_RG(this.borderColor.R(), this.borderColor.G(), this.borderColor.B()) - creator.Add_w(this.BorderWidth) + if fc.BorderEnabled { + creator.Add_RG(fc.borderColor.R(), fc.borderColor.G(), fc.borderColor.B()) + creator.Add_w(fc.BorderWidth) } if len(gsName) > 1 { // If a graphics state is provided, use it. (can support transparency). @@ -71,23 +71,23 @@ func (this *FilledCurve) draw(gsName string) ([]byte, *pdf.PdfRectangle, error) draw.DrawBezierPathWithCreator(bpath, creator) creator.Add_h() // Close the path. - if this.FillEnabled && this.BorderEnabled { + if fc.FillEnabled && fc.BorderEnabled { creator.Add_B() // fill and stroke. - } else if this.FillEnabled { + } else if fc.FillEnabled { creator.Add_f() // Fill. - } else if this.BorderEnabled { + } else if fc.BorderEnabled { creator.Add_S() // Stroke. } creator.Add_Q() // Get bounding box. pathBbox := bpath.GetBoundingBox() - if this.BorderEnabled { + if fc.BorderEnabled { // Account for stroke width. - pathBbox.Height += this.BorderWidth - pathBbox.Width += this.BorderWidth - pathBbox.X -= this.BorderWidth / 2 - pathBbox.Y -= this.BorderWidth / 2 + pathBbox.Height += fc.BorderWidth + pathBbox.Width += fc.BorderWidth + pathBbox.X -= fc.BorderWidth / 2 + pathBbox.Y -= fc.BorderWidth / 2 } // Bounding box - global coordinate system. @@ -100,10 +100,10 @@ func (this *FilledCurve) draw(gsName string) ([]byte, *pdf.PdfRectangle, error) } // GeneratePageBlocks draws the filled curve on page blocks. -func (this *FilledCurve) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { +func (fc *FilledCurve) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { block := NewBlock(ctx.PageWidth, ctx.PageHeight) - contents, _, err := this.draw("") + contents, _, err := fc.draw("") err = block.addContentsByString(string(contents)) if err != nil { return nil, ctx, err diff --git a/pdf/creator/image.go b/pdf/creator/image.go index ae5bbe59..1b2c860d 100644 --- a/pdf/creator/image.go +++ b/pdf/creator/image.go @@ -332,8 +332,7 @@ func drawImageOnBlock(blk *Block, img *Image, ctx DrawContext) (DrawContext, err ctx.Y += img.Height() ctx.Height -= img.Height() return ctx, nil - } else { - // Absolute positioning - return original context. - return origCtx, nil } + // Absolute positioning - return original context. + return origCtx, nil } diff --git a/pdf/creator/paragraph.go b/pdf/creator/paragraph.go index 70ec4af8..c9857830 100644 --- a/pdf/creator/paragraph.go +++ b/pdf/creator/paragraph.go @@ -193,9 +193,8 @@ func (p *Paragraph) SetWidth(width float64) { func (p *Paragraph) Width() float64 { if p.enableWrap { return p.wrapWidth - } else { - return p.getTextWidth() / 1000.0 } + return p.getTextWidth() / 1000.0 } // Height returns the height of the Paragraph. The height is calculated based on the input text and how it is wrapped @@ -382,10 +381,9 @@ func (p *Paragraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, ctx.X -= p.margins.left // Move back. ctx.Width = origContext.Width return blocks, ctx, nil - } else { - // Absolute: not changing the context. - return blocks, origContext, nil } + // Absolute: not changing the context. + return blocks, origContext, nil } // Draw block on specified location on Page, adding to the content stream. diff --git a/pdf/creator/table.go b/pdf/creator/table.go index 73c233a1..c012957d 100644 --- a/pdf/creator/table.go +++ b/pdf/creator/table.go @@ -390,14 +390,14 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, if table.positioning.isAbsolute() { return blocks, origCtx, nil - } else { - // Move back X after. - ctx.X = origCtx.X - // Return original width - ctx.Width = origCtx.Width - // Add the bottom margin - ctx.Y += table.margins.bottom } + // Relative mode. + // Move back X after. + ctx.X = origCtx.X + // Return original width + ctx.Width = origCtx.Width + // Add the bottom margin + ctx.Y += table.margins.bottom return blocks, ctx, nil } From 088c245f8bce8a08f27f6203943b06ce6cfa02ff Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Wed, 1 Aug 2018 22:12:31 +0000 Subject: [PATCH 10/17] Updated Jenkinsfile --- Jenkinsfile | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index c8d9eb1c..1c97cf8b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -10,7 +10,8 @@ node { sh 'go version' stage('Checkout') { - git url: 'https://github.com/unidoc/unidoc.git' + echo "Pulling unidoc on branch ${env.BRANCH_NAME}" + checkout scm } stage('Prepare') { @@ -56,6 +57,29 @@ node { sh 'go2xunit -fail -input gotest.txt -output gotest.xml' junit "gotest.xml" } + } + dir("${GOPATH}/src/github.com/unidoc/unidoc-examples") { + stage('Build examples') { + // Pull unidoc-examples from connected branch, or master otherwise. + def examplesBranch = "master" + switch("${env.BRANCH_NAME}") { + case "v3": + examplesBranch = "v3" + break + } + echo "Pulling unidoc-examples on branch ${examplesBranch}" + git url: 'https://github.com/unidoc/unidoc-examples.git', branch: examplesBranch + + // Dependencies for examples. + sh 'go get github.com/wcharczuk/go-chart' + + // Build all examples. + sh 'find . -name "*.go" -print0 | xargs -0 -n1 go build' + } + + stage('Passthrough benchmark pdfdb_small') { + sh './pdf_passthrough_bench /home/jenkins/corpus/pdfdb_small/* | grep -v "Testing " | grep -v "copy of" | grep -v "To get " | grep -v " - pass"' + } } } \ No newline at end of file From 2cc0c74b8001b4e08a04e127f6df7033d3eed160 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 9 Aug 2018 22:51:21 +0000 Subject: [PATCH 11/17] Avoid endless loop when parsing operand in contenstream. Fixes #176 --- pdf/contentstream/const.go | 12 ++++++++++++ pdf/contentstream/parser.go | 12 ++++++++++-- pdf/core/primitives.go | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 pdf/contentstream/const.go diff --git a/pdf/contentstream/const.go b/pdf/contentstream/const.go new file mode 100644 index 00000000..9b211d23 --- /dev/null +++ b/pdf/contentstream/const.go @@ -0,0 +1,12 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package contentstream + +import "errors" + +var ( + ErrInvalidOperand = errors.New("invalid operand") +) diff --git a/pdf/contentstream/parser.go b/pdf/contentstream/parser.go index df6847a0..ecf3e814 100644 --- a/pdf/contentstream/parser.go +++ b/pdf/contentstream/parser.go @@ -542,6 +542,7 @@ func (this *ContentStreamParser) parseObject() (PdfObject, error, bool) { } else if bb[0] == '(' { common.Log.Trace("->String!") str, err := this.parseString() + common.Log.Trace("(%s)\n", str.String()) return &str, err, false } else if bb[0] == '<' && bb[1] != '<' { common.Log.Trace("->Hex String!") @@ -559,11 +560,12 @@ func (this *ContentStreamParser) parseObject() (PdfObject, error, bool) { dict, err := this.parseDict() return dict, err, false } else { + // Otherwise, can be: keyword such as "null", "false", "true" or an operand... common.Log.Trace("->Operand or bool?") // Let's peek farther to find out. bb, _ = this.reader.Peek(5) peekStr := string(bb) - common.Log.Trace("Peek str: %s", peekStr) + common.Log.Trace("cont Peek str: %s", peekStr) if (len(peekStr) > 3) && (peekStr[:4] == "null") { null, err := this.parseNull() @@ -577,7 +579,13 @@ func (this *ContentStreamParser) parseObject() (PdfObject, error, bool) { } operand, err := this.parseOperand() - return &operand, err, true + if err != nil { + return &operand, err, false + } + if len(operand.String()) < 1 { + return &operand, ErrInvalidOperand, false + } + return &operand, nil, true } } } diff --git a/pdf/core/primitives.go b/pdf/core/primitives.go index 461f2fbe..e5f8fa9f 100644 --- a/pdf/core/primitives.go +++ b/pdf/core/primitives.go @@ -209,7 +209,7 @@ func (float *PdfObjectFloat) DefaultWriteString() string { } func (str *PdfObjectString) String() string { - return fmt.Sprintf("%s", string(*str)) + return string(*str) } // DefaultWriteString outputs the object as it is to be written to file. From d02631294865f212ee36e87365d547df8b3aafb7 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Tue, 14 Aug 2018 03:02:19 +0000 Subject: [PATCH 12/17] Godoc fixes in model reader.go --- pdf/model/reader.go | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/pdf/model/reader.go b/pdf/model/reader.go index 152f020f..f5fb92f8 100644 --- a/pdf/model/reader.go +++ b/pdf/model/reader.go @@ -15,6 +15,8 @@ import ( . "github.com/unidoc/unidoc/pdf/core" ) +// PdfReader represents a PDF file reader. It is a frontend to the lower level parsing mechanism and provides +// a higher level access to work with PDF structure and information, such as the page structure etc. type PdfReader struct { parser *PdfParser root PdfObject @@ -32,6 +34,9 @@ type PdfReader struct { traversed map[PdfObject]bool } +// NewPdfReader returns a new PdfReader for an input io.ReadSeeker interface. Can be used to read PDF from +// memory or file. Immediately loads and traverses the PDF structure including pages and page contents (if +// not encrypted). func NewPdfReader(rs io.ReadSeeker) (*PdfReader, error) { pdfReader := &PdfReader{} pdfReader.traversed = map[PdfObject]bool{} @@ -61,13 +66,13 @@ func NewPdfReader(rs io.ReadSeeker) (*PdfReader, error) { return pdfReader, nil } +// IsEncrypted returns true if the PDF file is encrypted. func (this *PdfReader) IsEncrypted() (bool, error) { return this.parser.IsEncrypted() } -// Returns a string containing some information about the encryption method used. -// Subject to changes. May be better to return a standardized struct with information. -// But challenging due to the many different types supported. +// GetEncryptionMethod returns a string containing some information about the encryption method used. +// XXX/TODO: May be better to return a standardized struct with information. func (this *PdfReader) GetEncryptionMethod() string { crypter := this.parser.GetCrypter() str := crypter.Filter + " - " @@ -95,7 +100,7 @@ func (this *PdfReader) GetEncryptionMethod() string { return str } -// Decrypt the PDF file with a specified password. Also tries to +// Decrypt decrypts the PDF file with a specified password. Also tries to // decrypt with an empty password. Returns true if successful, // false otherwise. func (this *PdfReader) Decrypt(password []byte) (bool, error) { @@ -116,8 +121,9 @@ func (this *PdfReader) Decrypt(password []byte) (bool, error) { return true, nil } -// Check access rights and permissions for a specified password. If either user/owner password is specified, -// full rights are granted, otherwise the access rights are specified by the Permissions flag. +// CheckAccessRights checks access rights and permissions for a specified password. If either user/owner +// password is specified, full rights are granted, otherwise the access rights are specified by the +// Permissions flag. // // The bool flag indicates that the user can access and view the file. // The AccessPermissions shows what access the user has for editing etc. @@ -218,7 +224,6 @@ func (this *PdfReader) loadStructure() error { return nil } -// // Trace to object. Keeps a list of already visited references to avoid circular references. // // Example circular reference. @@ -402,12 +407,12 @@ func (this *PdfReader) buildOutlineTree(obj PdfObject, parent *PdfOutlineTreeNod } } -// Get the outline tree. +// GetOutlineTree returns the outline tree. func (this *PdfReader) GetOutlineTree() *PdfOutlineTreeNode { return this.outlineTree } -// Return a flattened list of tree nodes and titles. +// GetOutlinesFlattened returns a flattened list of tree nodes and titles. func (this *PdfReader) GetOutlinesFlattened() ([]*PdfOutlineTreeNode, []string, error) { outlineNodeList := []*PdfOutlineTreeNode{} flattenedTitleList := []string{} @@ -442,6 +447,7 @@ func (this *PdfReader) GetOutlinesFlattened() ([]*PdfOutlineTreeNode, []string, return outlineNodeList, flattenedTitleList, nil } +// loadForms loads the AcroForm. func (this *PdfReader) loadForms() (*PdfAcroForm, error) { if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() { return nil, fmt.Errorf("File need to be decrypted first") @@ -588,7 +594,7 @@ func (this *PdfReader) buildPageList(node *PdfIndirectObject, parent *PdfIndirec return nil } -// Get the number of pages in the document. +// GetNumPages returns the number of pages in the document. func (this *PdfReader) GetNumPages() (int, error) { if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() { return 0, fmt.Errorf("File need to be decrypted first") @@ -694,7 +700,7 @@ func (this *PdfReader) traverseObjectData(o PdfObject) error { return nil } -// Get a page by the page number. Indirect object with type /Page. +// GetPageAsIndirectObject returns an indirect object containing the page dictionary for a specified page number. func (this *PdfReader) GetPageAsIndirectObject(pageNumber int) (PdfObject, error) { if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() { return nil, fmt.Errorf("File needs to be decrypted first") @@ -715,8 +721,7 @@ func (this *PdfReader) GetPageAsIndirectObject(pageNumber int) (PdfObject, error return page, nil } -// Get a page by the page number. -// Returns the PdfPage entry. +// GetPage returns the PdfPage model for the specified page number. func (this *PdfReader) GetPage(pageNumber int) (*PdfPage, error) { if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() { return nil, fmt.Errorf("File needs to be decrypted first") @@ -733,7 +738,7 @@ func (this *PdfReader) GetPage(pageNumber int) (*PdfPage, error) { return page, nil } -// Get optional content properties +// GetOCProperties returns the optional content properties PdfObject. func (this *PdfReader) GetOCProperties() (PdfObject, error) { dict := this.catalog obj := dict.Get("OCProperties") @@ -755,7 +760,8 @@ func (this *PdfReader) GetOCProperties() (PdfObject, error) { return obj, nil } -// Inspect the object types, subtypes and content in the PDF file. +// Inspect inspects the object types, subtypes and content in the PDF file returning a map of +// object type to number of instances of each. func (this *PdfReader) Inspect() (map[string]int, error) { return this.parser.Inspect() } @@ -769,12 +775,13 @@ func (r *PdfReader) GetObjectNums() []int { return r.parser.GetObjectNums() } -// Get specific object number. +// GetIndirectObjectByNumber retrieves and returns a specific PdfObject by object number. func (this *PdfReader) GetIndirectObjectByNumber(number int) (PdfObject, error) { obj, err := this.parser.LookupByNumber(number) return obj, err } +// GetTrailer returns the PDF's trailer dictionary. func (this *PdfReader) GetTrailer() (*PdfObjectDictionary, error) { trailerDict := this.parser.GetTrailer() if trailerDict == nil { From 036ab51b7994ecc476090e11c92b6bb78ffd25b2 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Tue, 14 Aug 2018 17:57:26 +0000 Subject: [PATCH 13/17] Address review comments. --- pdf/creator/table.go | 12 ++++++------ pdf/creator/table_test.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pdf/creator/table.go b/pdf/creator/table.go index c012957d..16c4d56c 100644 --- a/pdf/creator/table.go +++ b/pdf/creator/table.go @@ -207,7 +207,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, newh += 0.5 * p.fontSize * p.lineHeight // TODO: Make the top margin configurable? if newh > h { diffh := newh - h - // Add diff to last row + // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } case *Image: @@ -215,7 +215,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, newh := img.Height() + img.margins.top + img.margins.bottom if newh > h { diffh := newh - h - // Add diff to last row + // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } case *Division: @@ -238,7 +238,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, newh := ctx.Height - h if newh > h { diffh := newh - h - // Add diff to last row + // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } } @@ -249,7 +249,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, // Get available width and height. if newh > h { diffh := newh - h - // Add diff to last row + // Add diff to last row. table.rowHeights[cell.row+cell.rowspan-2] += diffh } } @@ -394,9 +394,9 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, // Relative mode. // Move back X after. ctx.X = origCtx.X - // Return original width + // Return original width. ctx.Width = origCtx.Width - // Add the bottom margin + // Add the bottom margin. ctx.Y += table.margins.bottom return blocks, ctx, nil diff --git a/pdf/creator/table_test.go b/pdf/creator/table_test.go index 12c76b41..372f95b3 100644 --- a/pdf/creator/table_test.go +++ b/pdf/creator/table_test.go @@ -83,7 +83,7 @@ func TestTableWithImage(t *testing.T) { pageHistoryTable.SkipCells(1) - // Add image + // Add image. imgData, err := ioutil.ReadFile(testImageFile1) if err != nil { t.Errorf("Fail: %v\n", err) @@ -195,7 +195,7 @@ func TestTableWithDiv(t *testing.T) { pageHistoryTable.SkipCells(1) - // Add image + // Add image. imgData, err := ioutil.ReadFile(testImageFile1) if err != nil { t.Errorf("Fail: %v\n", err) From b1c6401dfd552f38694c74b7bbe543e148a72d0e Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Tue, 14 Aug 2018 19:41:06 +0000 Subject: [PATCH 14/17] Bump version to 2.1.1 --- common/version.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/version.go b/common/version.go index 18d249a6..ce5fa222 100644 --- a/common/version.go +++ b/common/version.go @@ -11,12 +11,12 @@ import ( ) const releaseYear = 2018 -const releaseMonth = 5 -const releaseDay = 20 -const releaseHour = 23 -const releaseMin = 30 +const releaseMonth = 8 +const releaseDay = 14 +const releaseHour = 19 +const releaseMin = 40 // Holds version information, when bumping this make sure to bump the released at stamp also. -const Version = "2.1.0" +const Version = "2.1.1" var ReleasedAt = time.Date(releaseYear, releaseMonth, releaseDay, releaseHour, releaseMin, 0, 0, time.UTC) From 3733585ae36da08dfd5125e5749992b8ea134b5d Mon Sep 17 00:00:00 2001 From: Alfred Hall Date: Wed, 15 Aug 2018 11:12:01 +0000 Subject: [PATCH 15/17] Optional license expiry for trial purposes. --- common/license/crypto.go | 5 +++++ common/license/key.go | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/common/license/crypto.go b/common/license/crypto.go index dad48bb3..d1a5b397 100644 --- a/common/license/crypto.go +++ b/common/license/crypto.go @@ -159,5 +159,10 @@ func licenseKeyDecode(content string) (LicenseKey, error) { ret.CreatedAt = time.Unix(ret.CreatedAtInt, 0) + if ret.ExpiresAtInt > 0 { + expiresAt := time.Unix(ret.ExpiresAtInt, 0) + ret.ExpiresAt = &expiresAt + } + return ret, nil } diff --git a/common/license/key.go b/common/license/key.go index 045cf01b..0dd652ca 100644 --- a/common/license/key.go +++ b/common/license/key.go @@ -23,18 +23,22 @@ const ( var testTime = time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC) type LicenseKey struct { - LicenseId string `json:"license_id"` - CustomerId string `json:"customer_id"` - CustomerName string `json:"customer_name"` - Tier string `json:"tier"` - CreatedAt time.Time `json:"-"` - CreatedAtInt int64 `json:"created_at"` - CreatedBy string `json:"created_by"` - CreatorName string `json:"creator_name"` - CreatorEmail string `json:"creator_email"` + LicenseId string `json:"license_id"` + CustomerId string `json:"customer_id"` + CustomerName string `json:"customer_name"` + Tier string `json:"tier"` + CreatedAt time.Time `json:"-"` + CreatedAtInt int64 `json:"created_at"` + ExpiresAt *time.Time `json:"-"` + ExpiresAtInt int64 `json:"expires_at"` + CreatedBy string `json:"created_by"` + CreatorName string `json:"creator_name"` + CreatorEmail string `json:"creator_email"` } func (this *LicenseKey) Validate() error { + utcNow := time.Now().UTC() + if len(this.LicenseId) < 10 { return fmt.Errorf("Invalid license: License Id") } @@ -51,6 +55,16 @@ func (this *LicenseKey) Validate() error { return fmt.Errorf("Invalid license: Created At is invalid") } + if this.ExpiresAt != nil { + if this.CreatedAt.After(*this.ExpiresAt) { + return fmt.Errorf("Invalid license: Created At cannot be Greater than Expires At") + } + + if utcNow.After(*this.ExpiresAt) { + return fmt.Errorf("Invalid license: The license has already expired") + } + } + if len(this.CreatorName) < 1 { return fmt.Errorf("Invalid license: Creator name") } @@ -84,6 +98,13 @@ func (this *LicenseKey) ToString() string { str += fmt.Sprintf("Customer Name: %s\n", this.CustomerName) str += fmt.Sprintf("Tier: %s\n", this.Tier) str += fmt.Sprintf("Created At: %s\n", common.UtcTimeFormat(this.CreatedAt)) + + if this.ExpiresAt == nil { + str += fmt.Sprintf("Expires At: Never\n") + } else { + str += fmt.Sprintf("Expires At: %s\n", common.UtcTimeFormat(*this.ExpiresAt)) + } + str += fmt.Sprintf("Creator: %s <%s>\n", this.CreatorName, this.CreatorEmail) return str } From 7311bdfd8e994253b0e84fda053f9c6c3c53f557 Mon Sep 17 00:00:00 2001 From: Alfred Hall Date: Wed, 15 Aug 2018 13:39:58 +0000 Subject: [PATCH 16/17] Fixing so old licenses still work correctly. --- common/license/key.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/common/license/key.go b/common/license/key.go index 0dd652ca..aef42833 100644 --- a/common/license/key.go +++ b/common/license/key.go @@ -22,6 +22,10 @@ const ( // Make sure all time is at least after this for sanity check. var testTime = time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC) +// Old licenses had expiry that were not meant to expire. Only checking expiry +// on licenses issued later than this date. +var startCheckingExpiry = time.Date(2018, 8, 1, 0, 0, 0, 0, time.UTC) + type LicenseKey struct { LicenseId string `json:"license_id"` CustomerId string `json:"customer_id"` @@ -36,9 +40,20 @@ type LicenseKey struct { CreatorEmail string `json:"creator_email"` } -func (this *LicenseKey) Validate() error { - utcNow := time.Now().UTC() +func (this *LicenseKey) isExpired() bool { + if this.ExpiresAt == nil { + return false + } + if this.CreatedAt.Before(startCheckingExpiry) { + return false + } + + utcNow := time.Now().UTC() + return utcNow.After(*this.ExpiresAt) +} + +func (this *LicenseKey) Validate() error { if len(this.LicenseId) < 10 { return fmt.Errorf("Invalid license: License Id") } @@ -59,10 +74,10 @@ func (this *LicenseKey) Validate() error { if this.CreatedAt.After(*this.ExpiresAt) { return fmt.Errorf("Invalid license: Created At cannot be Greater than Expires At") } + } - if utcNow.After(*this.ExpiresAt) { - return fmt.Errorf("Invalid license: The license has already expired") - } + if this.isExpired() { + return fmt.Errorf("Invalid license: The license has already expired") } if len(this.CreatorName) < 1 { From c250f04b5214e44f5b4e3cfcba86e7220e0c6199 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Mon, 3 Sep 2018 10:59:32 +0000 Subject: [PATCH 17/17] Jenkinsfile - Add ghostscript checks on created output pdfs --- Jenkinsfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 99fddd41..aae8cece 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,6 +5,7 @@ node { env.GOROOT="${root}" env.GOPATH="${WORKSPACE}/gopath" env.PATH="${root}/bin:${env.GOPATH}/bin:${env.PATH}" + env.GOCACHE="off" dir("${GOPATH}/src/github.com/unidoc/unidoc") { sh 'go version' @@ -36,10 +37,15 @@ node { stage('Testing') { // Go test - No tolerance. - //sh 'go test -v ./... >gotest.txt 2>&1' + sh 'rm -f /tmp/*.pdf' sh '2>&1 go test -v ./... | tee gotest.txt' } + stage('Check generated PDFs') { + // Check the created output pdf files. + sh 'find /tmp -maxdepth 1 -name "*.pdf" -print0 | xargs -t -n 1 -0 gs -dNOPAUSE -dBATCH -sDEVICE=nullpage -sPDFPassword=password -dPDFSTOPONERROR -dPDFSTOPONWARNING' + } + stage('Test coverage') { sh 'go test -coverprofile=coverage.out ./...' sh 'gocover-cobertura < coverage.out > coverage.xml'