codeberg.org/go-pdf/fpdf@v0.11.1/attachments.go (about) 1 // Copyright ©2023 The go-pdf Authors. All rights reserved. 2 // Use of this source code is governed by a MIT-style 3 // license that can be found in the LICENSE file. 4 5 package fpdf 6 7 import ( 8 "crypto/md5" 9 "encoding/hex" 10 "fmt" 11 "strings" 12 ) 13 14 // Attachment defines a content to be included in the pdf, in one 15 // of the following ways : 16 // - associated with the document as a whole : see SetAttachments() 17 // - accessible via a link localized on a page : see AddAttachmentAnnotation() 18 type Attachment struct { 19 Content []byte 20 21 // Filename is the displayed name of the attachment 22 Filename string 23 24 // Description is only displayed when using AddAttachmentAnnotation(), 25 // and might be modified by the pdf reader. 26 Description string 27 28 objectNumber int // filled when content is included 29 } 30 31 // return the hex encoded checksum of `data` 32 func checksum(data []byte) string { 33 sl := md5.Sum(data) 34 return hex.EncodeToString(sl[:]) 35 } 36 37 // Writes a compressed file like object as "/EmbeddedFile". Compressing is 38 // done with deflate. Includes length, compressed length and MD5 checksum. 39 func (f *Fpdf) writeCompressedFileObject(content []byte) { 40 lenUncompressed := len(content) 41 sum := checksum(content) 42 mem := xmem.compress(content) 43 defer mem.release() 44 compressed := mem.bytes() 45 lenCompressed := len(compressed) 46 f.newobj() 47 f.outf("<< /Type /EmbeddedFile /Length %d /Filter /FlateDecode /Params << /CheckSum <%s> /Size %d >> >>\n", 48 lenCompressed, sum, lenUncompressed) 49 f.putstream(compressed) 50 f.out("endobj") 51 } 52 53 // Embed includes the content of `a`, and update its internal reference. 54 func (f *Fpdf) embed(a *Attachment) { 55 if a.objectNumber != 0 { // already embedded (objectNumber start at 2) 56 return 57 } 58 oldState := f.state 59 f.state = 1 // we write file content in the main buffer 60 f.writeCompressedFileObject(a.Content) 61 streamID := f.n 62 f.newobj() 63 f.outf("<< /Type /Filespec /F () /UF %s /EF << /F %d 0 R >> /Desc %s\n>>", 64 f.textstring(utf8toutf16(a.Filename)), 65 streamID, 66 f.textstring(utf8toutf16(a.Description))) 67 f.out("endobj") 68 a.objectNumber = f.n 69 f.state = oldState 70 } 71 72 // SetAttachments writes attachments as embedded files (document attachment). 73 // These attachments are global, see AddAttachmentAnnotation() for a link 74 // anchored in a page. Note that only the last call of SetAttachments is 75 // useful, previous calls are discarded. Be aware that not all PDF readers 76 // support document attachments. See the SetAttachment example for a 77 // demonstration of this method. 78 func (f *Fpdf) SetAttachments(as []Attachment) { 79 f.attachments = as 80 } 81 82 // embed current attachments. store object numbers 83 // for later use by getEmbeddedFiles() 84 func (f *Fpdf) putAttachments() { 85 for i, a := range f.attachments { 86 f.embed(&a) 87 f.attachments[i] = a 88 } 89 } 90 91 // return /EmbeddedFiles tree name catalog entry. 92 func (f Fpdf) getEmbeddedFiles() string { 93 names := make([]string, len(f.attachments)) 94 for i, as := range f.attachments { 95 names[i] = fmt.Sprintf("(Attachement%d) %d 0 R ", i+1, as.objectNumber) 96 } 97 nameTree := fmt.Sprintf("<< /Names [\n %s \n] >>", strings.Join(names, "\n")) 98 return nameTree 99 } 100 101 // ---------------------------------- Annotations ---------------------------------- 102 103 type annotationAttach struct { 104 *Attachment 105 106 x, y, w, h float64 // fpdf coordinates (y diff and scaling done) 107 } 108 109 // AddAttachmentAnnotation puts a link on the current page, on the rectangle 110 // defined by `x`, `y`, `w`, `h`. This link points towards the content defined 111 // in `a`, which is embedded in the document. Note than no drawing is done by 112 // this method : a method like `Cell()` or `Rect()` should be called to 113 // indicate to the reader that there is a link here. Requiring a pointer to an 114 // Attachment avoids useless copies in the resulting pdf: attachment pointing 115 // to the same data will have their content only be included once, and be 116 // shared amongst all links. Be aware that not all PDF readers support 117 // annotated attachments. See the AddAttachmentAnnotation example for a 118 // demonstration of this method. 119 func (f *Fpdf) AddAttachmentAnnotation(a *Attachment, x, y, w, h float64) { 120 if a == nil { 121 return 122 } 123 f.pageAttachments[f.page] = append(f.pageAttachments[f.page], annotationAttach{ 124 Attachment: a, 125 x: x * f.k, y: f.hPt - y*f.k, w: w * f.k, h: h * f.k, 126 }) 127 } 128 129 // embed current annotations attachments. store object numbers 130 // for later use by putAttachmentAnnotationLinks(), which is 131 // called for each page. 132 func (f *Fpdf) putAnnotationsAttachments() { 133 // avoid duplication 134 m := map[*Attachment]bool{} 135 for _, l := range f.pageAttachments { 136 for _, an := range l { 137 if m[an.Attachment] { // already embedded 138 continue 139 } 140 f.embed(an.Attachment) 141 } 142 } 143 } 144 145 func (f *Fpdf) putAttachmentAnnotationLinks(out *fmtBuffer, page int) { 146 for _, an := range f.pageAttachments[page] { 147 x1, y1, x2, y2 := an.x, an.y, an.x+an.w, an.y-an.h 148 as := fmt.Sprintf("<< /Type /XObject /Subtype /Form /BBox [%.2f %.2f %.2f %.2f] /Length 0 >>", 149 x1, y1, x2, y2) 150 as += "\nstream\nendstream" 151 152 out.printf("<< /Type /Annot /Subtype /FileAttachment /Rect [%.2f %.2f %.2f %.2f] /Border [0 0 0]\n", 153 x1, y1, x2, y2) 154 out.printf("/Contents %s ", f.textstring(utf8toutf16(an.Description))) 155 out.printf("/T %s ", f.textstring(utf8toutf16(an.Filename))) 156 out.printf("/AP << /N %s>>", as) 157 out.printf("/FS %d 0 R >>\n", an.objectNumber) 158 } 159 }