mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-26 13:48:55 +08:00
Merge branch 'master' into v3-merge-master-in
This commit is contained in:
commit
e63a74cc9c
@ -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
|
||||
}
|
||||
|
@ -22,16 +22,35 @@ 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"`
|
||||
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) 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 {
|
||||
@ -51,6 +70,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 this.isExpired() {
|
||||
return fmt.Errorf("Invalid license: The license has already expired")
|
||||
}
|
||||
|
||||
if len(this.CreatorName) < 1 {
|
||||
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("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
|
||||
}
|
||||
|
@ -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)
|
||||
|
12
pdf/contentstream/const.go
Normal file
12
pdf/contentstream/const.go
Normal 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")
|
||||
)
|
@ -559,11 +559,12 @@ func (this *ContentStreamParser) parseObject() (obj PdfObject, err error, isop b
|
||||
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 +578,13 @@ func (this *ContentStreamParser) parseObject() (obj PdfObject, err error, isop b
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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{}
|
||||
|
||||
@ -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)
|
||||
@ -167,7 +168,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 +176,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 +236,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 +246,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 +281,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 +332,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 {
|
||||
|
@ -193,7 +193,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
|
||||
|
136
pdf/creator/division.go
Normal file
136
pdf/creator/division.go
Normal 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
|
||||
}
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ func (p *Paragraph) Height() float64 {
|
||||
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 {
|
||||
w := 0.0
|
||||
|
||||
@ -387,10 +387,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
|
||||
}
|
||||
|
||||
// drawParagraphOnBlock draws Paragraph `p` on Block `blk` at the specified location on the page,
|
||||
|
@ -200,7 +200,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)
|
||||
}
|
||||
@ -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?
|
||||
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:
|
||||
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.
|
||||
@ -244,6 +289,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)
|
||||
@ -354,14 +400,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
|
||||
}
|
||||
@ -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.
|
||||
// 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:
|
||||
@ -637,6 +682,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", vd)
|
||||
|
242
pdf/creator/table_test.go
Normal file
242
pdf/creator/table_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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,7 +34,9 @@ type PdfReader struct {
|
||||
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) {
|
||||
pdfReader := &PdfReader{}
|
||||
pdfReader.traversed = map[PdfObject]bool{}
|
||||
@ -67,14 +71,12 @@ func (this *PdfReader) PdfVersion() string {
|
||||
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) {
|
||||
return this.parser.IsEncrypted()
|
||||
}
|
||||
|
||||
// GetEncryptionMethod 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 descriptive information string about the encryption method used.
|
||||
func (this *PdfReader) GetEncryptionMethod() string {
|
||||
crypter := this.parser.GetCrypter()
|
||||
str := crypter.Filter + " - "
|
||||
@ -123,8 +125,9 @@ func (this *PdfReader) Decrypt(password []byte) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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.
|
||||
@ -225,7 +228,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.
|
||||
@ -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 {
|
||||
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{}
|
||||
@ -449,6 +451,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")
|
||||
@ -701,7 +704,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")
|
||||
@ -722,8 +725,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")
|
||||
@ -740,7 +742,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")
|
||||
@ -762,7 +764,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()
|
||||
}
|
||||
@ -776,12 +779,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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user