From be4069c18e9ea00b285e686be927ad528139a009 Mon Sep 17 00:00:00 2001 From: BK Date: Mon, 25 May 2020 13:16:12 -0700 Subject: [PATCH] Multi-step flattening to massively improve the quality of the generated PDF's. PDF viewing software such as preview and acrobat reader auto positions the unflattened form fields when the user views the PDF file in it. But the `pdftk` library doesn't auto position fields before flattening. The `pdftk` does support the ability to create better-positioned fields without flattening using its `need_appearances` option. ref. https://www.pdflabs.com/docs/pdftk-man-page/#dest-output-need-appearances So, we are using a multi-step approach to get the same quality from the `pdftk`, which does the following: 1. fill-up the form with the `need_appearances` flag and without the `flatten` flag 2. regenerating the flattened file out of the non-flattened PDF file, which has better positioning. Empirical results have shown massive improvements in the quality of the generated PDF's: https://ibb.co/KLqtYJC --- fillpdf.go | 91 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/fillpdf.go b/fillpdf.go index 64fca3a..dfaaad7 100644 --- a/fillpdf.go +++ b/fillpdf.go @@ -38,17 +38,17 @@ func Fill(form Form, formPDFFile, destPDFFile string, overwrite ...bool) (err er // Get the absolute paths. formPDFFile, err = filepath.Abs(formPDFFile) if err != nil { - return fmt.Errorf("failed to create the absolute path: %v", err) + 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 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 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) } @@ -56,13 +56,13 @@ func Fill(form Form, formPDFFile, destPDFFile string, overwrite ...bool) (err er // Check if the pdftk utility exists. _, err = exec.LookPath("pdftk") if err != nil { - return fmt.Errorf("pdftk utility is not installed!") + return fmt.Errorf("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) + return fmt.Errorf("failed to create temporary directory: %+v", err) } // Remove the temporary directory on defer again. @@ -70,38 +70,69 @@ func Fill(form Form, formPDFFile, destPDFFile string, overwrite ...bool) (err er errD := os.RemoveAll(tmpDir) // Log the error only. if errD != nil { - log.Printf("fillpdf: failed to remove temporary directory '%s' again: %v", tmpDir, errD) + 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") + // 1. Generate an FDF file. + // create the temporary intermediate output file path. + intermediateOutputFile := filepath.Clean(tmpDir + "/intermediate-output.pdf") - // Create the fdf data file. - fdfFile := filepath.Clean(tmpDir + "/data.fdf") - err = createFdfFile(form, fdfFile) + // create an FDF file from the input form. + inputFDFFile := filepath.Clean(tmpDir + "/input.fdf") + err = createFDFFile(form, inputFDFFile) if err != nil { - return fmt.Errorf("failed to create fdf form data file: %v", err) + return fmt.Errorf("failed to create FDF file from the input form: %+v", err) } - // Create the pdftk command line arguments. + // 2. Generate a PDF file with need_appearances. + // create the pdftk command-line arguments. args := []string{ formPDFFile, - "fill_form", fdfFile, - "output", outputFile, - "flatten", + "fill_form", inputFDFFile, + "output", intermediateOutputFile, + "need_appearances", + } + + // run the pdftk utility. + err = runCommandInPath(tmpDir, "pdftk", args...) + if err != nil { + return fmt.Errorf("pdftk error: %+v\n%s\n%+v", err, tmpDir, args) + } + + // 3. Export an FDF file out of it. + // create the pdftk command-line arguments. + exportedFDF := filepath.Clean(tmpDir + "/exported.fdf") + args = []string{ + intermediateOutputFile, + "generate_FDF", + "output", exportedFDF, + } + // run the pdftk utility. + err = runCommandInPath(tmpDir, "pdftk", args...) + if err != nil { + return fmt.Errorf("pdftk error: %+v\n%s\n%+v", err, tmpDir, args) } - // Run the pdftk utility. + // 4. Generate a flattened PDF file out of the exported FDF file. + // create the pdftk command-line arguments. + finalOutputFile := filepath.Clean(tmpDir + "/final-output.pdf") + args = []string{ + intermediateOutputFile, + "fill_form", exportedFDF, + "output", finalOutputFile, + "flatten", + } + // run the pdftk utility. err = runCommandInPath(tmpDir, "pdftk", args...) if err != nil { - return fmt.Errorf("pdftk error: %v", err) + return fmt.Errorf("pdftk error: %+v\n%s\n%+v", err, tmpDir, args) } // 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) + return fmt.Errorf("failed to check if destination PDF file exists: %+v", err) } else if e { if len(overwrite) == 0 || !overwrite[0] { return fmt.Errorf("destination PDF file already exists: '%s'", destPDFFile) @@ -109,20 +140,20 @@ func Fill(form Form, formPDFFile, destPDFFile string, overwrite ...bool) (err er err = os.Remove(destPDFFile) if err != nil { - return fmt.Errorf("failed to remove destination PDF file: %v", err) + return fmt.Errorf("failed to remove destination PDF file: %+v", err) } } // On success, copy the output file to the final destination. - err = copyFile(outputFile, destPDFFile) + err = copyFile(finalOutputFile, destPDFFile) if err != nil { - return fmt.Errorf("failed to copy created output PDF to final destination: %v", err) + return fmt.Errorf("failed to copy created output PDF to final destination: %+v", err) } return nil } -func createFdfFile(form Form, path string) error { +func createFDFFile(form Form, path string) error { // Create the file. file, err := os.Create(path) if err != nil { @@ -133,28 +164,30 @@ func createFdfFile(form Form, path string) error { // Create a new writer. w := bufio.NewWriter(file) - // Write the fdf header. - fmt.Fprintln(w, fdfHeader) + // Write the FDF header. + fmt.Fprintln(w, FDFHeader) // Write the form data. for key, value := range form { fmt.Fprintf(w, "<< /T (%s) /V (%v)>>\n", key, value) } - // Write the fdf footer. - fmt.Fprintln(w, fdfFooter) + // Write the FDF footer. + fmt.Fprintln(w, FDFFooter) // Flush everything. return w.Flush() } -const fdfHeader = `%FDF-1.2 +// FDFHeader marks the begining of the FDF file. +const FDFHeader = `%FDF-1.2 %,,oe" 1 0 obj << /FDF << /Fields [` -const fdfFooter = `] +// FDFFooter marks the end of the FDF file. +const FDFFooter = `] >> >> endobj