Skip to content

Commit

Permalink
Merge branch 'benoitkugler-master'
Browse files Browse the repository at this point in the history
  • Loading branch information
jung-kurt committed Nov 9, 2019
2 parents 47a2c24 + 9fbe8c0 commit 50ed053
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 7 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,9 @@ printing PDF documents. Wojciech Matusiak added supported for word
spacing. Artem Korotkiy added support of UTF-8 fonts. Dave Barnes added
support for imported objects and templates. Brigham Thompson added
support for rounded rectangles. Joe Westcott added underline
functionality and optimized image storage.
functionality and optimized image storage. Benoit KUGLER contributed
support for rectangles with corners of unequal radius, and for file
attachments and annotations.

## Roadmap

Expand Down
157 changes: 157 additions & 0 deletions attachments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package gofpdf

import (
"crypto/md5"
"encoding/hex"
"fmt"
"strings"
)

// Attachment defines a content to be included in the pdf, in one
// of the following ways :
// - associated with the document as a whole : see SetAttachments()
// - accessible via a link localized on a page : see AddAttachmentAnnotation()
type Attachment struct {
Content []byte

// Filename is the displayed name of the attachment
Filename string

// Description is only displayed when using AddAttachmentAnnotation(),
// and might be modified by the pdf reader.
Description string

objectNumber int // filled when content is included
}

// return the hex encoded checksum of `data`
func checksum(data []byte) string {
tmp := md5.Sum(data)
sl := make([]byte, len(tmp))
for i, v := range tmp {
sl[i] = v
}
return hex.EncodeToString(sl)
}

// Writes a compressed file like object as ``/EmbeddedFile``. Compressing is
// done with deflate. Includes length, compressed length and MD5 checksum.
func (f *Fpdf) writeCompressedFileObject(content []byte) {
lenUncompressed := len(content)
sum := checksum(content)
compressed := sliceCompress(content)
lenCompressed := len(compressed)
f.newobj()
f.outf("<< /Type /EmbeddedFile /Length %d /Filter /FlateDecode /Params << /CheckSum <%s> /Size %d >> >>\n",
lenCompressed, sum, lenUncompressed)
f.putstream(compressed)
f.out("endobj")
}

// Embed includes the content of `a`, and update its internal reference.
func (f *Fpdf) embed(a *Attachment) {
if a.objectNumber != 0 { // already embedded (objectNumber start at 2)
return
}
oldState := f.state
f.state = 1 // we write file content in the main buffer
f.writeCompressedFileObject(a.Content)
streamID := f.n
f.newobj()
f.outf("<< /Type /Filespec /F () /UF %s /EF << /F %d 0 R >> /Desc %s\n>>",
f.textstring(utf8toutf16(a.Filename)),
streamID,
f.textstring(utf8toutf16(a.Description)))
f.out("endobj")
a.objectNumber = f.n
f.state = oldState
}

// SetAttachments writes attachments as embedded files (document attachment).
// These attachments are global, see AddAttachmentAnnotation() for a link
// anchored in a page. Note that only the last call of SetAttachments is
// useful, previous calls are discarded. Be aware that not all PDF readers
// support document attachments. See the SetAttachment example for a
// demonstration of this method.
func (f *Fpdf) SetAttachments(as []Attachment) {
f.attachments = as
}

// embed current attachments. store object numbers
// for later use by getEmbeddedFiles()
func (f *Fpdf) putAttachments() {
for i, a := range f.attachments {
f.embed(&a)
f.attachments[i] = a
}
}

// return /EmbeddedFiles tree name catalog entry.
func (f Fpdf) getEmbeddedFiles() string {
names := make([]string, len(f.attachments))
for i, as := range f.attachments {
names[i] = fmt.Sprintf("(Attachement%d) %d 0 R ", i+1, as.objectNumber)
}
nameTree := fmt.Sprintf("<< /Names [\n %s \n] >>", strings.Join(names, "\n"))
return nameTree
}

// ---------------------------------- Annotations ----------------------------------

type annotationAttach struct {
*Attachment

x, y, w, h float64 // fpdf coordinates (y diff and scaling done)
}

// AddAttachmentAnnotation puts a link on the current page, on the rectangle
// defined by `x`, `y`, `w`, `h`. This link points towards the content defined
// in `a`, which is embedded in the document. Note than no drawing is done by
// this method : a method like `Cell()` or `Rect()` should be called to
// indicate to the reader that there is a link here. Requiring a pointer to an
// Attachment avoids useless copies in the resulting pdf: attachment pointing
// to the same data will have their content only be included once, and be
// shared amongst all links. Be aware that not all PDF readers support
// annotated attachments. See the AddAttachmentAnnotation example for a
// demonstration of this method.
func (f *Fpdf) AddAttachmentAnnotation(a *Attachment, x, y, w, h float64) {
if a == nil {
return
}
f.pageAttachments[f.page] = append(f.pageAttachments[f.page], annotationAttach{
Attachment: a,
x: x * f.k, y: f.hPt - y*f.k, w: w * f.k, h: h * f.k,
})
}

