diff --git a/attachments.go b/attachments.go new file mode 100644 index 00000000..c3f016e6 --- /dev/null +++ b/attachments.go @@ -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) + } +} diff --git a/def.go b/def.go index 8e2bea5f..e940390b 100644 --- a/def.go +++ b/def.go @@ -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 diff --git a/fpdf.go b/fpdf.go index fdfcc15c..e1e9267e 100644 --- a/fpdf.go +++ b/fpdf.go @@ -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 @@ -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 @@ -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("< 0 { + if len(f.pageLinks[n])+len(f.pageAttachments[n]) > 0 { var annots fmtBuffer annots.printf("/Annots [") for _, pl := range f.pageLinks[n] { @@ -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()) } @@ -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()) @@ -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 <>", f.nJs) + f.outf("/JavaScript %d 0 R", f.nJs) } + // Embedded files + f.outf("/EmbeddedFiles %s", f.getEmbeddedFiles()) + f.out(">>") } func (f *Fpdf) putheader() { @@ -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 { diff --git a/fpdf_test.go b/fpdf_test.go index 99eb2e72..1b3deed5 100644 --- a/fpdf_test.go +++ b/fpdf_test.go @@ -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 +}