Merge branch 'development' of https://github.com/unidoc/unipdf into development

This commit is contained in:
Gunnsteinn Hall 2020-05-02 14:15:48 +00:00
commit 1a6fda031b
6 changed files with 154 additions and 87 deletions

View File

@ -112,7 +112,7 @@ func (div *Division) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
ctx.X += div.margins.left
ctx.Y += div.margins.top
ctx.Width -= div.margins.left + div.margins.right
ctx.Height -= div.margins.top
ctx.Height -= div.margins.top + div.margins.bottom
}
// Set the inline mode of the division to the context.

View File

@ -5,19 +5,29 @@
package creator
import "fmt"
import (
"fmt"
)
// InvoiceAddress contains contact information that can be displayed
// in an invoice. It is used for the seller and buyer information in the
// invoice template.
type InvoiceAddress struct {
Heading string
Name string
Street string
Street2 string
Zip string
City string
State string
Country string
Phone string
Email string
// Separator defines the separator between different address components,
// such as the city, state and zip code. It defaults to ", " when the
// field is an empty string.
Separator string
}
// InvoiceCellProps holds all style properties for an invoice cell.
@ -52,6 +62,7 @@ type Invoice struct {
// Invoice addresses.
buyerAddress *InvoiceAddress
sellerAddress *InvoiceAddress
addrSeparator string
// Invoice information.
number [2]*InvoiceCell
@ -101,14 +112,22 @@ func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice {
title: "INVOICE",
// Addresses.
sellerAddress: &InvoiceAddress{},
buyerAddress: &InvoiceAddress{},
addrSeparator: ", ",
// Styles.
defaultStyle: defaultStyle,
headingStyle: headingStyle,
}
// Addresses.
i.sellerAddress = &InvoiceAddress{
Separator: i.addrSeparator,
}
i.buyerAddress = &InvoiceAddress{
Heading: "Bill to",
Separator: i.addrSeparator,
}
// Default colors.
lightGrey := ColorRGBFrom8bit(245, 245, 245)
mediumGrey := ColorRGBFrom8bit(155, 155, 155)
@ -158,7 +177,7 @@ func newInvoice(defaultStyle, headingStyle TextStyle) *Invoice {
}
i.dueDate = [2]*InvoiceCell{
i.newCell("Date", i.infoProps),
i.newCell("Due Date", i.infoProps),
i.newCell("", i.infoProps),
}
@ -536,14 +555,14 @@ func (i *Invoice) setCellBorder(cell *TableCell, invoiceCell *InvoiceCell) {
cell.SetBorderColor(invoiceCell.BorderColor)
}
func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*StyledParagraph {
func (i *Invoice) drawAddress(addr *InvoiceAddress) []*StyledParagraph {
var paragraphs []*StyledParagraph
// Address title.
if title != "" {
if addr.Heading != "" {
titleParagraph := newStyledParagraph(i.addressHeadingStyle)
titleParagraph.SetMargins(0, 0, 0, 7)
titleParagraph.Append(title)
titleParagraph.Append(addr.Heading)
paragraphs = append(paragraphs, titleParagraph)
}
@ -552,23 +571,36 @@ func (i *Invoice) drawAddress(title, name string, addr *InvoiceAddress) []*Style
addressParagraph := newStyledParagraph(i.addressStyle)
addressParagraph.SetLineHeight(1.2)
city := addr.City
if addr.Zip != "" {
if city != "" {
city += ", "
}
city += addr.Zip
separator := addr.Separator
if separator == "" {
separator = i.addrSeparator
}
if name != "" {
locality := addr.City
if addr.State != "" {
if locality != "" {
locality += separator
}
locality += addr.State
}
if addr.Zip != "" {
if locality != "" {
locality += separator
}
locality += addr.Zip
}
if addr.Name != "" {
addressParagraph.Append(addr.Name + "\n")
}
if addr.Street != "" {
addressParagraph.Append(addr.Street + "\n")
}
if city != "" {
addressParagraph.Append(city + "\n")
if addr.Street2 != "" {
addressParagraph.Append(addr.Street2 + "\n")
}
if locality != "" {
addressParagraph.Append(locality + "\n")
}
if addr.Country != "" {
addressParagraph.Append(addr.Country + "\n")
@ -727,10 +759,9 @@ func (i *Invoice) generateInformationBlocks(ctx DrawContext) ([]*Block, DrawCont
separatorParagraph := newStyledParagraph(i.defaultStyle)
separatorParagraph.SetMargins(0, 0, 0, 20)
addrParagraphs := i.drawAddress(i.sellerAddress.Name, "", i.sellerAddress)
addrParagraphs := i.drawAddress(i.sellerAddress)
addrParagraphs = append(addrParagraphs, separatorParagraph)
addrParagraphs = append(addrParagraphs,
i.drawAddress("Bill to", i.buyerAddress.Name, i.buyerAddress)...)
addrParagraphs = append(addrParagraphs, i.drawAddress(i.buyerAddress)...)
addrDivision := newDivision()
for _, addrParagraph := range addrParagraphs {

View File

@ -30,7 +30,7 @@ func TestInvoiceSimple(t *testing.T) {
// Set invoice addresses.
invoice.SetSellerAddress(&InvoiceAddress{
Name: "John Doe",
Heading: "John Doe",
Street: "8 Elm Street",
City: "Cambridge",
Zip: "CB14DH",
@ -40,6 +40,7 @@ func TestInvoiceSimple(t *testing.T) {
})
invoice.SetBuyerAddress(&InvoiceAddress{
Heading: "Bill to",
Name: "Jane Doe",
Street: "9 Elm Street",
City: "London",
@ -127,7 +128,7 @@ func TestInvoiceAdvanced(t *testing.T) {
// Set invoice addresses.
invoice.SetSellerAddress(&InvoiceAddress{
Name: "John Doe",
Heading: "JOHN DOE",
Street: "8 Elm Street",
City: "Cambridge",
Zip: "CB14DH",
@ -137,13 +138,17 @@ func TestInvoiceAdvanced(t *testing.T) {
})
invoice.SetBuyerAddress(&InvoiceAddress{
Name: "Jane Doe",
Street: "9 Elm Street",
City: "London",
Zip: "LB15FH",
Country: "United Kingdom",
Phone: "xxx-xxx-xxxx",
Email: "janedoe@email.com",
Heading: "JANE DOE",
Name: "Jane Doe and Associates",
Street: "Suite #134569",
Street2: "1960 W CHELSEA AVE STE 2006R",
City: "ALLENTOWN",
State: "PA",
Zip: "18104",
Country: "United States",
Phone: "xxx-xxx-xxxx",
Email: "janedoe@email.com",
Separator: " ",
})
// Customize address styles.
@ -198,9 +203,12 @@ func TestInvoiceAdvanced(t *testing.T) {
invoice.SetTotal("$85.00")
// Set invoice content sections.
invoice.SetNotes("Notes", "Thank you for your business.")
invoice.SetTerms("Terms and conditions", "Full refund for 60 days after purchase.")
invoice.AddSection("Custom section", "This is a custom section.")
invoice.SetNotes("NOTES", "Thank you for your business.")
invoice.SetTerms("I. TERMS OF PAYMENT", "Net 30 days on all invoices. In addition, Buyer shall pay all sales, use, customs, excise or other taxes presently or hereafter payable in regards to this transaction, and Buyer shall reimburse Seller for any such taxes or charges paid by the Seller.\nSeller shall have the continuing right to approve Buyers credit. Seller may at any time demand advance payment, additional security or guarantee of prompt payment. If Buyer refuses to give the payment, security or guarantee demanded, Seller may terminate the Agreement, refuse to deliver any undelivered goods and Buyer shall immediately become liable to Seller for the unpaid price of all goods delivered & for damages as provided in Paragraph V below. Buyer agrees to pay Seller cost of collection of overdue invoices, including reasonable attorneys fees incurred by Seller in collecting said sums. F.O.B. point shall be point of SHIP TO on face hereof.")
invoice.AddSection("II. DELIVERY, TOLERANCES, WEIGHT", "Upon due tender of goods for delivery at the F.O.B. point all risk of loss or damage and other incident of ownership pass to Buyer, but Seller retains a security interest in the goods until purchase price is paid. All deliveries are subject to weight at shipping point which shall govern.")
invoice.AddSection("III. WARRANTIES", "Seller warrants that goods sold hereunder are merchantable UNLESS manufactured in conformance with Buyers particular specification, and that Seller conveys good title thereto. IN NO EVENT WILL SELLER BE LIABLE FOR CONSEQUENTIAL DAMAGES EVEN IF CUSTOMER HAS NOT BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. EXCEPT FOR THE EXPRESS WARRANTY STATED IN THIS PARAGRAPH IV, SELLER GRANTS NO WARRANTIES, EITHER EXPRESS OR IMPLIED HEREIN, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, AND THIS STATED EXPRESS WARRANTY IS IN LIEU OF ALL LIABILITIES OR OBLIGATIONS OF SELLER FOR DAMAGES INCLUDING BUT NOT LIMITED TO, CONSEQUENTIAL DAMAGES OCCURRING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF ANY GOODS SOLD HEREUNDER. Seller specifically does not warrant the accuracy of sufficiency of any advice or recommendations given to Buyer in connection with the sale of goods hereunder.")
invoice.AddSection("IV. FORCE MAJEURE", "Seller shall not be liable for any damages resulting from: any delay or failure of performance arising from any cause not reasonably within Sellers control; accidents to, breakdowns or mechanical failure of machinery or equipment, however caused; strikes or other labor troubles, shortage of labor, transportation, raw materials, energy sources, or failure of ususal means of supply; fire; flood; war, declared or undeclared; insurrection; riots; acts of God or the public enemy; or priorities, allocations or limitations or other acts required or requested by Federal, State or local governments or any of their sub-divisions, bureaus or agencies. Seller may, at its option, cancel this Agreement or delay performance hereunder for any period reasonably necessary due to any of the foregoing, during which time this Agreement shall remain in full force and effect. Seller shall have the further right to then allocate its available goods between its own uses and its customers in such manner as Seller may consider equitable.")
invoice.AddSection("V. PATENT INDEMNITY", "Seller shall defend and hold Buyer harmless for any action against Seller based in a claim that Buyers sale or use of goods normally offered for sale by Seller, supplied by Seller hereunder, and while in the form, state or conditions supplies constitutes infringement of any United States letters patent; provided Seller shall receive prompt written notice of the claim or action, and Buyer shall give Seller authority, information and assistance at Sellers expense. Buyer shall defend and hold Seller harmless for any action against Seller or its suppliers based in a claim that the manufacture or sale of goods hereunder constitutes infringement of any United States letters patent, if such goods were manufactured pursuant to Buyers designs, specifications and /or formulae, and were not normally offered for sale by Seller; provided Buyer shall receive prompt written notice of the claim or action and Seller shall give Buyer authority, information and assistance at Buyers expense. Buyer and Seller agree that the foregoing constitutes the parties entire liability for claims or actions based on patent infringement.")
// Customize note styles.
noteStyle := invoice.NoteStyle()

View File

@ -507,7 +507,7 @@ func (p *StyledParagraph) wrapText() error {
return nil
}
// GeneratePageBlocks generates the page blocks. Multiple blocks are generated
// GeneratePageBlocks generates the page blocks. Multiple blocks are generated
// if the contents wrap over multiple pages. Implements the Drawable interface.
func (p *StyledParagraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
origContext := ctx
@ -523,29 +523,9 @@ func (p *StyledParagraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawCon
// Use available space.
p.SetWidth(ctx.Width)
if p.Height() > ctx.Height {
// Goes out of the bounds. Write on a new template instead and create a new context at upper
// left corner.
// TODO: Handle case when Paragraph is larger than the Page...
// Should be fine if we just break on the paragraph, i.e. splitting it up over 2+ pages
blocks = append(blocks, blk)
blk = NewBlock(ctx.PageWidth, ctx.PageHeight)
// New Page.
ctx.Page++
newContext := ctx
newContext.Y = ctx.Margins.top // + p.Margins.top
newContext.X = ctx.Margins.left + p.margins.left
newContext.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom - p.margins.bottom
newContext.Width = ctx.PageWidth - ctx.Margins.left - ctx.Margins.right - p.margins.left - p.margins.right
ctx = newContext
}
} else {
// Absolute.
// Absolute. Use necessary space.
if int(p.wrapWidth) <= 0 {
// Use necessary space.
p.SetWidth(p.getTextWidth())
}
ctx.X = p.xPos
@ -556,14 +536,40 @@ func (p *StyledParagraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawCon
p.beforeRender(p, ctx)
}
// Place the Paragraph on the template at position (x,y) based on the ctx.
ctx, err := drawStyledParagraphOnBlock(blk, p, ctx)
if err != nil {
common.Log.Debug("ERROR: %v", err)
// Wrap paragraph chunks into lines, based on the available context width.
if err := p.wrapText(); err != nil {
return nil, ctx, err
}
blocks = append(blocks, blk)
// Draw paragraph blocks.
lines := p.lines
for {
// Draw paragraph on block.
newCtx, remaining, err := drawStyledParagraphOnBlock(blk, p, lines, ctx)
if err != nil {
common.Log.Debug("ERROR: %v", err)
return nil, ctx, err
}
ctx = newCtx
blocks = append(blocks, blk)
// If the current block height was not sufficient for all the paragraph
// lines, create a new block to draw the remaining lines on.
if lines = remaining; len(remaining) == 0 {
break
}
// Create new page block.
blk = NewBlock(ctx.PageWidth, ctx.PageHeight)
ctx.Page++
newCtx = ctx
newCtx.Y = ctx.Margins.top
newCtx.X = ctx.Margins.left + p.margins.left
newCtx.Height = ctx.PageHeight - ctx.Margins.top - ctx.Margins.bottom - p.margins.bottom
newCtx.Width = ctx.PageWidth - ctx.Margins.left - ctx.Margins.right - p.margins.left - p.margins.right
ctx = newCtx
}
if p.positioning.isRelative() {
ctx.X -= p.margins.left // Move back.
ctx.Width = origContext.Width
@ -574,7 +580,7 @@ func (p *StyledParagraph) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawCon
}
// Draw block on specified location on Page, adding to the content stream.
func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext) (DrawContext, error) {
func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, lines [][]*TextChunk, ctx DrawContext) (DrawContext, [][]*TextChunk, error) {
// Find first free index for the font resources of the paragraph.
num := 1
fontName := core.PdfObjectName(fmt.Sprintf("Font%d", num))
@ -586,40 +592,51 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
// Add default font to the page resources.
err := blk.resources.SetFontByName(fontName, p.defaultStyle.Font.ToPdfObject())
if err != nil {
return ctx, err
return ctx, nil, err
}
num++
defaultFontName := fontName
defaultFontSize := p.defaultStyle.FontSize
// Wrap the text into lines.
p.wrapText()
// Add the fonts of all chunks to the page resources.
relativePos := p.positioning.isRelative()
var fonts [][]core.PdfObjectName
var yOffset float64
for i, line := range p.lines {
var nextBlockLines [][]*TextChunk
var totalHeight float64
for i, line := range lines {
var fontLine []core.PdfObjectName
var height float64
for _, chunk := range line {
style := chunk.Style
if i == 0 && style.FontSize > yOffset {
yOffset = style.FontSize
}
if style.FontSize > height {
height = style.FontSize
}
fontName = core.PdfObjectName(fmt.Sprintf("Font%d", num))
err := blk.resources.SetFontByName(fontName, style.Font.ToPdfObject())
if err != nil {
return ctx, err
return ctx, nil, err
}
fontLine = append(fontLine, fontName)
num++
}
// Check if line fits on the current block.
height *= p.lineHeight
if relativePos && totalHeight+height > ctx.Height {
nextBlockLines = lines[i:]
lines = lines[:i]
break
}
totalHeight += height
fonts = append(fonts, fontLine)
}
@ -637,7 +654,7 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
cc.Add_BT()
currY := yPos
for idx, line := range p.lines {
for idx, line := range lines {
currX := ctx.X
if idx != 0 {
@ -645,7 +662,7 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
cc.Add_Tstar()
}
isLastLine := idx == len(p.lines)-1
isLastLine := idx == len(lines)-1
// Get width of the line (excluding spaces).
var (
@ -665,7 +682,7 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
spaceMetrics, found := style.Font.GetRuneMetrics(' ')
if !found {
return ctx, errors.New("the font does not have a space glyph")
return ctx, nil, errors.New("the font does not have a space glyph")
}
var chunkSpaces uint
@ -683,7 +700,7 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
metrics, found := style.Font.GetRuneMetrics(r)
if !found {
common.Log.Debug("Unsupported rune %v in font\n", r)
return ctx, errors.New("unsupported text glyph")
return ctx, nil, errors.New("unsupported text glyph")
}
chunkWidth += style.FontSize * metrics.Wx
@ -750,7 +767,7 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
if p.alignment != TextAlignmentJustify || isLastLine {
spaceMetrics, found := style.Font.GetRuneMetrics(' ')
if !found {
return ctx, errors.New("the font does not have a space glyph")
return ctx, nil, errors.New("the font does not have a space glyph")
}
fontName = fonts[idx][k]
@ -870,8 +887,8 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
blk.addContents(ops)
if p.positioning.isRelative() {
pHeight := p.Height() + p.margins.bottom
if relativePos {
pHeight := totalHeight + p.margins.bottom
ctx.Y += pHeight
ctx.Height -= pHeight
@ -881,5 +898,5 @@ func drawStyledParagraphOnBlock(blk *Block, p *StyledParagraph, ctx DrawContext)
}
}
return ctx, nil
return ctx, nextBlockLines, nil
}

File diff suppressed because one or more lines are too long

View File

@ -373,21 +373,20 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
case *Division:
div := t
ctx := DrawContext{
X: xrel,
Y: yrel,
Width: w,
}
c := ctx
c.X = xrel
c.Y = yrel
c.Width = w
// Mock call to generate page blocks.
divBlocks, updCtx, err := div.GeneratePageBlocks(ctx)
divBlocks, _, err := div.GeneratePageBlocks(c)
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
newh := c.Height - h
if newh > h {
diffh := newh - h
// Add diff to last row.
@ -395,10 +394,8 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
}
}
newh := div.Height() + div.margins.top + div.margins.bottom
_ = updCtx
// Get available width and height.
newh := div.Height() + div.margins.top + div.margins.bottom
if newh > h {
diffh := newh - h
// Add diff to last row.
@ -604,6 +601,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
}
ctx.Y += h
ctx.Height -= h
// Resume previous state after headers have been rendered.
if drawingHeaders && cellIdx+1 > endHeaderCell {
@ -629,6 +627,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext,
ctx.Width = origCtx.Width
// Add the bottom margin.
ctx.Y += table.margins.bottom
ctx.Height -= table.margins.bottom
return blocks, ctx, nil
}