Merge branch 'master' into v3-merge-master-in

This commit is contained in:
Gunnsteinn Hall 2018-09-06 09:45:04 +00:00
commit e63a74cc9c
15 changed files with 578 additions and 83 deletions

View File

@ -159,5 +159,10 @@ func licenseKeyDecode(content string) (LicenseKey, error) {
ret.CreatedAt = time.Unix(ret.CreatedAtInt, 0) ret.CreatedAt = time.Unix(ret.CreatedAtInt, 0)
if ret.ExpiresAtInt > 0 {
expiresAt := time.Unix(ret.ExpiresAtInt, 0)
ret.ExpiresAt = &expiresAt
}
return ret, nil return ret, nil
} }

View File

@ -22,6 +22,10 @@ const (
// Make sure all time is at least after this for sanity check. // 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) 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 { type LicenseKey struct {
LicenseId string `json:"license_id"` LicenseId string `json:"license_id"`
CustomerId string `json:"customer_id"` CustomerId string `json:"customer_id"`
@ -29,11 +33,26 @@ type LicenseKey struct {
Tier string `json:"tier"` Tier string `json:"tier"`
CreatedAt time.Time `json:"-"` CreatedAt time.Time `json:"-"`
CreatedAtInt int64 `json:"created_at"` CreatedAtInt int64 `json:"created_at"`
ExpiresAt *time.Time `json:"-"`
ExpiresAtInt int64 `json:"expires_at"`
CreatedBy string `json:"created_by"` CreatedBy string `json:"created_by"`
CreatorName string `json:"creator_name"` CreatorName string `json:"creator_name"`
CreatorEmail string `json:"creator_email"` CreatorEmail string `json:"creator_email"`
} }
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 { func (this *LicenseKey) Validate() error {
if len(this.LicenseId) < 10 { if len(this.LicenseId) < 10 {
return fmt.Errorf("Invalid license: License Id") return fmt.Errorf("Invalid license: License Id")
@ -51,6 +70,16 @@ func (this *LicenseKey) Validate() error {
return fmt.Errorf("Invalid license: Created At is invalid") 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 this.isExpired() {
return fmt.Errorf("Invalid license: The license has already expired")
}
if len(this.CreatorName) < 1 { if len(this.CreatorName) < 1 {
return fmt.Errorf("Invalid license: Creator name") return fmt.Errorf("Invalid license: Creator name")
} }
@ -84,6 +113,13 @@ func (this *LicenseKey) ToString() string {
str += fmt.Sprintf("Customer Name: %s\n", this.CustomerName) str += fmt.Sprintf("Customer Name: %s\n", this.CustomerName)
str += fmt.Sprintf("Tier: %s\n", this.Tier) str += fmt.Sprintf("Tier: %s\n", this.Tier)
str += fmt.Sprintf("Created At: %s\n", common.UtcTimeFormat(this.CreatedAt)) 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) str += fmt.Sprintf("Creator: %s <%s>\n", this.CreatorName, this.CreatorEmail)
return str return str
} }

View File

@ -11,12 +11,12 @@ import (
) )
const releaseYear = 2018 const releaseYear = 2018
const releaseMonth = 5 const releaseMonth = 8
const releaseDay = 20 const releaseDay = 14
const releaseHour = 23 const releaseHour = 19
const releaseMin = 30 const releaseMin = 40
// Holds version information, when bumping this make sure to bump the released at stamp also. // 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) var ReleasedAt = time.Date(releaseYear, releaseMonth, releaseDay, releaseHour, releaseMin, 0, 0, time.UTC)

View File

@ -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")
)

View File

@ -559,11 +559,12 @@ func (this *ContentStreamParser) parseObject() (obj PdfObject, err error, isop b
dict, err := this.parseDict() dict, err := this.parseDict()
return dict, err, false return dict, err, false
} else { } else {
// Otherwise, can be: keyword such as "null", "false", "true" or an operand...
common.Log.Trace("->Operand or bool?") common.Log.Trace("->Operand or bool?")
// Let's peek farther to find out. // Let's peek farther to find out.
bb, _ = this.reader.Peek(5) bb, _ = this.reader.Peek(5)
peekStr := string(bb) 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") { if (len(peekStr) > 3) && (peekStr[:4] == "null") {
null, err := this.parseNull() null, err := this.parseNull()
@ -577,7 +578,13 @@ func (this *ContentStreamParser) parseObject() (obj PdfObject, err error, isop b
} }
operand, err := this.parseOperand() 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
} }
} }
} }

