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  }