feat: dix all pdf problems. Validator now green
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"gitlab.com/romalor/pdflib/pkg/components/attachment"
|
||||
"gitlab.com/romalor/pdflib/pkg/core"
|
||||
"gitlab.com/romalor/pdflib/pkg/pdf"
|
||||
)
|
||||
|
||||
// AddAttachment embeds data as a file attachment.
|
||||
// The att.Name field specifies the attachment name.
|
||||
func AddAttachment(d *pdf.Document, att attachment.Attachment, data []byte) error {
|
||||
if !d.IsWritable() || d.Writer() == nil {
|
||||
return pdf.ErrNotWritable
|
||||
}
|
||||
|
||||
if att.Name == "" {
|
||||
return errors.New("attachment name cannot be empty")
|
||||
}
|
||||
|
||||
// Apply defaults
|
||||
modDate := att.ModDate
|
||||
if modDate.IsZero() {
|
||||
modDate = time.Now()
|
||||
}
|
||||
relationship := att.Relationship
|
||||
if relationship == "" {
|
||||
relationship = "Unspecified"
|
||||
}
|
||||
|
||||
// Guess MIME type if not provided
|
||||
mimeType := att.MIMEType
|
||||
if mimeType == "" {
|
||||
mimeType = guessMIMEType(att.Name)
|
||||
}
|
||||
|
||||
// Calculate checksum
|
||||
checksum := md5.Sum(data)
|
||||
|
||||
// Create embedded file stream
|
||||
efStreamDict := core.NewDictionary()
|
||||
efStreamDict.Set(core.Name("Type"), core.Name("EmbeddedFile"))
|
||||
if mimeType != "" {
|
||||
efStreamDict.Set(core.Name("Subtype"), core.Name(mimeType))
|
||||
}
|
||||
|
||||
// Create Params dictionary
|
||||
params := core.NewDictionary()
|
||||
params.Set(core.Name("Size"), core.Integer(len(data)))
|
||||
params.Set(core.Name("ModDate"), core.String(Format(modDate)))
|
||||
params.Set(core.Name("CheckSum"), core.HexString(checksum[:]))
|
||||
efStreamDict.Set(core.Name("Params"), params)
|
||||
|
||||
// Create the stream (compression is handled by the writer)
|
||||
var efStream *core.Stream
|
||||
if !att.NoCompress {
|
||||
efStreamDict.Set(core.Name("Filter"), core.Name("FlateDecode"))
|
||||
}
|
||||
efStream = core.NewStreamFromBytes(efStreamDict, data)
|
||||
efStreamRef := d.Writer().Add(efStream)
|
||||
|
||||
// Create EF (embedded files) dictionary
|
||||
efDict := core.NewDictionary()
|
||||
efDict.Set(core.Name("F"), efStreamRef)
|
||||
efDict.Set(core.Name("UF"), efStreamRef) // Unicode-compatible version
|
||||
|
||||
// Create file specification dictionary
|
||||
fileSpec := core.NewDictionary()
|
||||
fileSpec.Set(core.Name("Type"), core.Name("Filespec"))
|
||||
fileSpec.Set(core.Name("F"), core.String(att.Name))
|
||||
fileSpec.Set(core.Name("UF"), core.String(att.Name)) // Unicode filename
|
||||
fileSpec.Set(core.Name("EF"), efDict)
|
||||
|
||||
if att.Description != "" {
|
||||
fileSpec.Set(core.Name("Desc"), core.String(att.Description))
|
||||
}
|
||||
|
||||
if relationship != "Unspecified" {
|
||||
fileSpec.Set(core.Name("AFRelationship"), core.Name(relationship))
|
||||
}
|
||||
|
||||
fileSpecRef := d.Writer().Add(fileSpec)
|
||||
|
||||
// Update the EmbeddedFiles name tree
|
||||
if err := addToEmbeddedFilesNameTree(d, att.Name, fileSpecRef); err != nil {
|
||||
return fmt.Errorf("adding to embedded files tree: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addToEmbeddedFilesNameTree adds an entry to the EmbeddedFiles name tree.
|
||||
func addToEmbeddedFilesNameTree(d *pdf.Document, name string, fileSpecRef core.Reference) error {
|
||||
catalog, catalogRef, err := d.Resolver().Catalog()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Try to get existing Names dictionary
|
||||
var namesDict *core.Dictionary
|
||||
var namesRef core.Reference
|
||||
var namesIsRef bool
|
||||
|
||||
if namesObj, ok := catalog.Get(core.Name("Names")); ok {
|
||||
if ref, isRef := namesObj.(core.Reference); isRef {
|
||||
namesRef = ref
|
||||
namesIsRef = true
|
||||
}
|
||||
namesDict, err = core.ResolveDict(d.Resolver(), namesObj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving Names dictionary: %w", err)
|
||||
}
|
||||
// Clone the dictionary
|
||||
namesDict = namesDict.CloneDict()
|
||||
} else {
|
||||
// Create new Names dictionary
|
||||
namesDict = core.NewDictionary()
|
||||
}
|
||||
|
||||
// Try to get existing EmbeddedFiles name tree
|
||||
var efDict *core.Dictionary
|
||||
var efRef core.Reference
|
||||
var efIsRef bool
|
||||
|
||||
if efObj, ok := namesDict.Get(core.Name("EmbeddedFiles")); ok {
|
||||
if ref, isRef := efObj.(core.Reference); isRef {
|
||||
efRef = ref
|
||||
efIsRef = true
|
||||
}
|
||||
efDict, err = core.ResolveDict(d.Resolver(), efObj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolving EmbeddedFiles dictionary: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Build new name tree with the added entry
|
||||
var newNamesArray core.Array
|
||||
|
||||
if efDict != nil {
|
||||
// Get existing entries
|
||||
entries, err := collectNameTreeEntries(d, efDict)
|
||||
if err != nil {
|
||||
return fmt.Errorf("collecting existing entries: %w", err)
|
||||
}
|
||||
|
||||
// Add new entry
|
||||
entries = append(entries, nameTreeEntry{name: name, value: fileSpecRef})
|
||||
|
||||
// Sort by name
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].name < entries[j].name
|
||||
})
|
||||
|
||||
// Build names array
|
||||
newNamesArray = make(core.Array, 0, len(entries)*2)
|
||||
for _, entry := range entries {
|
||||
newNamesArray = append(newNamesArray, core.String(entry.name), entry.value)
|
||||
}
|
||||
} else {
|
||||
// Create new names array with single entry
|
||||
newNamesArray = core.Array{core.String(name), fileSpecRef}
|
||||
}
|
||||
|
||||
// Create new EmbeddedFiles dictionary
|
||||
newEFDict := core.NewDictionary()
|
||||
newEFDict.Set(core.Name("Names"), newNamesArray)
|
||||
|
||||
// Update or create EmbeddedFiles entry
|
||||
if efIsRef {
|
||||
if err := d.Writer().Modify(efRef, newEFDict); err != nil {
|
||||
return fmt.Errorf("modifying EmbeddedFiles: %w", err)
|
||||
}
|
||||
} else {
|
||||
namesDict.Set(core.Name("EmbeddedFiles"), newEFDict)
|
||||
}
|
||||
|
||||
// Update or create Names dictionary
|
||||
if namesIsRef {
|
||||
if !efIsRef {
|
||||
// We added a new EmbeddedFiles entry, need to update Names
|
||||
if err := d.Writer().Modify(namesRef, namesDict); err != nil {
|
||||
return fmt.Errorf("modifying Names: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Update catalog
|
||||
newCatalog := catalog.CloneDict()
|
||||
newCatalog.Set(core.Name("Names"), namesDict)
|
||||
|
||||
// Extend AF array
|
||||
afArr := core.Array{fileSpecRef}
|
||||
newCatalog.Set(core.Name("AF"), afArr)
|
||||
|
||||
if err := d.Writer().Modify(catalogRef, newCatalog); err != nil {
|
||||
return fmt.Errorf("modifying catalog: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nameTreeEntry represents a name-value pair from a name tree.
|
||||
type nameTreeEntry struct {
|
||||
name string
|
||||
value core.Object
|
||||
}
|
||||
|
||||
// collectNameTreeEntries collects all entries from a name tree.
|
||||
func collectNameTreeEntries(d *pdf.Document, dict *core.Dictionary) ([]nameTreeEntry, error) {
|
||||
var entries []nameTreeEntry
|
||||
|
||||
// Check for Names array (leaf node)
|
||||
if namesObj, ok := dict.Get(core.Name("Names")); ok {
|
||||
namesArray, err := core.ResolveArray(d.Resolver(), namesObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Names array has pairs: [name1, value1, name2, value2, ...]
|
||||
for i := 0; i+1 < len(namesArray); i += 2 {
|
||||
var name string
|
||||
switch n := namesArray[i].(type) {
|
||||
case core.String:
|
||||
name = string(n)
|
||||
case core.HexString:
|
||||
name = string(n)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
entries = append(entries, nameTreeEntry{name: name, value: namesArray[i+1]})
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Kids array (intermediate node)
|
||||
if kidsObj, ok := dict.Get(core.Name("Kids")); ok {
|
||||
kidsArray, err := core.ResolveArray(d.Resolver(), kidsObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, kid := range kidsArray {
|
||||
kidDict, err := core.ResolveDict(d.Resolver(), kid)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
kidEntries, err := collectNameTreeEntries(d, kidDict)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, kidEntries...)
|
||||
}
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// guessMIMEType guesses the MIME type from a filename.
|
||||
func guessMIMEType(filename string) string {
|
||||
ext := filepath.Ext(filename)
|
||||
if ext == "" {
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// Common MIME types
|
||||
mimeTypes := map[string]string{
|
||||
".pdf": "application/pdf",
|
||||
".txt": "text/plain",
|
||||
".html": "text/html",
|
||||
".htm": "text/html",
|
||||
".xml": "application/xml",
|
||||
".json": "application/json",
|
||||
".csv": "text/csv",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".mp3": "audio/mpeg",
|
||||
".mp4": "video/mp4",
|
||||
".zip": "application/zip",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
}
|
||||
|
||||
if mime, ok := mimeTypes[ext]; ok {
|
||||
return mime
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// FormatPDFDate formats a time.Time as a PDF date string.
|
||||
// The format is: D:YYYYMMDDHHmmSSOHH'mm'
|
||||
// where O is the timezone indicator (+, or -) and HH'mm' is the offset.
|
||||
func Format(t time.Time) string {
|
||||
// Get timezone offset
|
||||
_, offset := t.Zone()
|
||||
offsetHours := offset / 3600
|
||||
offsetMins := (offset % 3600) / 60
|
||||
|
||||
var sign byte = '+'
|
||||
if offsetHours < 0 {
|
||||
sign = '-'
|
||||
offsetHours = -offsetHours
|
||||
offsetMins = -offsetMins
|
||||
}
|
||||
|
||||
return fmt.Sprintf("D:%04d%02d%02d%02d%02d%02d%c%02d'%02d'",
|
||||
t.Year(), t.Month(), t.Day(),
|
||||
t.Hour(), t.Minute(), t.Second(),
|
||||
sign, offsetHours, offsetMins)
|
||||
}
|
||||
Reference in New Issue
Block a user