From d65b29fe5afdef83304679cbd52c067b9b24347a Mon Sep 17 00:00:00 2001 From: Elbek Khoshimjonov Date: Wed, 26 Apr 2023 15:24:52 +0500 Subject: [PATCH 1/3] feat: Add flag to remove metadata(so pdf is deterministic) --- fillpdf.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++--- sample/main.go | 6 +++++- utils.go | 9 ++++---- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/fillpdf.go b/fillpdf.go index 1254d1a..2eaca3b 100644 --- a/fillpdf.go +++ b/fillpdf.go @@ -26,6 +26,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "github.com/gdamore/encoding" ) @@ -46,12 +47,15 @@ type Options struct { Overwrite bool // Flatten will flatten the document making the form fields no longer editable Flatten bool + // Remove metadata + RemoveMetadata bool } func defaultOptions() Options { return Options{ - Overwrite: true, - Flatten: true, + Overwrite: true, + Flatten: true, + RemoveMetadata: false, } } @@ -94,6 +98,15 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e return fmt.Errorf("failed to create temporary directory: %v", err) } + var metadataFile string + if opts.RemoveMetadata { + metadataFile = filepath.Clean(tmpDir + "/metadata.tmp") + errM := createMetadataFile(formPDFFile, metadataFile) + if errM != nil { + return fmt.Errorf("failed to create metadata file: %v", errM) + } + } + // Remove the temporary directory on defer again. defer func() { errD := os.RemoveAll(tmpDir) @@ -126,11 +139,25 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e } // Run the pdftk utility. - err = runCommandInPath(tmpDir, "pdftk", args...) + _, err = runCommandInPath(tmpDir, "pdftk", args...) if err != nil { return fmt.Errorf("pdftk error: %v", err) } + if opts.RemoveMetadata { + outputFile2 := filepath.Clean(tmpDir + "output2.pdf") + args = []string{ + outputFile, + "update_info", metadataFile, + "output", outputFile2, + } + _, err = runCommandInPath(tmpDir, "pdftk", args...) + if err != nil { + return fmt.Errorf("pdftk error: %v", err) + } + outputFile = outputFile2 + } + // Check if the destination file exists. e, err = exists(destPDFFile) if err != nil { @@ -187,6 +214,30 @@ func createFdfFile(form Form, path string) error { return w.Flush() } +func createMetadataFile(formPDFFile, path string) error { + // Create the file. + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + out, err := runCommandInPath("", "pdftk", formPDFFile, "dump_data") + if err != nil { + return err + } + + lines := strings.Split(string(out.Bytes()), "\n") + for i := range lines { + if strings.HasPrefix(lines[i], "InfoValue:") { + lines[i] = "InfoValue:" + } + } + + _, err = file.Write([]byte(strings.Join(lines, "\n"))) + return err +} + const fdfHeader = `%FDF-1.2 %,,oe" 1 0 obj diff --git a/sample/main.go b/sample/main.go index 0e2d258..9fa33b4 100644 --- a/sample/main.go +++ b/sample/main.go @@ -14,7 +14,11 @@ func main() { } // Fill the form PDF with our values. - err := fillpdf.Fill(form, "form.pdf", "filled.pdf") + err := fillpdf.Fill(form, "form.pdf", "filled.pdf", fillpdf.Options{ + Overwrite: true, + Flatten: true, + RemoveMetadata: true, + }) if err != nil { log.Fatal(err) } diff --git a/utils.go b/utils.go index 7ca645c..f3eba11 100644 --- a/utils.go +++ b/utils.go @@ -69,18 +69,19 @@ func copyFile(src, dst string) (err error) { // runCommandInPath runs a command and waits for it to exit. // The working directory is also set. // The stderr error message is returned on error. -func runCommandInPath(dir, name string, args ...string) error { +func runCommandInPath(dir, name string, args ...string) (*bytes.Buffer, error) { // Create the command. - var stderr bytes.Buffer + var stdout, stderr bytes.Buffer cmd := exec.Command(name, args...) + 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 } From cbb3474739b06b11fe4b3058ce9485c3f7b24faf Mon Sep 17 00:00:00 2001 From: Elbek Khoshimjonov Date: Mon, 15 May 2023 15:23:19 +0500 Subject: [PATCH 2/3] wrapped.BoolValue --- fillpdf.go | 37 +++++++++++++++++++++++++------------ go.mod | 5 ++++- go.sum | 8 ++++++++ sample/main.go | 5 ++--- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/fillpdf.go b/fillpdf.go index 2eaca3b..4efac53 100644 --- a/fillpdf.go +++ b/fillpdf.go @@ -29,6 +29,7 @@ import ( "strings" "github.com/gdamore/encoding" + "google.golang.org/protobuf/types/known/wrapperspb" ) var ( @@ -44,18 +45,30 @@ 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 + Overwrite *wrapperspb.BoolValue // Flatten will flatten the document making the form fields no longer editable - Flatten bool + Flatten *wrapperspb.BoolValue // Remove metadata - RemoveMetadata bool + RemoveMetadata *wrapperspb.BoolValue +} + +func (o *Options) Override(opt Options) { + if opt.Overwrite != nil { + o.Overwrite = opt.Overwrite + } + 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, - RemoveMetadata: false, + Overwrite: wrapperspb.Bool(true), + Flatten: wrapperspb.Bool(true), + RemoveMetadata: wrapperspb.Bool(false), } } @@ -64,8 +77,8 @@ func defaultOptions() Options { func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (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. @@ -99,7 +112,7 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e } var metadataFile string - if opts.RemoveMetadata { + if opts.RemoveMetadata.GetValue() { metadataFile = filepath.Clean(tmpDir + "/metadata.tmp") errM := createMetadataFile(formPDFFile, metadataFile) if errM != nil { @@ -134,7 +147,7 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e } // 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") } @@ -144,7 +157,7 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e return fmt.Errorf("pdftk error: %v", err) } - if opts.RemoveMetadata { + if opts.RemoveMetadata.GetValue() { outputFile2 := filepath.Clean(tmpDir + "output2.pdf") args = []string{ outputFile, @@ -163,7 +176,7 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e if err != nil { return fmt.Errorf("failed to check if destination PDF file exists: %v", err) } else if e { - if !opts.Overwrite { + if !opts.Overwrite.GetValue() { return fmt.Errorf("destination PDF file already exists: '%s'", destPDFFile) } diff --git a/go.mod b/go.mod index 61796dd..381c91c 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum index a0f78ee..ba197c8 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/sample/main.go b/sample/main.go index 9fa33b4..6c31c70 100644 --- a/sample/main.go +++ b/sample/main.go @@ -4,6 +4,7 @@ import ( "log" "github.com/desertbit/fillpdf" + "google.golang.org/protobuf/types/known/wrapperspb" ) func main() { @@ -15,9 +16,7 @@ func main() { // Fill the form PDF with our values. err := fillpdf.Fill(form, "form.pdf", "filled.pdf", fillpdf.Options{ - Overwrite: true, - Flatten: true, - RemoveMetadata: true, + RemoveMetadata: wrapperspb.Bool(true), }) if err != nil { log.Fatal(err) From f46daae9d16b5b776fc3aa5968249cc6a4ec7276 Mon Sep 17 00:00:00 2001 From: Elbek Khoshimjonov Date: Mon, 31 Jul 2023 20:48:35 +0500 Subject: [PATCH 3/3] make pdftk work with read-only fs --- fillpdf.go | 153 +++++++++---------------------------------------- sample/main.go | 6 +- utils.go | 34 +---------- 3 files changed, 35 insertions(+), 158 deletions(-) diff --git a/fillpdf.go b/fillpdf.go index 4efac53..b8b2252 100644 --- a/fillpdf.go +++ b/fillpdf.go @@ -19,14 +19,11 @@ package fillpdf import ( - "bufio" + "bytes" + "errors" "fmt" - "io/ioutil" - "log" - "os" "os/exec" "path/filepath" - "strings" "github.com/gdamore/encoding" "google.golang.org/protobuf/types/known/wrapperspb" @@ -44,8 +41,6 @@ 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 *wrapperspb.BoolValue // Flatten will flatten the document making the form fields no longer editable Flatten *wrapperspb.BoolValue // Remove metadata @@ -53,9 +48,6 @@ type Options struct { } func (o *Options) Override(opt Options) { - if opt.Overwrite != nil { - o.Overwrite = opt.Overwrite - } if opt.Flatten != nil { o.Flatten = opt.Flatten } @@ -66,7 +58,6 @@ func (o *Options) Override(opt Options) { func defaultOptions() Options { return Options{ - Overwrite: wrapperspb.Bool(true), Flatten: wrapperspb.Bool(true), RemoveMetadata: wrapperspb.Bool(false), } @@ -74,7 +65,7 @@ func defaultOptions() Options { // 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() for _, opt := range options { @@ -84,66 +75,34 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e // 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-") + // Create the fdf content. + fdfContent, err := createFdfFile(form) if err != nil { - return fmt.Errorf("failed to create temporary directory: %v", err) - } - - var metadataFile string - if opts.RemoveMetadata.GetValue() { - metadataFile = filepath.Clean(tmpDir + "/metadata.tmp") - errM := createMetadataFile(formPDFFile, metadataFile) - if errM != nil { - return fmt.Errorf("failed to create metadata file: %v", errM) - } - } - - // 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) - 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. @@ -152,62 +111,30 @@ func Fill(form Form, formPDFFile, destPDFFile string, options ...Options) (err e } // 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) } if opts.RemoveMetadata.GetValue() { - outputFile2 := filepath.Clean(tmpDir + "output2.pdf") - args = []string{ - outputFile, - "update_info", metadataFile, - "output", outputFile2, - } - _, err = runCommandInPath(tmpDir, "pdftk", args...) + // Check if the exiftool utility exists. + _, err = exec.LookPath("exiftool") if err != nil { - return fmt.Errorf("pdftk error: %v", err) + return nil, errors.New("exiftool utility is not installed!") } - outputFile = outputFile2 - } - - // 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.GetValue() { - return fmt.Errorf("destination PDF file already exists: '%s'", destPDFFile) - } - - 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 @@ -215,40 +142,14 @@ func createFdfFile(form Form, path string) error { // 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() -} - -func createMetadataFile(formPDFFile, path string) error { - // Create the file. - file, err := os.Create(path) - if err != nil { - return err - } - defer file.Close() - - out, err := runCommandInPath("", "pdftk", formPDFFile, "dump_data") - if err != nil { - return err - } - - lines := strings.Split(string(out.Bytes()), "\n") - for i := range lines { - if strings.HasPrefix(lines[i], "InfoValue:") { - lines[i] = "InfoValue:" - } - } - - _, err = file.Write([]byte(strings.Join(lines, "\n"))) - return err + output += fdfFooter + return output, nil } const fdfHeader = `%FDF-1.2 diff --git a/sample/main.go b/sample/main.go index 6c31c70..e6c23ff 100644 --- a/sample/main.go +++ b/sample/main.go @@ -2,6 +2,7 @@ package main import ( "log" + "os" "github.com/desertbit/fillpdf" "google.golang.org/protobuf/types/known/wrapperspb" @@ -15,10 +16,13 @@ func main() { } // Fill the form PDF with our values. - err := fillpdf.Fill(form, "form.pdf", "filled.pdf", fillpdf.Options{ + 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) + } diff --git a/utils.go b/utils.go index f3eba11..008ed89 100644 --- a/utils.go +++ b/utils.go @@ -39,43 +39,15 @@ 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) (*bytes.Buffer, error) { +func runCommand(name string, stdin io.Reader, args ...string) (*bytes.Buffer, error) { // Create the command. 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()