View File

@ -96,7 +96,7 @@ func (blk *Block) SetAngle(angleDeg float64) {
blk.angle = angleDeg 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 { func (blk *Block) duplicate() *Block {
dup := &Block{} dup := &Block{}
@ -130,6 +130,7 @@ func (blk *Block) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, er
cc.Translate(0, -blk.Height()) cc.Translate(0, -blk.Height())
} }
contents := append(*cc.Operations(), *dup.contents...) contents := append(*cc.Operations(), *dup.contents...)
contents.WrapIfNeeded()
dup.contents = &contents dup.contents = &contents
blocks = append(blocks, dup) blocks = append(blocks, dup)
@ -167,7 +168,7 @@ func (blk *Block) Width() float64 {
return blk.width 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. // independence of content operations.
func (blk *Block) addContents(operations *contentstream.ContentStreamOperations) { func (blk *Block) addContents(operations *contentstream.ContentStreamOperations) {
blk.contents.WrapIfNeeded() blk.contents.WrapIfNeeded()
@ -175,7 +176,7 @@ func (blk *Block) addContents(operations *contentstream.ContentStreamOperations)
*blk.contents = append(*blk.contents, *operations...) *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 { func (blk *Block) addContentsByString(contents string) error {
cc := contentstream.NewContentStreamParser(contents) cc := contentstream.NewContentStreamParser(contents)
operations, err := cc.Parse() operations, err := cc.Parse()
@ -235,7 +236,7 @@ func (blk *Block) ScaleToHeight(h float64) {
blk.Scale(ratio, ratio) 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) { func (blk *Block) translate(tx, ty float64) {
ops := contentstream.NewContentCreator(). ops := contentstream.NewContentCreator().
Translate(tx, -ty). Translate(tx, -ty).
@ -245,7 +246,8 @@ func (blk *Block) translate(tx, ty float64) {
blk.contents.WrapIfNeeded() 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 { func (blk *Block) drawToPage(page *model.PdfPage) error {
// Check if Page contents are wrapped - if not wrap it. // Check if Page contents are wrapped - if not wrap it.
content, err := page.GetAllContentStreams() content, err := page.GetAllContentStreams()
@ -279,7 +281,7 @@ func (blk *Block) drawToPage(page *model.PdfPage) error {
return nil 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. // 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 { func (blk *Block) Draw(d Drawable) error {
ctx := DrawContext{} ctx := DrawContext{}
@ -330,13 +332,13 @@ func (blk *Block) DrawWithContext(d Drawable, ctx DrawContext) error {
return nil return nil
} }
// Append another block onto the block. // mergeBlocks appends another block onto the block.
func (blk *Block) mergeBlocks(toAdd *Block) error { func (blk *Block) mergeBlocks(toAdd *Block) error {
err := mergeContents(blk.contents, blk.resources, toAdd.contents, toAdd.resources) err := mergeContents(blk.contents, blk.resources, toAdd.contents, toAdd.resources)
return err 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. // Active in the sense that it modified the input contents and resources.
func mergeContents(contents *contentstream.ContentStreamOperations, resources *model.PdfPageResources, func mergeContents(contents *contentstream.ContentStreamOperations, resources *model.PdfPageResources,
contentsToAdd *contentstream.ContentStreamOperations, resourcesToAdd *model.PdfPageResources) error { contentsToAdd *contentstream.ContentStreamOperations, resourcesToAdd *model.PdfPageResources) error {

View File

@ -193,7 +193,7 @@ func (c *Creator) newPage() *model.PdfPage {
width := c.pagesize[0] width := c.pagesize[0]
height := c.pagesize[1] 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 page.MediaBox = &bbox
c.pageWidth = width c.pageWidth = width

136
pdf/creator/division.go Normal file
View File

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

View File

@ -16,7 +16,11 @@ type Drawable interface {
// VectorDrawable is a Drawable with a specified width and height. // VectorDrawable is a Drawable with a specified width and height.
type VectorDrawable interface { type VectorDrawable interface {
Drawable Drawable
// Width returns the width of the Drawable.
Width() float64 Width() float64
// Height returns the height of the Drawable.
Height() float64 Height() float64
} }
@ -30,7 +34,7 @@ type DrawContext struct {
// Current position. In a relative positioning mode, a drawable will be placed at these coordinates. // Current position. In a relative positioning mode, a drawable will be placed at these coordinates.
X, Y float64 X, Y float64
// Context dimensions. Available width and height. // Context dimensions. Available width and height (on current page).
Width, Height float64 Width, Height float64
// Page Margins. // Page Margins.

View File

@ -30,38 +30,38 @@ func NewFilledCurve() *FilledCurve {
} }
// AppendCurve appends a Bezier curve to the filled curve. // AppendCurve appends a Bezier curve to the filled curve.
func (this *FilledCurve) AppendCurve(curve draw.CubicBezierCurve) *FilledCurve { func (fc *FilledCurve) AppendCurve(curve draw.CubicBezierCurve) *FilledCurve {
this.curves = append(this.curves, curve) fc.curves = append(fc.curves, curve)
return this return fc
} }
// SetFillColor sets the fill color for the path. // SetFillColor sets the fill color for the path.
func (this *FilledCurve) SetFillColor(color Color) { func (fc *FilledCurve) SetFillColor(color Color) {
this.fillColor = pdf.NewPdfColorDeviceRGB(color.ToRGB()) fc.fillColor = pdf.NewPdfColorDeviceRGB(color.ToRGB())
} }
// SetBorderColor sets the border color for the path. // SetBorderColor sets the border color for the path.
func (this *FilledCurve) SetBorderColor(color Color) { func (fc *FilledCurve) SetBorderColor(color Color) {
this.borderColor = pdf.NewPdfColorDeviceRGB(color.ToRGB()) fc.borderColor = pdf.NewPdfColorDeviceRGB(color.ToRGB())
} }
// draw draws the filled curve. Can specify a graphics state (gsName) for setting opacity etc. Otherwise leave empty (""). // 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. // 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() bpath := draw.NewCubicBezierPath()
for _, c := range this.curves { for _, c := range fc.curves {
bpath = bpath.AppendCurve(c) bpath = bpath.AppendCurve(c)
} }
creator := pdfcontent.NewContentCreator() creator := pdfcontent.NewContentCreator()
creator.Add_q() creator.Add_q()
if this.FillEnabled { if fc.FillEnabled {
creator.Add_rg(this.fillColor.R(), this.fillColor.G(), this.fillColor.B()) creator.Add_rg(fc.fillColor.R(), fc.fillColor.G(), fc.fillColor.B())
} }
if this.BorderEnabled { if fc.BorderEnabled {
creator.Add_RG(this.borderColor.R(), this.borderColor.G(), this.borderColor.B()) creator.Add_RG(fc.borderColor.R(), fc.borderColor.G(), fc.borderColor.B())
creator.Add_w(this.BorderWidth) creator.Add_w(fc.BorderWidth)
} }
if len(gsName) > 1 { if len(gsName) > 1 {
// If a graphics state is provided, use it. (can support transparency). // 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) draw.DrawBezierPathWithCreator(bpath, creator)
creator.Add_h() // Close the path. creator.Add_h() // Close the path.
if this.FillEnabled && this.BorderEnabled { if fc.FillEnabled && fc.BorderEnabled {
creator.Add_B() // fill and stroke. creator.Add_B() // fill and stroke.
} else if this.FillEnabled { } else if fc.FillEnabled {
creator.Add_f() // Fill. creator.Add_f() // Fill.
} else if this.BorderEnabled { } else if fc.BorderEnabled {
creator.Add_S() // Stroke. creator.Add_S() // Stroke.
} }
creator.Add_Q() creator.Add_Q()
// Get bounding box. // Get bounding box.
pathBbox := bpath.GetBoundingBox() pathBbox := bpath.GetBoundingBox()
if this.BorderEnabled { if fc.BorderEnabled {
// Account for stroke width. // Account for stroke width.
pathBbox.Height += this.BorderWidth pathBbox.Height += fc.BorderWidth
pathBbox.Width += this.BorderWidth pathBbox.Width += fc.BorderWidth
pathBbox.X -= this.BorderWidth / 2 pathBbox.X -= fc.BorderWidth / 2
pathBbox.Y -= this.BorderWidth / 2 pathBbox.Y -= fc.BorderWidth / 2
} }
// Bounding box - global coordinate system. // 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. // 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) block := NewBlock(ctx.PageWidth, ctx.PageHeight)
contents, _, err := this.draw("") contents, _, err := fc.draw("")
err = block.addContentsByString(string(contents)) err = block.addContentsByString(string(contents))
if err != nil { if err != nil {
return nil, ctx, err return nil, ctx, err

View File

@ -332,8 +332,7 @@ func drawImageOnBlock(blk *Block, img *Image, ctx DrawContext) (DrawContext, err
ctx.Y += img.Height() ctx.Y += img.Height()
ctx.Height -= img.Height() ctx.Height -= img.Height()
return ctx, nil return ctx, nil
} else { }
// Absolute positioning - return original context. // Absolute positioning - return original context.
return origCtx, nil return origCtx, nil
}
} }

View File

@ -209,7 +209,7 @@ func (p *Paragraph) Height() float64 {
return float64(len(p.textLines)) * p.lineHeight * p.fontSize return float64(len(p.textLines)) * p.lineHeight * p.fontSize
} }
// 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 { func (p *Paragraph) getTextWidth() float64 {
w := 0.0 w := 0.0
@ -387,10 +387,9 @@ func (p *Paragraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
ctx.X -= p.margins.left // Move back. ctx.X -= p.margins.left // Move back.
ctx.Width = origContext.Width ctx.Width = origContext.Width
return blocks, ctx, nil return blocks, ctx, nil
} else { }
// Absolute: not changing the context. // Absolute: not changing the context.
return blocks, origContext, nil return blocks, origContext, nil
}
} }
// drawParagraphOnBlock draws Paragraph `p` on Block `blk` at the specified location on the page, // drawParagraphOnBlock draws Paragraph `p` on Block `blk` at the specified location on the page,

View File

@ -200,7 +200,9 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
} }
// For text: Calculate width, height, wrapping within available space if specified. // 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 { if p.enableWrap {
p.SetWidth(w - cell.indent) p.SetWidth(w - cell.indent)
} }
@ -209,10 +211,53 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
newh += 0.5 * p.fontSize * p.lineHeight // TODO: Make the top margin configurable? newh += 0.5 * p.fontSize * p.lineHeight // TODO: Make the top margin configurable?
if newh > h { if newh > h {
diffh := 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:
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 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. // Draw cells.
@ -244,6 +289,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
} }
ctx.Height = origHeight - yrel ctx.Height = origHeight - yrel
if h > ctx.Height { if h > ctx.Height {
// Go to next page. // Go to next page.
blocks = append(blocks, block) blocks = append(blocks, block)
@ -354,14 +400,14 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
if table.positioning.isAbsolute() { if table.positioning.isAbsolute() {
return blocks, origCtx, nil return blocks, origCtx, nil
} else { }
// Relative mode.
// Move back X after. // Move back X after.
ctx.X = origCtx.X ctx.X = origCtx.X
// Return original width // Return original width.
ctx.Width = origCtx.Width ctx.Width = origCtx.Width
// Add the bottom margin // Add the bottom margin.
ctx.Y += table.margins.bottom ctx.Y += table.margins.bottom
}
return blocks, ctx, nil return blocks, ctx, nil
} }
@ -628,7 +674,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. // 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. // The currently supported VectorDrawable is: *Paragraph.
// TODO: Add support for *Image, *Block.
func (cell *TableCell) SetContent(vd VectorDrawable) error { func (cell *TableCell) SetContent(vd VectorDrawable) error {
switch t := vd.(type) { switch t := vd.(type) {
case *Paragraph: case *Paragraph:
@ -637,6 +682,10 @@ func (cell *TableCell) SetContent(vd VectorDrawable) error {
t.enableWrap = false // No wrapping. t.enableWrap = false // No wrapping.
} }
cell.content = vd
case *Image:
cell.content = vd
case *Division:
cell.content = vd cell.content = vd
default: default:
common.Log.Debug("ERROR: unsupported cell content type %T", vd) common.Log.Debug("ERROR: unsupported cell content type %T", vd)

242
pdf/creator/table_test.go Normal file
View File

@ -0,0 +1,242 @@
/*
* 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"
)
// makeStandardFontMustCompile makes the standard font specified by `fontName` and returns the created font.
// Panics on error.
func makeStandardFontMustCompile(fontName string) *model.PdfFont {
font, err := model.NewStandard14Font(fontName)
if err != nil {
panic(err)
}
return font
}
var (
fontHelvetica = makeStandardFontMustCompile(`Helvetica`)
fontHelveticaBold = makeStandardFontMustCompile(`Helvetica-Bold`)
)
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"},
}
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(CellBorderSideAll, CellBorderStyleSingle, 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"},
}
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(CellBorderSideAll, CellBorderStyleSingle, 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(CellBorderSideAll, CellBorderStyleSingle, 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"},
}
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(CellBorderSideAll, CellBorderStyleSingle, 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(CellBorderSideAll, CellBorderStyleSingle, 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)
}
}

View File

@ -15,6 +15,8 @@ import (
. "github.com/unidoc/unidoc/pdf/core" . "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 { type PdfReader struct {
parser *PdfParser parser *PdfParser
root PdfObject root PdfObject
@ -32,7 +34,9 @@ type PdfReader struct {
traversed map[PdfObject]bool traversed map[PdfObject]bool
} }
// NewPdfReader returns a new PdfReader for reading a PDF document accessed via io.ReadSeeker. // 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) { func NewPdfReader(rs io.ReadSeeker) (*PdfReader, error) {
pdfReader := &PdfReader{} pdfReader := &PdfReader{}
pdfReader.traversed = map[PdfObject]bool{} pdfReader.traversed = map[PdfObject]bool{}
@ -67,14 +71,12 @@ func (this *PdfReader) PdfVersion() string {
return this.parser.PdfVersion() return this.parser.PdfVersion()
} }
// IsEncrypted returns true if the document is encrypted, false otherwise. // IsEncrypted returns true if the PDF file is encrypted.
func (this *PdfReader) IsEncrypted() (bool, error) { func (this *PdfReader) IsEncrypted() (bool, error) {
return this.parser.IsEncrypted() return this.parser.IsEncrypted()
} }
// GetEncryptionMethod returns a string containing some information about the encryption method used. // GetEncryptionMethod returns a descriptive information string 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.
func (this *PdfReader) GetEncryptionMethod() string { func (this *PdfReader) GetEncryptionMethod() string {
crypter := this.parser.GetCrypter() crypter := this.parser.GetCrypter()
str := crypter.Filter + " - " str := crypter.Filter + " - "
@ -123,8 +125,9 @@ func (this *PdfReader) Decrypt(password []byte) (bool, error) {
return true, nil return true, nil
} }
// CheckAccessRights checks access rights and permissions for a specified password. If either user/owner password // CheckAccessRights checks access rights and permissions for a specified password. If either user/owner
// is specified, full rights are granted, otherwise the access rights are specified by the Permissions flag. // 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 bool flag indicates that the user can access and view the file.
// The AccessPermissions shows what access the user has for editing etc. // The AccessPermissions shows what access the user has for editing etc.
@ -225,7 +228,6 @@ func (this *PdfReader) loadStructure() error {
return nil return nil
} }
//
// Trace to object. Keeps a list of already visited references to avoid circular references. // Trace to object. Keeps a list of already visited references to avoid circular references.
// //
// Example circular reference. // Example circular reference.
@ -409,12 +411,12 @@ func (this *PdfReader) buildOutlineTree(obj PdfObject, parent *PdfOutlineTreeNod
} }
} }
// Get the outline tree. // GetOutlineTree returns the outline tree.
func (this *PdfReader) GetOutlineTree() *PdfOutlineTreeNode { func (this *PdfReader) GetOutlineTree() *PdfOutlineTreeNode {
return this.outlineTree 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) { func (this *PdfReader) GetOutlinesFlattened() ([]*PdfOutlineTreeNode, []string, error) {
outlineNodeList := []*PdfOutlineTreeNode{} outlineNodeList := []*PdfOutlineTreeNode{}
flattenedTitleList := []string{} flattenedTitleList := []string{}
@ -449,6 +451,7 @@ func (this *PdfReader) GetOutlinesFlattened() ([]*PdfOutlineTreeNode, []string,
return outlineNodeList, flattenedTitleList, nil return outlineNodeList, flattenedTitleList, nil
} }
// loadForms loads the AcroForm.
func (this *PdfReader) loadForms() (*PdfAcroForm, error) { func (this *PdfReader) loadForms() (*PdfAcroForm, error) {
if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() { if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() {
return nil, fmt.Errorf("File need to be decrypted first") return nil, fmt.Errorf("File need to be decrypted first")
@ -701,7 +704,7 @@ func (this *PdfReader) traverseObjectData(o PdfObject) error {
return nil 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) { func (this *PdfReader) GetPageAsIndirectObject(pageNumber int) (PdfObject, error) {
if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() { if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() {
return nil, fmt.Errorf("File needs to be decrypted first") return nil, fmt.Errorf("File needs to be decrypted first")
@ -722,8 +725,7 @@ func (this *PdfReader) GetPageAsIndirectObject(pageNumber int) (PdfObject, error
return page, nil return page, nil
} }
// Get a page by the page number. // GetPage returns the PdfPage model for the specified page number.
// Returns the PdfPage entry.
func (this *PdfReader) GetPage(pageNumber int) (*PdfPage, error) { func (this *PdfReader) GetPage(pageNumber int) (*PdfPage, error) {
if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() { if this.parser.GetCrypter() != nil && !this.parser.IsAuthenticated() {
return nil, fmt.Errorf("File needs to be decrypted first") return nil, fmt.Errorf("File needs to be decrypted first")
@ -740,7 +742,7 @@ func (this *PdfReader) GetPage(pageNumber int) (*PdfPage, error) {
return page, nil return page, nil
} }
// Get optional content properties // GetOCProperties returns the optional content properties PdfObject.
func (this *PdfReader) GetOCProperties() (PdfObject, error) { func (this *PdfReader) GetOCProperties() (PdfObject, error) {
dict := this.catalog dict := this.catalog
obj := dict.Get("OCProperties") obj := dict.Get("OCProperties")
@ -762,7 +764,8 @@ func (this *PdfReader) GetOCProperties() (PdfObject, error) {
return obj, nil 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) { func (this *PdfReader) Inspect() (map[string]int, error) {
return this.parser.Inspect() return this.parser.Inspect()
} }
@ -776,12 +779,13 @@ func (r *PdfReader) GetObjectNums() []int {
return r.parser.GetObjectNums() 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) { func (this *PdfReader) GetIndirectObjectByNumber(number int) (PdfObject, error) {
obj, err := this.parser.LookupByNumber(number) obj, err := this.parser.LookupByNumber(number)
return obj, err return obj, err
} }
// GetTrailer returns the PDF's trailer dictionary.
func (this *PdfReader) GetTrailer() (*PdfObjectDictionary, error) { func (this *PdfReader) GetTrailer() (*PdfObjectDictionary, error) {
trailerDict := this.parser.GetTrailer() trailerDict := this.parser.GetTrailer()
if trailerDict == nil { if trailerDict == nil {