diff --git a/README.md b/README.md index 7c6bd3c80..414ad8f29 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,50 @@ # UniPDF - PDF for Go -[UniDoc](http://unidoc.io)'s UniPDF is a powerful PDF library for Go (golang) with capabilities for -creating and processing PDF files. The library is written and supported by -the [FoxyUtils.com](https://foxyutils.com) website, where the library is used to power -many of the PDF services offered. +[UniDoc](http://unidoc.io)'s UniPDF (formerly unidoc) is a PDF library for Go (golang) with capabilities for +creating and reading, processing PDF files. The library is written and supported by +[FoxyUtils.com](https://foxyutils.com), where the library is used to power many of its services. [![Build Status](https://app.wercker.com/status/22b50db125a6d376080f3f0c80d085fa/s/master "wercker status")](https://app.wercker.com/project/bykey/22b50db125a6d376080f3f0c80d085fa) +[![GitHub (pre-)release](https://img.shields.io/github/release/unidoc/unipdf/all.svg)](https://github.com/unidoc/unipdf/releases) [![License: AGPL v3](https://img.shields.io/badge/License-Dual%20AGPL%20v3/Commercial-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) [![Go Report Card](https://goreportcard.com/badge/github.com/unidoc/unipdf)](https://goreportcard.com/report/github.com/unidoc/unipdf) [![GoDoc](https://godoc.org/github.com/unidoc/unipdf?status.svg)](https://godoc.org/github.com/unidoc/unipdf) -## News -- unidoc is being renamed to unipdf and will be maintained under https://github.com/unidoc/unipdf -- The old repository will remain under https://github.com/unidoc/unidoc for backwards compatibility and will be read-only. -All development will be under the unipdf repository. -- The initial release of unipdf v3.0.0 will be compliant with Go modules from the start. +## Features +- [Create PDF reports](https://github.com/unidoc/unipdf-examples/blob/v3/report/pdf_report.go) +- [Invoice creation](https://unidoc.io/news/simple-invoices) +- Advanced table generation in the creator with subtable support +- Paragraph in creator handling multiple styles within the same paragraph +- [Merge PDF pages](https://github.com/unidoc/unipdf-examples/blob/v3/pages/pdf_merge.go) +- [Split PDF pages](https://github.com/unidoc/unipdf-examples/blob/v3/pages/pdf_split.go) and change page order +- [Rotate pages](https://github.com/unidoc/unipdf-examples/blob/v3/pages/pdf_rotate.go) +- [Extract text from PDF files](https://github.com/unidoc/unipdf-examples/blob/v3/text/pdf_extract_text.go) +- [Extract images](https://github.com/unidoc/unipdf-examples/blob/v3/image/pdf_extract_images.go) with coordinates +- [Images to PDF](https://github.com/unidoc/unipdf-examples/blob/v3/image/pdf_images_to_pdf.go) +- [Add images to pages](https://github.com/unidoc/unipdf-examples/blob/v3/image/pdf_add_image_to_page.go) +- [Compress and optimize PDF](https://github.com/unidoc/unipdf-examples/blob/v3/compress/pdf_optimize.go) +- [Watermark PDF files](https://github.com/unidoc/unipdf-examples/blob/v3/image/pdf_watermark_image.go) +- Advanced page manipulation (blocks/templates) +- Load PDF templates and modify +- [Form creation](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_add.go) +- [Fill and flatten forms](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_flatten.go) +- [Fill out forms](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_fill_json.go) and [FDF merging](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_fill_fdf_merge.go) +- [Unlock PDF files / remove password](https://github.com/unidoc/unipdf-examples/blob/v3/security/pdf_unlock.go) +- [Protect PDF files with a password](https://github.com/unidoc/unipdf-examples/blob/v3/security/pdf_protect.go) +- [Digital signing validation and signing](https://github.com/unidoc/unipdf-examples/tree/v3/signatures) +- CCITTFaxDecode decoding and encoding support -## Features -unipdf has a powerful set of features both for reading, processing and writing PDF. -The following list describes some key features: - -- [x] [Create PDF reports](https://github.com/unidoc/unipdf-examples/blob/v3/report/pdf_report.go) -- [x] [Create PDF invoices](https://unidoc.io/news/simple-invoices) -- [x] Advanced table generation in the creator with subtable support -- [x] Paragraph in creator handling multiple styles within the same paragraph -- [x] Table of contents automatically generated -- [x] Text extraction significantly improved in quality and foundation in place for vectorized (position-based) text extraction (XY) -- [x] Image extraction with coordinates -- [x] [Merge PDF pages](https://github.com/unidoc/unipdf-examples/blob/v3/pages/pdf_merge.go) -- [x] Merge page contents -- [x] [Split PDF pages and change page order](https://github.com/unidoc/unipdf-examples/blob/v3/pages/pdf_split.go) -- [x] [Rotate pages](https://github.com/unidoc/unipdf-examples/blob/v3/pages/pdf_rotate.go) -- [x] [Extract text from PDF files](https://github.com/unidoc/unipdf-examples/blob/v3/text/pdf_extract_text.go) -- [x] Extract images -- [x] Add images to pages -- [x] [Compress and optimize PDF output](https://github.com/unidoc/unipdf-examples/blob/v3/compress/pdf_optimize.g) -- [x] [Draw watermark on PDF files](https://github.com/unidoc/unipdf-examples/blob/v3/image/pdf_watermark_image.go) -- [x] Advanced page manipulation (blocks/templates) -- [x] Load PDF templates and modify -- [x] [Flatten forms and generate appearance streams](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_flatten.go) -- [x] [Fill out forms and FDF merging](https://github.com/unidoc/unipdf-examples/tree/v3/forms) -- [x] [FDF merge](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_fill_fdf_merge.go) and [form filling via JSON data](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_fill_json.go) -- [x] [Form creation](https://github.com/unidoc/unipdf-examples/blob/v3/forms/pdf_form_add.go) -- [x] [Unlock PDF files / remove password](https://github.com/unidoc/unipdf-examples/blob/v3/security/pdf_unlock.go) -- [x] [Protect PDF files with a password](https://github.com/unidoc/unipdf-examples/blob/v3/security/pdf_protect.go) -- [x] [Digital signing validation and signing](https://github.com/unidoc/unipdf-examples/tree/v3/signatures) -- [x] CCITTFaxDecode decoding and encoding support -- [x] Append mode +Multiple examples are provided in our example repository https://github.com/unidoc/unidoc-examples +as well as [documented examples](https://unidoc.io/examples) on our website. + +Contact us if you need any specific examples. + +## News +- unidoc has been renamed to unipdf and is maintained under https://github.com/unidoc/unipdf +- The old repository remains under https://github.com/unidoc/unidoc for backwards compatibility and will be read-only. +All development is under the unipdf repository. +- The initial release of unipdf v3.0.0 is compatible with Go modules from the start. ## Installation With modules: @@ -55,6 +52,11 @@ With modules: go get github.com/unidoc/unipdf/v3 ~~~ +With GOPATH: +~~~ +go get github.com/unidoc/unipdf/... +~~~ + ## How can I convince myself and my boss to buy unipdf rather using a free alternative? @@ -67,13 +69,6 @@ Security. We take security very seriously and we restrict access to github.com/ The profits are invested back into making unipdf better. We want to make the best possible product and in order to do that we need the best people to contribute. A large fraction of the profits made goes back into developing unipdf. That way we have been able to get many excellent people to work and contribute to unipdf that would not be able to contribute their work for free. -## Examples - -Multiple examples are provided in our example repository https://github.com/unidoc/unidoc-examples -as well as [documented examples](https://unidoc.io/examples) on our website. - -Contact us if you need any specific examples. - ## Contributing [![CLA assistant](https://cla-assistant.io/readme/badge/unidoc/unipdf)](https://cla-assistant.io/unidoc/unipdf) diff --git a/common/version.go b/common/version.go index f3e5f4ca4..0a66bb3fa 100644 --- a/common/version.go +++ b/common/version.go @@ -11,12 +11,12 @@ import ( ) const releaseYear = 2019 -const releaseMonth = 4 -const releaseDay = 20 -const releaseHour = 23 +const releaseMonth = 6 +const releaseDay = 2 +const releaseHour = 11 const releaseMin = 30 // Version holds version information, when bumping this make sure to bump the released at stamp also. -const Version = "3.0.0-rc.1" +const Version = "3.0.1" var ReleasedAt = time.Date(releaseYear, releaseMonth, releaseDay, releaseHour, releaseMin, 0, 0, time.UTC) diff --git a/core/primitives.go b/core/primitives.go index 439d6c227..d2e8ca816 100644 --- a/core/primitives.go +++ b/core/primitives.go @@ -605,13 +605,16 @@ func (array *PdfObjectArray) GetAsFloat64Slice() ([]float64, error) { } // Merge merges in key/values from another dictionary. Overwriting if has same keys. -func (d *PdfObjectDictionary) Merge(another *PdfObjectDictionary) { +// The mutated dictionary (d) is returned in order to allow method chaining. +func (d *PdfObjectDictionary) Merge(another *PdfObjectDictionary) *PdfObjectDictionary { if another != nil { for _, key := range another.Keys() { val := another.Get(key) d.Set(key, val) } } + + return d } // String returns a string describing `d`. diff --git a/creator/styled_paragraph.go b/creator/styled_paragraph.go index fa39c0f4f..1359194ec 100644 --- a/creator/styled_paragraph.go +++ b/creator/styled_paragraph.go @@ -226,6 +226,49 @@ func (p *StyledParagraph) Height() float64 { return height } +// getLineHeight returns both the capheight and the font size based height of +// the line with the specified index. +func (p *StyledParagraph) getLineHeight(idx int) (capHeight, height float64) { + if p.lines == nil || len(p.lines) == 0 { + p.wrapText() + } + if idx < 0 || idx > len(p.lines)-1 { + common.Log.Debug("ERROR: invalid paragraph line index %d. Returning 0, 0", idx) + return 0, 0 + } + + line := p.lines[idx] + for _, chunk := range line { + descriptor, err := chunk.Style.Font.GetFontDescriptor() + if err != nil { + common.Log.Debug("ERROR: Unable to get font descriptor") + } + + var fontCapHeight float64 + if descriptor != nil { + if fontCapHeight, err = descriptor.GetCapHeight(); err != nil { + common.Log.Debug("ERROR: Unable to get font CapHeight: %v", err) + } + } + if int(fontCapHeight) <= 0 { + common.Log.Debug("WARN: CapHeight not available - setting to 1000") + fontCapHeight = 1000 + } + + h := fontCapHeight / 1000.0 * chunk.Style.FontSize * p.lineHeight + if h > capHeight { + capHeight = h + } + + h = p.lineHeight * chunk.Style.FontSize + if h > height { + height = h + } + } + + return capHeight, height +} + // getTextWidth calculates the text width as if all in one line (not taking // wrapping into account). func (p *StyledParagraph) getTextWidth() float64 { diff --git a/creator/styled_paragraph_test.go b/creator/styled_paragraph_test.go index d82acc549..9829b4b12 100644 --- a/creator/styled_paragraph_test.go +++ b/creator/styled_paragraph_test.go @@ -798,3 +798,66 @@ func TestStyledLinkRotation(t *testing.T) { t.Fatalf("Fail: %v\n", err) } } + +func TestStyledParagraphTableVerticalAlignment(t *testing.T) { + c := New() + + fontRegular := newStandard14Font(t, model.CourierName) + + createTable := func(c *Creator, text string, align CellVerticalAlignment, fontSize float64) { + textStyle := c.NewTextStyle() + textStyle.Font = fontRegular + textStyle.FontSize = fontSize + + table := c.NewTable(1) + table.SetMargins(0, 0, 5, 5) + + cell := table.NewCell() + sp := c.NewStyledParagraph() + textChunk := sp.Append(text) + textChunk.Style = textStyle + + cell.SetVerticalAlignment(align) + cell.SetContent(sp) + cell.SetBorder(CellBorderSideAll, CellBorderStyleSingle, 1) + + if err := c.Draw(table); err != nil { + t.Fatalf("Error drawing: %v", err) + } + } + + chunks := []string{ + "TR", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lacus viverra vitae congue eu consequat. Cras adipiscing enim eu turpis. Lectus magna fringilla urna porttitor. Condimentum id venenatis a condimentum. Quis ipsum suspendisse ultrices gravida dictum fusce. In fermentum posuere urna nec tincidunt.", + } + + alignments := []struct { + Label string + Alignment CellVerticalAlignment + }{ + {"Top alignment", CellVerticalAlignmentTop}, + {"Middle alignment", CellVerticalAlignmentMiddle}, + {"Bottom alignment", CellVerticalAlignmentBottom}, + } + + for _, chunk := range chunks { + for _, alignment := range alignments { + c.NewPage() + + sp := c.NewStyledParagraph() + sp.Append(alignment.Label).Style.FontSize = 16 + sp.SetMargins(0, 0, 0, 5) + + if err := c.Draw(sp); err != nil { + t.Fatalf("Error drawing: %v", err) + } + + for i := 4; i <= 20; i += 2 { + createTable(c, chunk, alignment.Alignment, float64(i)) + } + } + } + + // Write output file. + testWriteAndRender(t, c, "styled_paragraph_table_vertical_align.pdf") +} diff --git a/creator/table.go b/creator/table.go index 585f2cd65..4e5aa843c 100644 --- a/creator/table.go +++ b/creator/table.go @@ -516,8 +516,10 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, } if cell.content != nil { - // content width. - cw := cell.content.Width() + cw := cell.content.Width() // content width. + ch := cell.content.Height() // content height. + vertOffset := 0.0 + switch t := cell.content.(type) { case *Paragraph: if t.enableWrap { @@ -527,6 +529,26 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, if t.enableWrap { cw = t.getMaxLineWidth() / 1000.0 } + + // Calculate the height of the paragraph. + lineCapHeight, lineHeight := t.getLineHeight(0) + if len(t.lines) == 1 { + ch = lineCapHeight + } else { + ch = ch - lineHeight + lineCapHeight + } + + // Account for the top offset the paragraph adds. + vertOffset = lineCapHeight - t.defaultStyle.FontSize*t.lineHeight + + switch cell.verticalAlignment { + case CellVerticalAlignmentTop: + // Add a bit of space from the top border of the cell. + vertOffset += lineCapHeight * 0.5 + case CellVerticalAlignmentBottom: + // Add a bit of space from the bottom border of the cell. + vertOffset -= lineCapHeight * 0.5 + } case *Table: cw = w case *List: @@ -553,8 +575,9 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, } } + ctx.Y += vertOffset + // Account for vertical alignment. - ch := cell.content.Height() // content height. switch cell.verticalAlignment { case CellVerticalAlignmentTop: // Default: do nothing. @@ -567,7 +590,7 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, case CellVerticalAlignmentBottom: if h > ch { ctx.Y = ctx.Y + h - ch - ctx.Height = ch + ctx.Height = h } } @@ -575,6 +598,8 @@ func (table *Table) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, if err != nil { common.Log.Debug("ERROR: %v", err) } + + ctx.Y -= vertOffset } ctx.Y += h diff --git a/internal/e2etest/split_test.go b/internal/e2etest/split_test.go index f471a96dc..b49cf435a 100644 --- a/internal/e2etest/split_test.go +++ b/internal/e2etest/split_test.go @@ -30,8 +30,8 @@ var ( // knownHashes defines a list of known output hashes to ensure that the output is constant. // If there is a change in hash need to find out why and update only if the change is accepted. var knownHashes = map[string]string{ - "bf7c9d5dabc7e7ec2fc0cf9db2d9c8e7aa456fca.pdf": "f7891d491fa9f20ed2975dd28961c205", - "371dce2c2720581a3eef3f123e5741dd3566ef87.pdf": "4a25934226b6b64e5d95d571260b1f01", + "bf7c9d5dabc7e7ec2fc0cf9db2d9c8e7aa456fca.pdf": "fdd638603c6f655babbc90358de66107", + "371dce2c2720581a3eef3f123e5741dd3566ef87.pdf": "4c5356ac623a96004d80315f24613fff", "e815311526b50036db6e89c54af2b9626edecf30.pdf": "97dcfdde59a2f3a6eb105d0c31ebd3fb", "3bf64014e0c9e4a56f1a9363f1b34fd707bd9fa0.pdf": "6f310c9fdd44d49766d3cc32d3053b89", "004feecd47e2da4f2ed5cdbbf4791a77dd59ce20.pdf": "309a072a97d0566aa3f85edae504bb53", diff --git a/model/appender_test.go b/model/appender_test.go index 1e4e6104a..9af5938bd 100644 --- a/model/appender_test.go +++ b/model/appender_test.go @@ -66,6 +66,8 @@ func TestAppenderNoop(t *testing.T) { appender, err := model.NewPdfAppender(reader) require.NoError(t, err) + model.SetPdfProducer("UniPDF") + model.SetPdfCreator("UniDoc UniPDF") err = appender.WriteToFile(tempFile("appender_noop.pdf")) if err != nil { t.Errorf("Fail: %v\n", err) @@ -123,7 +125,7 @@ func TestAppenderNoop(t *testing.T) { 3: core.XrefObject{ObjectNumber: 3, XType: 0, Offset: 178}, 4: core.XrefObject{ObjectNumber: 4, XType: 0, Offset: 457}, 5: core.XrefObject{ObjectNumber: 5, XType: 0, Offset: 740}, - 6: core.XrefObject{ObjectNumber: 6, XType: 0, Offset: 860}, + 6: core.XrefObject{ObjectNumber: 6, XType: 0, Offset: 802}, } require.Equal(t, expected, xrefs.ObjectMap) } diff --git a/model/writer.go b/model/writer.go index 5999fb59c..192120f02 100644 --- a/model/writer.go +++ b/model/writer.go @@ -532,11 +532,11 @@ func (w *PdfWriter) addObjects(obj core.PdfObject) error { // AddPage adds a page to the PDF file. The new page should be an indirect object. func (w *PdfWriter) AddPage(page *PdfPage) error { + procPage(page) obj := page.ToPdfObject() common.Log.Trace("==========") common.Log.Trace("Appending to page list %T", obj) - procPage(page) pageObj, ok := obj.(*core.PdfIndirectObject) if !ok { @@ -627,14 +627,16 @@ func procPage(p *PdfPage) { return } - // Add font as needed. - f := DefaultFont() - p.Resources.SetFontByName("UF1", f.ToPdfObject()) + // Add font, if needed. + fontName := core.PdfObjectName("UF1") + if !p.Resources.HasFontByName(fontName) { + p.Resources.SetFontByName(fontName, DefaultFont().ToPdfObject()) + } var ops []string ops = append(ops, "q") ops = append(ops, "BT") - ops = append(ops, "/UF1 14 Tf") + ops = append(ops, fmt.Sprintf("/%s 14 Tf", fontName.String())) ops = append(ops, "1 0 0 rg") ops = append(ops, "10 10 Td") s := "Unlicensed UniDoc - Get a license on https://unidoc.io"