// embed current annotations attachments. store object numbers
// for later use by putAttachmentAnnotationLinks(), which is
// called for each page.
func (f *Fpdf) putAnnotationsAttachments() {
// avoid duplication
m := map[*Attachment]bool{}
for _, l := range f.pageAttachments {
for _, an := range l {
if m[an.Attachment] { // already embedded
continue
}
f.embed(an.Attachment)
}
}
}

func (f *Fpdf) putAttachmentAnnotationLinks(out *fmtBuffer, page int) {
for _, an := range f.pageAttachments[page] {
x1, y1, x2, y2 := an.x, an.y, an.x+an.w, an.y-an.h
as := fmt.Sprintf("<< /Type /XObject /Subtype /Form /BBox [%.2f %.2f %.2f %.2f] /Length 0 >>",
x1, y1, x2, y2)
as += "\nstream\nendstream"

out.printf("<< /Type /Annot /Subtype /FileAttachment /Rect [%.2f %.2f %.2f %.2f] /Border [0 0 0]\n",
x1, y1, x2, y2)
out.printf("/Contents %s ", f.textstring(utf8toutf16(an.Description)))
out.printf("/T %s ", f.textstring(utf8toutf16(an.Filename)))
out.printf("/AP << /N %s>>", as)
out.printf("/FS %d 0 R >>\n", an.objectNumber)
}
}
2 changes: 2 additions & 0 deletions def.go
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,8 @@ type Fpdf struct {
aliasMap map[string]string // map of alias->replacement
pageLinks [][]linkType // pageLinks[page][link], both 1-based
links []intLinkType // array of internal links
attachments []Attachment // slice of content to embed globally
pageAttachments [][]annotationAttach // 1-based array of annotation for file attachments (per page)
outlines []outlineType // array of outlines
outlineRoot int // root of outlines
autoPageBreak bool // automatic page breaking
Expand Down
4 changes: 3 additions & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ printing PDF documents. Wojciech Matusiak added supported for word
spacing. Artem Korotkiy added support of UTF-8 fonts. Dave Barnes added
support for imported objects and templates. Brigham Thompson added
support for rounded rectangles. Joe Westcott added underline
functionality and optimized image storage.
functionality and optimized image storage. Benoit KUGLER contributed
support for rectangles with corners of unequal radius, and for file
attachments and annotations.
Roadmap
Expand Down
4 changes: 3 additions & 1 deletion doc/document.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ also added support for page boxes used in printing PDF documents. Wojciech
Matusiak added supported for word spacing. Artem Korotkiy added support of
UTF-8 fonts. Dave Barnes added support for imported objects and templates.
Brigham Thompson added support for rounded rectangles. Joe Westcott added
underline functionality and optimized image storage.
underline functionality and optimized image storage. Benoit KUGLER contributed
support for rectangles with corners of unequal radius, and for file attachments
and annotations.

## Roadmap

Expand Down
24 changes: 20 additions & 4 deletions fpdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ func fpdfNew(orientationStr, unitStr, sizeStr, fontDirStr string, size SizeType)
f.pageLinks = append(f.pageLinks, make([]linkType, 0, 0)) // pageLinks[0] is unused (1-based)
f.links = make([]intLinkType, 0, 8)
f.links = append(f.links, intLinkType{}) // links[0] is unused (1-based)
f.pageAttachments = make([][]annotationAttach, 0, 8)
f.pageAttachments = append(f.pageAttachments, []annotationAttach{}) //
f.aliasMap = make(map[string]string)
f.inHeader = false
f.inFooter = false
Expand Down Expand Up @@ -3493,6 +3495,7 @@ func (f *Fpdf) beginpage(orientationStr string, size SizeType) {
}
f.pages = append(f.pages, bytes.NewBufferString(""))
f.pageLinks = append(f.pageLinks, make([]linkType, 0, 0))
f.pageAttachments = append(f.pageAttachments, []annotationAttach{})
f.state = 2
f.x = f.lMargin
f.y = f.tMargin
Expand Down Expand Up @@ -3852,9 +3855,11 @@ func (f *Fpdf) putpages() {
wPt = f.defPageSize.Ht * f.k
hPt = f.defPageSize.Wd * f.k
}
pagesObjectNumbers := make([]int, nb+1) // 1-based
for n := 1; n <= nb; n++ {
// Page
f.newobj()
pagesObjectNumbers[n] = f.n // save for /Kids
f.out("<</Type /Page")
f.out("/Parent 1 0 R")
pageSize, ok = f.pageSizes[n]
Expand All @@ -3866,7 +3871,7 @@ func (f *Fpdf) putpages() {
}
f.out("/Resources 2 0 R")
// Links
if len(f.pageLinks[n]) > 0 {
if len(f.pageLinks[n])+len(f.pageAttachments[n]) > 0 {
var annots fmtBuffer
annots.printf("/Annots [")
for _, pl := range f.pageLinks[n] {
Expand All @@ -3888,6 +3893,7 @@ func (f *Fpdf) putpages() {
annots.printf("/Dest [%d 0 R /XYZ 0 %.2f null]>>", 1+2*l.page, h-l.y*f.k)
}
}
f.putAttachmentAnnotationLinks(&annots, n)
annots.printf("]")
f.out(annots.String())
}
Expand All @@ -3914,8 +3920,8 @@ func (f *Fpdf) putpages() {
f.out("<</Type /Pages")
var kids fmtBuffer
kids.printf("/Kids [")
for i := 0; i < nb; i++ {
kids.printf("%d 0 R ", 3+2*i)
for i := 1; i <= nb; i++ {
kids.printf("%d 0 R ", pagesObjectNumbers[i])
}
kids.printf("]")
f.out(kids.String())
Expand Down Expand Up @@ -4635,10 +4641,17 @@ func (f *Fpdf) putcatalog() {
}
// Layers
f.layerPutCatalog()
// Name dictionary :
// -> Javascript
// -> Embedded files
f.out("/Names <<")
// JavaScript
if f.javascript != nil {
f.outf("/Names <</JavaScript %d 0 R>>", f.nJs)
f.outf("/JavaScript %d 0 R", f.nJs)
}
// Embedded files
f.outf("/EmbeddedFiles %s", f.getEmbeddedFiles())
f.out(">>")
}

func (f *Fpdf) putheader() {
Expand Down Expand Up @@ -4727,6 +4740,9 @@ func (f *Fpdf) enddoc() {
}
f.layerEndDoc()
f.putheader()
// Embedded files
f.putAttachments()
f.putAnnotationsAttachments()
f.putpages()
f.putresources()
if f.err != nil {
Expand Down
54 changes: 54 additions & 0 deletions fpdf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2832,3 +2832,57 @@ func TestIssue0316(t *testing.T) {
t.Fatal("Font data changed during pdf generation")
}
}

// ExampleFpdf_SetTextRenderingMode demonstrates embedding files in PDFs,
// at the top-level.
func ExampleFpdf_SetAttachments() {
pdf := gofpdf.New("P", "mm", "A4", "")

// Global attachments
file, err := ioutil.ReadFile("grid.go")
if err != nil {
pdf.SetError(err)
}
a1 := gofpdf.Attachment{Content: file, Filename: "grid.go"}
file, err = ioutil.ReadFile("LICENSE")
if err != nil {
pdf.SetError(err)
}
a2 := gofpdf.Attachment{Content: file, Filename: "License"}
pdf.SetAttachments([]gofpdf.Attachment{a1, a2})

fileStr := example.Filename("Fpdf_EmbeddedFiles")
err = pdf.OutputFileAndClose(fileStr)
example.Summary(err, fileStr)
// Output:
// Successfully generated pdf/Fpdf_EmbeddedFiles.pdf
}

func ExampleFpdf_AddAttachmentAnnotation() {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetFont("Arial", "", 12)
pdf.AddPage()

// Per page attachment
file, err := ioutil.ReadFile("grid.go")
if err != nil {
pdf.SetError(err)
}
a := gofpdf.Attachment{Content: file, Filename: "grid.go", Description: "Some amazing code !"}

pdf.SetXY(5, 10)
pdf.Rect(2, 10, 50, 15, "D")
pdf.AddAttachmentAnnotation(&a, 2, 10, 50, 15)
pdf.Cell(50, 15, "A first link")

pdf.SetXY(5, 80)
pdf.Rect(2, 80, 50, 15, "D")
pdf.AddAttachmentAnnotation(&a, 2, 80, 50, 15)
pdf.Cell(50, 15, "A second link (no copy)")

fileStr := example.Filename("Fpdf_FileAnnotations")
err = pdf.OutputFileAndClose(fileStr)
example.Summary(err, fileStr)
// Output:
// Successfully generated pdf/Fpdf_FileAnnotations.pdf
}

0 comments on commit 50ed053

Please sign in to comment.