Skip to content

Commit

Permalink
support for attachment
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitkugler committed Nov 7, 2019
1 parent 47a2c24 commit 5e6addf
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 3 deletions.
150 changes: 150 additions & 0 deletions attachments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
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.
// You only need to call `Embed` explicitly
// if you want to put multiple links towards the same content, using AddAttachmentAnnotation().
// To avoid useless copies in the resulting pdf, call `Embded` once.
// After the call, `a` can now be used in several annotations
// without duplicating its content .
func (a *Attachment) Embed(f *Fpdf) {
if a.objectNumber != 0 { // already embeded (objectNumber start at 2)
return
}
oldState := f.state
if f.state == 2 { // page mode
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
}

// Write 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 usefull, previous calls are discarded.
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 {
a.Embed(f)
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.
// No drawing is done.
// See Attachment.Embed() to avoid unwanted data duplication.
func (f *Fpdf) AddAttachmentAnnotation(a Attachment, x, y, w, h float64) {
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() {
for _, l := range f.pageAttachments {
for i, an := range l {
an.Attachment.Embed(f)
l[i] = an
}
}
}

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 >>", 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 globaly
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
22 changes: 19 additions & 3 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 Down Expand Up @@ -3915,7 +3921,7 @@ func (f *Fpdf) putpages() {
var kids fmtBuffer
kids.printf("/Kids [")
for i := 0; i < nb; i++ {
kids.printf("%d 0 R ", 3+2*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 dictionnary :
// -> 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()
// Embeded 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 !"}
a.Embed(pdf) // copy data once for all
pdf.SetXY(5, 10)
pdf.Rect(2, 10, 50, 15, "D")
pdf.Cell(50, 15, "A first link")
pdf.AddAttachmentAnnotation(a, 2, 10, 50, 15)

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

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

0 comments on commit 5e6addf

Please sign in to comment.