Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 47 additions & 82 deletions fillpdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@
package fillpdf

import (
"bufio"
"bytes"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"

"github.com/gdamore/encoding"
"google.golang.org/protobuf/types/known/wrapperspb"
)

var (
Expand All @@ -42,149 +41,115 @@ type Form map[string]interface{}

// Options represents the options to alter the PDF filling process
type Options struct {
// Overwrite will overwrite any pre existing filled PDF
Overwrite bool
// Flatten will flatten the document making the form fields no longer editable
Flatten bool
Flatten *wrapperspb.BoolValue
// Remove metadata
RemoveMetadata *wrapperspb.BoolValue
}

func (o *Options) Override(opt Options) {
if opt.Flatten != nil {
o.Flatten = opt.Flatten
}
if opt.RemoveMetadata != nil {
o.RemoveMetadata = opt.RemoveMetadata
}
}

func defaultOptions() Options {
return Options{
Overwrite: true,
Flatten: true,
Flatten: wrapperspb.Bool(true),
RemoveMetadata: wrapperspb.Bool(false),
}
}

// Fill a PDF form with the specified form values and create a final filled PDF file.
// The options parameter alters few aspects of the generation.
func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err error) {
func Fill(form Form, formPDFFile string, options ...Options) (out []byte, err error) {
// If the user provided the options we overwrite the defaults with the given struct.
opts := defaultOptions()
if len(options) > 0 {
opts = options[0]
for _, opt := range options {
opts.Override(opt)
}

// Get the absolute paths.
formPDFFile, err = filepath.Abs(formPDFFile)
if err != nil {
return fmt.Errorf("failed to create the absolute path: %v", err)
}
destPDFFile, err = filepath.Abs(destPDFFile)
if err != nil {
return fmt.Errorf("failed to create the absolute path: %v", err)
return nil, fmt.Errorf("failed to create the absolute path: %v", err)
}

// Check if the form file exists.
e, err := exists(formPDFFile)
if err != nil {
return fmt.Errorf("failed to check if form PDF file exists: %v", err)
return nil, fmt.Errorf("failed to check if form PDF file exists: %v", err)
} else if !e {
return fmt.Errorf("form PDF file does not exists: '%s'", formPDFFile)
return nil, fmt.Errorf("form PDF file does not exists: '%s'", formPDFFile)
}

// Check if the pdftk utility exists.
_, err = exec.LookPath("pdftk")
if err != nil {
return fmt.Errorf("pdftk utility is not installed!")
return nil, errors.New("pdftk utility is not installed!")
}

// Create a temporary directory.
tmpDir, err := ioutil.TempDir("", "fillpdf-")
if err != nil {
return fmt.Errorf("failed to create temporary directory: %v", err)
}

// Remove the temporary directory on defer again.
defer func() {
errD := os.RemoveAll(tmpDir)
// Log the error only.
if errD != nil {
log.Printf("fillpdf: failed to remove temporary directory '%s' again: %v", tmpDir, errD)
}
}()

// Create the temporary output file path.
outputFile := filepath.Clean(tmpDir + "/output.pdf")

// Create the fdf data file.
fdfFile := filepath.Clean(tmpDir + "/data.fdf")
err = createFdfFile(form, fdfFile)
// Create the fdf content.
fdfContent, err := createFdfFile(form)
if err != nil {
return fmt.Errorf("failed to create fdf form data file: %v", err)
return nil, fmt.Errorf("failed to create fdf form data file: %v", err)
}

// Create the pdftk command line arguments.
args := []string{
formPDFFile,
"fill_form", fdfFile,
"output", outputFile,
"fill_form", "-",
"output", "-",
}

// If the user specified to flatten the output PDF we append the related parameter.
if opts.Flatten {
if opts.Flatten.GetValue() {
args = append(args, "flatten")
}

// Run the pdftk utility.
err = runCommandInPath(tmpDir, "pdftk", args...)
output, err := runCommand("pdftk", bytes.NewBuffer([]byte(fdfContent)), args...)
if err != nil {
return fmt.Errorf("pdftk error: %v", err)
return nil, fmt.Errorf("pdftk error: %v", err)
}

// Check if the destination file exists.
e, err = exists(destPDFFile)
if err != nil {
return fmt.Errorf("failed to check if destination PDF file exists: %v", err)
} else if e {
if !opts.Overwrite {
return fmt.Errorf("destination PDF file already exists: '%s'", destPDFFile)
if opts.RemoveMetadata.GetValue() {
// Check if the exiftool utility exists.
_, err = exec.LookPath("exiftool")
if err != nil {
return nil, errors.New("exiftool utility is not installed!")
}

err = os.Remove(destPDFFile)
// exiftool -all:all= - -o -
output, err = runCommand("exiftool", output, "-all:all=", "-", "-o", "-")
if err != nil {
return fmt.Errorf("failed to remove destination PDF file: %v", err)
return nil, fmt.Errorf("exiftool error: %v", err)
}
}

// On success, copy the output file to the final destination.
err = copyFile(outputFile, destPDFFile)
if err != nil {
return fmt.Errorf("failed to copy created output PDF to final destination: %v", err)
}

return nil
return output.Bytes(), nil
}

func createFdfFile(form Form, path string) error {
// Create the file.
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()

// Create a new writer.
w := bufio.NewWriter(file)

func createFdfFile(form Form) (output string, err error) {
// Write the fdf header.
fmt.Fprintln(w, fdfHeader)
output = fdfHeader

// Write the form data.
var valueStr string
for key, value := range form {
// Convert to Latin-1.
valueStr, err = latin1Encoder.String(fmt.Sprintf("%v", value))
if err != nil {
return fmt.Errorf("failed to convert string to Latin-1")
return "", fmt.Errorf("failed to convert string to Latin-1")
}
fmt.Fprintf(w, "<< /T (%s) /V (%s)>>\n", key, valueStr)
output += fmt.Sprintf("<< /T (%s) /V (%s)>>\n", key, valueStr)
}

// Write the fdf footer.
fmt.Fprintln(w, fdfFooter)

// Flush everything.
return w.Flush()
output += fdfFooter
return output, nil
}

const fdfHeader = `%FDF-1.2
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module github.com/desertbit/fillpdf

go 1.16

require github.com/gdamore/encoding v1.0.0 // indirect
require (
github.com/gdamore/encoding v1.0.0
google.golang.org/protobuf v1.30.0
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
9 changes: 8 additions & 1 deletion sample/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package main

import (
"log"
"os"

"github.com/desertbit/fillpdf"
"google.golang.org/protobuf/types/known/wrapperspb"
)

func main() {
Expand All @@ -14,8 +16,13 @@ func main() {
}

// Fill the form PDF with our values.
err := fillpdf.Fill(form, "form.pdf", "filled.pdf")
out, err := fillpdf.Fill(form, "form.pdf", fillpdf.Options{
RemoveMetadata: wrapperspb.Bool(true),
})
if err != nil {
log.Fatal(err)
}

os.WriteFile("filled.pdf", out, 0600)

}
41 changes: 7 additions & 34 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,48 +39,21 @@ func exists(path string) (bool, error) {
return false, err
}

// copyFile copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
return
}

// runCommandInPath runs a command and waits for it to exit.
// The working directory is also set.
// runCommand runs a command and waits for it to exit.
// The stderr error message is returned on error.
func runCommandInPath(dir, name string, args ...string) error {
func runCommand(name string, stdin io.Reader, args ...string) (*bytes.Buffer, error) {
// Create the command.
var stderr bytes.Buffer
var stdout, stderr bytes.Buffer
cmd := exec.Command(name, args...)
cmd.Stdin = stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Dir = dir

// Start the command and wait for it to exit.
err := cmd.Run()
if err != nil {
return fmt.Errorf(strings.TrimSpace(stderr.String()))
return nil, fmt.Errorf(strings.TrimSpace(stderr.String()))
}

return nil
return &stdout, nil
}