4
\$\begingroup\$

I am working on a replacement for a program called pdftk. The program needs to be able to take pdf's, and generate fdf files for them, fill a pdf from an fdf, output info about the forms, and output data about the pdf. I'm sorry for horrible formatting (I'm new to Java). Right now the program works, but is quite slow. Any advice to speed it up would be great.

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;

import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.fdf.FDFDocument;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;

public class ppdftk {
    public static void main(String[] args) throws IOException{
        String[] arg = args;
        //String[] arg = {"leadlevel.pdf", "fill_form", "out.fdf", "output", "out.pdf", "flatten"};

        //checking that request is valid
        if(arg.length == 0){
            System.err.println(info(false));
            return;
        }
        if("-h".equals(arg[0]) || "--help".equals(arg[0]) || "-help".equals(arg[0])){
            System.out.println(info(true));
            return;
        }
        if(arg.length == 1){
            System.err.println(info(false));
            return;
        }

        String path = arg[0];
        if("PROMPT".equals(path)){
            System.out.println("Please enter a filename for an input PDF:");
            Scanner console = new Scanner(System.in);
            path = console.nextLine();
            console.close();
        }
        PDDocument doc;
        try{
            if("-".equals(path))
                doc = PDDocument.load(System.in);
            else
                doc = PDDocument.load(new File(path));
        }catch (IOException e){
            System.err.println("Error: Failed to open PDF file:");
            System.err.println('\t' + path);
            System.err.println("Done.  Input errors, so no output created.");
            System.err.println(e);
            return;
        }
        String output = "";
        String out = "";
        if(arg.length >= 3 && "output".equals(arg[1])){
            out = arg[2];
            if(out == path){
                System.err.println("Output can not equal input");
                return;
            }
        }else{
            if("fill_form".equals(arg[1])){
                if(arg.length >= 5 && "output".equals(arg[3]))
                    out = arg[4];
                else
                    out = "-";
            }else if(arg.length >= 4 && "output".equals(arg[2]))
                out = arg[3];
            else
                out = "-";
            if(out == path){
                System.err.println("Output can not equal input");
                return;
            }
        }

        //farming out ops to helper functions
        if("dump_data_fields".equals(arg[1])){
            output = dump_data_fields(doc.getDocumentCatalog().getAcroForm());
        }else if("dump_data".equals(arg[1])){
            output = dump_data(doc);
        }else if("generate_fdf".equals(arg[1])){
            output = generate_fdf(doc.getDocumentCatalog().getAcroForm());
        }else if("fill_form".equals(arg[1])){
            PDAcroForm acro = doc.getDocumentCatalog().getAcroForm();
            fill_form(acro,arg[2]);
            if(Arrays.asList(arg).contains("flatten")){
                try{
                    acro.flatten();
                }catch (IOException e){
                    System.err.println("Error: unflatenable pdf: "+e);
                }
            }
        }else{
            System.out.println(info(false));
            return;
        }
        if("-".equals(out)){
            if("fill_form".equals(arg[1]) || "output".equals(arg[1])){
                doc.save(System.out);
            }
            System.out.println(output);
        }else{
            File file = new File(out);
            try{
                if("fill_form".equals(arg[1]) || "output".equals(arg[1])){
                    doc.save(file);
                }else{
                    //file.createNewFile();
                    try(BufferedWriter write = new BufferedWriter(new FileWriter(file))){
                        write.write(output);
                        write.close();
                    }catch(IOException e){
                        System.err.println("Error: unable to open file for output: " + out);
                    }
                }
            }catch (IOException e){
                System.err.println("Error: unable to open file for output: " + out);
            }
        }
        doc.close();
    }

    public static String info(boolean verbose){
        //Documentation from PDFTK, with unsupported options removed.
        if(verbose){
            return "\nSYNOPSIS"
    +"  pdf_form.py <input PDF file | - | PROMPT> [ <operation> <operation arguments> ]\n"
    +"      [ output <output filename | - | PROMPT> ] [ flatten ]\n"
    +"  Where:\n"
    +"      <operation> may be empty, or: [generate_fdf | fill_form |dump_data |dump_data_fields]\n"
    +"\n"
    +"  OPTIONS\n"
    +"  --help, -h\n"
    +"      show summary of options.\n"
    +"      \n"
    +"  <input PDF files | - | PROMPT>\n"
    +"      An input PDF file. Use - to pass a single PDF into pdftk via stdin.\n"
    +"      \n"
    +"  [<operation> <operation arguments>]\n"
    +"      Available operations are:\n"
    +"      generate_fdf,fill_form, dump_data,dump_data_fields\n"
    +"      Some operations takes additional arguments, described below.\n"
    +"      \n"
    +"      generate_fdf\n"
    +"          Reads a single, input PDF file and generates an FDF file suitable for fill_form out of it\n"
    +"          to the given output filename or (if no output is given) to stdout. Does not create a new PDF.\n"
    +""
    +"      fill_form <FDF data filename | - | PROMPT>\n"
    +"          Fills the input PDF's form fields with the data from an FDF file, or stdin.\n"
    +"          Enter the data filename after fill_form, or use - to pass the data via stdin, like so:\n"
    +"          \n"
    +"          ./pdf_form.py form.pdf fill_form data.fdf output form.filled.pdf\n"
    +"          \n"
    +"          After filling a form, the form fields remain interactive unless flatten is used.\n"
    +"          flatten merges the form fields with the PDF pages. You can also use flatten alone, as shown:\n"
    +"          \n"
    +"          ./pdf_form.py form.pdf fill_form data.fdf output out.pdf flatten\n"
    +"          \n"
    +"          or:\n"
    +"          \n"
    +"          ./pdf_form.py form.filled.pdf output out.pdf flatten\n"
    +"\n"
    +"      dump_data\n"
    +"          Reads a single, input PDF file and reports various statistics, metadata, bookmarks\n"
    +"          (a/k/a outlines), and page labels to the given output filename or (if no output is given)\n"
    +"          to stdout. Does not create a new PDF.\n"
    +"\n"
    +"      dump_data_fields\n"
    +"           Reads a single, input PDF file and reports form field statistics to the given output filename\n"
    +"           or (if no output is given) to stdout. Does not create a new PDF.\n"
    +"\n"
    +"  [flatten]\n"
    +"      Use this option to merge an input PDF's interactive form fields and their data with the PDF's pages. \n"
    +"      Sometimes used with the fill_form operation.\n";
        }
        return "SYNOPSIS\n"
            +"  pdf_form.py <input PDF file | - | PROMPT> [ <operation> <operation arguments> ]\n"
            +"      [ output <output filename | - | PROMPT> ] [ flatten ]\n"
            +"  Where:\n"
            +"      <operation> may be empty, or: [generate_fdf | fill_form |dump_data |dump_data_fields]\n"
            +"\n"
            +"  For Complete Help: pdf_form.py -h\n";
    }

    public static String generate_fdf(PDAcroForm acro){
        //Generates and ugly but functional fdf file.
        //That said, PDFTK's has unreadable characters and this doesn't.
        //Outputs to stdout if no output given (Follows PDFTK's docs rather than function)

        try(ByteArrayOutputStream x = new ByteArrayOutputStream()){
            acro.exportFDF().save(x);
            return x.toString("UTF-8");

        }catch (IOException e){
            System.out.println("Not sure how this happened");
            System.out.println(e);
            return "";

        }catch (NullPointerException e){
            return "%FDF-1.2\n"
                 + "%????\n"
                 + "1 0 obj\n"
                 + "<< /FDF 2 0 R >>\n"
                 + "endobj\n"
                 + "2 0 obj\n"
                 + "<< /Fields [] >>\n"
                 + "endobj\n"
                 + "trailer\n"
                 + "<< /Root 1 0 R >>\n"
                 + "%%EOF";
        }
    }

    public static void fill_form(PDAcroForm acro, String fdf){
        //Same as PDFTK (possibly slightly different pdfs made, but they look the same).

        FDFDocument f;
        try{
            if("-".equals(fdf)){
                f = FDFDocument.load(System.in);
            }else{
                f = FDFDocument.load(new File(fdf));
            }
            acro.importFDF(f);
            f.close();
            acro.refreshAppearances(acro.getFields());

        }catch (IOException e){
            System.err.println("Error: Failed to load form data file:");
            System.err.println(fdf);
            System.err.println(e);
            System.err.println("No output created.");
        }
    }

    public static String dump_data(PDDocument in) throws IOException{
        //PDFID0 and PDFID1 may not always be correct.

        COSDictionary info = in.getDocumentInformation().getCOSObject();
        String output = "";
        for (COSName temp : info.keySet()) {
            output += "InfoKey: " + temp.getName() + "\n";
            output += "InfoValue: " + info.getString(temp) + "\n";
        }
        /*COSArray id=in.getDocument().getDocumentID();*/
        output += "PdfID0: ";
        String id0 = in.getDocument().getDocumentID().getString(0);
        for(int i=0; i<id0.length(); i++){
            output += (Integer.toHexString(id0.codePointAt(i) & 0xFF));
        }
        output += "\nPdfID1: ";
        String id1 = in.getDocument().getDocumentID().getString(1);
        for(int i=0; i<id1.length(); i++){
            output += (Integer.toHexString(id1.codePointAt(i) & 0xFF));
        }
        output += "\n";
        output += "NumberOvPages: "+ in.getNumberOfPages();
        return output;
    }

    public static String dump_data_fields(PDAcroForm acro){
        //Copy of PDFTK

        try{
        String line = "---\nFieldType: %s\n"
                   + "FieldName: %s\n"
                   + "%sFieldFlags: %s\n"
                   + "%s%sFieldJustification: %s\n";
        String output = "";
        HashMap<String, String> field_types=new HashMap<String, String>();
        field_types.put("Tx", "Text");
        field_types.put("Btn", "Button");
        field_types.put("Ch", "Choice");

        HashMap<Integer, String> quad=new HashMap<Integer, String>();
        quad.put(0, "left");
        quad.put(1, "Centered");
        quad.put(2, "right");

        for (Iterator<PDField> fields=acro.getFieldIterator(); fields.hasNext();){
            PDField field = fields.next();
            String[] pieces = {field_types.get(field.getFieldType()),
                               field.getPartialName(),
                               "",
                               "" + field.getFieldFlags(),
                               "",
                               "",
                               quad.get(field.getAcroForm().getQ())
                               };

            if(field.getAlternateFieldName() != null)
                pieces[2] = "FieldNameAlt: " + field.getAlternateFieldName() + '\n';
            else
                pieces[2] = "";
            if(field.getValueAsString() != "")
                pieces[4]="FieldValue: " + field.getValueAsString() + "\n";
            try{
                if(field.getCOSObject().getNameAsString("DV") != "")
                    pieces[5] = "FieldValueDefault: " + field.getCOSObject().getNameAsString("DV") + "\n";
            }catch(NullPointerException e){}
            output = output + String.format(line, pieces[0], pieces[1], pieces[2], pieces[3],
                                            pieces[4], pieces[5], pieces[6]);
        }
        return output;
        }catch(NullPointerException e){
            System.err.println(e);
            return "";
        }
    }
}
\$\endgroup\$
  • \$\begingroup\$ pdftk development is continued at gitlab.com/marcvinyals/pdftk \$\endgroup\$ – gliptak Apr 9 '18 at 20:15
  • \$\begingroup\$ how does this work with the original licence?, the author of the original had a weird custom license that probably violated it's own terms, making things complicated legally. \$\endgroup\$ – Oscar Smith Apr 9 '18 at 23:33
  • \$\begingroup\$ maybe you can open an issue at gitlab.com/marcvinyals/pdftk/issues discussing? the new codebase doesn't have a LICENSE file (at the project root ...) \$\endgroup\$ – gliptak Apr 11 '18 at 0:37
2
\$\begingroup\$
  • For formatting it helps tremendously to use an editor/IDE that does that for you, at least while you're still starting out. E.g. with IDEA it's one key-stroke to reformat it according to the current preset.
  • Next, you should try and use one of the existing libraries for command-line argument parsing. I'm not going to try and go through the whole main function since there's just too much going on in there.
  • doc.close() is being called manually at the end - consider not doing that, because the program ends right after anyway, or use the try (PDDocument doc = ...) { } syntax to automatically close the document (assuming it's AutoCloseable and if not perhaps create a helper for you). That goes for all resources that can, in the widest sense, be "closed" or "freed". Actually you're already doing that in some cases, so be consistent.
  • In a real program I'd probably put the documentation into a separate file and load it from there (as a resource from the generated JAR file), since that makes editing and working with it much easier.
  • Catching NullPointerExceptions is probably wrong. I can't even tell where they're coming from, so even if you are catching them, move the scope of the exception handler right over the specific call, otherwise it's really hard to figure out where this kind of exception came from originally. Oh and even worse catching it and not doing anything with the exception.
  • Now the part about if being slow ... well that's hard to tell as you're mostly using some API. Consider using a profiler to figure out what's actually the slow part.
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.