2
\$\begingroup\$

Introduction

If you know what FDF files are, you can skip this section.

PDF files sometimes includes form fields. These fields can be represented as FDF in plain text form, extracted using certain utilities. Important things to note about this file are:

A field is represented by

/V ()
/T (Field)

where "Field" is the field name and whatever is written within the parenthesis next to /V will become the input for the utility that will fill the form.

It is possible for there to be nested fields of arbitrary depth. A basic example is

/Kids [
<<
/V ()
/T (node1)
>> 
<<
/V ()
/T (node2)
>>]
/T (root)

Here there are two fields node1 and node2 both nested under root. When fields are nested, names that are seen by a user are the components of the hierarchy, separated by .s. For instance name of node1 is root.node1.

The code

The big picture goal of this is to identify the lines that need to be modified when a user provides a bunch of field names (eg. Field, root.node1)

This code reads the FDF file and marks the lines that should be modified to fill a field of a given name. The way I do it involves iterating every line to detect the tree roots and append the name of the roots to every child. Since this will end up in CRAN I want to make sure the approach isn't overly convoluted.

fdfAnnotate = function(fdfLines){
  fields = vector(length = length(fdfLines),mode= 'character')
  nests = 0
  # iterate over every line
  for (i in seq_along(fdfLines)){
    if(grepl('/T \\(',fdfLines[i])){
      # /T represents a field or a root name
      # take the name
      name = stringr::str_extract(fdfLines[i],'(?<=\\().*?(?=\\))')
      if(grepl('/V',fdfLines[i-1])){
        # if the line before the naming line starts with /V
        # there is no hierarhcy, just name the line
        fields[i-1] = name
      } else if(grepl('>>\\]',fdfLines[i-1])){
        # if the line above the name is >>] the name represents a root
        # start reading from the line above
        z = i-2
        # this keeps track of the nest levels.
        # we will be reading the file backwards trying to
        # reach to the end of this root
        nest = 1
        while(nest!=0){
          if(grepl('/V',fdfLines[z])){
            # if a field is found, append the name of the root to the left
            # separated by a "."
            fields[z] = paste0(name,'.',fields[z])
          } else if(grepl('>>\\]',fdfLines[z])){
            # if another nest stops, that means we are inside another root
            nest = nest + 1
          } else if(grepl('/Kids \\[',fdfLines[z])){
            # every time a root closes reduce the nest. if you reach 0
            # it means its over
            nest = nest - 1
          }
          # go back one line in the file.
          z = z - 1
        }
      }
    }
  }
  data.frame(fdfLines,fields,stringsAsFactors = FALSE)
}

Usage

You can use it by doing

fdfLines = readLines([pathToFDFfile])
fdfAnnotate(fdfLines)

Below is the FDF file I use for my tests. It includes single and double layered hierarchies as well as a bunch of normal fields.

%FDF-1.2
%âãÏÓ
1 0 obj 
<<
/FDF 
<<
/Fields [
<<
/V ()
/T (node1)
>> 
<<
/Kids [
<<
/Kids [
<<
/V ()
/T (node1)
>> 
<<
/V ()
/T (node3)
>> 
<<
/V ()
/T (node2)
>>]
/T (child)
>> 
<<
/Kids [
<<
/V ()
/T (node1)
>> 
<<
/V ()
/T (node2)
>>]
/T (child2)
>>]
/T (hierarchy2)
>> 
<<
/V ()
/T (TextField1)
>> 
<<
/V ()
/T (TextField2)
>> 
<<
/V ()
/T (TextFieldPage2)
>> 
<<
/V ()
/T (List Box)
>> 
<<
/V ()
/T (TextFieldPage3)
>> 
<<
/Kids [
<<
/V ()
/T (node1)
>> 
<<
/V ()
/T (node4)
>> 
<<
/V ()
/T (node3)
>> 
<<
/V ()
/T (node2)
>>]
/T (hierarchy)
>> 
<<
/V ()
/T (betweenHierarch)
>> 
<<
/V /Off
/T (RadioGroup)
>> 
<<
/V /Off
/T (checkBox)
>>]
>>
>>
endobj 
trailer

<<
/Root 1 0 R
>>
%%EOF

This file represents the form fields in this pdf file.

Using my function, the output is:

                fdfLines                  fields
1               %FDF-1.2                        
2                  %âãÏÓ                        
3               1 0 obj                         
4                     <<                        
5                  /FDF                         
6                     <<                        
7              /Fields [                        
8                     <<                        
9                  /V ()                   node1
10            /T (node1)                        
11                   >>                         
12                    <<                        
13               /Kids [                        
14                    <<                        
15               /Kids [                        
16                    <<                        
17                 /V ()  hierarchy2.child.node1
18            /T (node1)                        
19                   >>                         
20                    <<                        
21                 /V ()  hierarchy2.child.node3
22            /T (node3)                        
23                   >>                         
24                    <<                        
25                 /V ()  hierarchy2.child.node2
26            /T (node2)                        
27                   >>]                        
28            /T (child)                        
29                   >>                         
30                    <<                        
31               /Kids [                        
32                    <<                        
33                 /V () hierarchy2.child2.node1
34            /T (node1)                        
35                   >>                         
36                    <<                        
37                 /V () hierarchy2.child2.node2
38            /T (node2)                        
39                   >>]                        
40           /T (child2)                        
41                   >>]                        
42       /T (hierarchy2)                        
43                   >>                         
44                    <<                        
45                 /V ()              TextField1
46       /T (TextField1)                        
47                   >>                         
48                    <<                        
49                 /V ()              TextField2
50       /T (TextField2)                        
51                   >>                         
52                    <<                        
53                 /V ()          TextFieldPage2
54   /T (TextFieldPage2)                        
55                   >>                         
56                    <<                        
57                 /V ()                List Box
58         /T (List Box)                        
59                   >>                         
60                    <<                        
61                 /V ()          TextFieldPage3
62   /T (TextFieldPage3)                        
63                   >>                         
64                    <<                        
65               /Kids [                        
66                    <<                        
67                 /V ()         hierarchy.node1
68            /T (node1)                        
69                   >>                         
70                    <<                        
71                 /V ()         hierarchy.node4
72            /T (node4)                        
73                   >>                         
74                    <<                        
75                 /V ()         hierarchy.node3
76            /T (node3)                        
77                   >>                         
78                    <<                        
79                 /V ()         hierarchy.node2
80            /T (node2)                        
81                   >>]                        
82        /T (hierarchy)                        
83                   >>                         
84                    <<                        
85                 /V ()         betweenHierarch
86  /T (betweenHierarch)                        
87                   >>                         
88                    <<                        
89               /V /Off              RadioGroup
90       /T (RadioGroup)                        
91                   >>                         
92                    <<                        
93               /V /Off                checkBox
94         /T (checkBox)                        
95                   >>]                        
96                    >>                        
97                    >>                        
98               endobj                         
99               trailer                        
100                                             
101                   <<                        
102          /Root 1 0 R                        
103                   >>                        
104                %%EOF                        
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I tried to wrap my head around this file format (what a weird one?!) and came to the realization that it is a lot easier to build the tree structure if you read the file from the bottom-up, would you agree? Based on that, I came up with this much simpler implementation where I only maintain a stack (last in, first out) of field names. The output is the same for your example data and I hope I did not miss anything.

fdfAnnotate <- function(fdfLines) {
    fields <- vector(length = length(fdfLines), mode = "character")
    store <- NULL
    for (i in rev(seq_along(fdfLines))) {
        line <- fdfLines[i]
        if (grepl("/V", line)) {
            fields[i] <- paste(store, collapse = ".")
            store <- head(store, -1)
        } else if (grepl("/T [(]", line)) {
            name <- sub(".*[(](.*)[)].*", "\\1", line)
            store <- c(store, name)
        } else if (grepl("/Kids \\[", line)) {
           store <- head(store, -1)
        }
    }
    data.frame(fdfLines, fields, stringsAsFactors = FALSE)
}

Some general comments or other recommendations

  1. Considering this will go on CRAN, I would try to limit package dependencies as much as possible. Here I replaced stringr::str_extract with a call to the base sub
  2. You are not using the commonly accepted syntax, to list a few: use <- for assignments; use double quotes instead of single quotes; use a space after a comma, between binary operators, after if, before {
  3. The code is making a lot of assumptions about the input, essentially that the input comes from a valid fdf file. Maybe some of these assumptions ought to be tested
  4. Are the regex patterns strong enough? For example, should you be using ^ and $ where appropriate?
\$\endgroup\$
2
  • \$\begingroup\$ Huh never used the head with a negative integer before. It is a little late for not having stringr as a dependency as I originally used it to solve an encoding issue that I couldn't solve using base R but using sub seems to make the function 30% faster. Initially I assumed your's would run faster since it doesn't have that while loop that traces back but it is actually slower (15%) on the example data, at least after replacing the str_replace with sub. Can't quite tell why. Maybe it's the growing store? \$\endgroup\$
    – OganM
    Jan 25, 2019 at 4:27
  • \$\begingroup\$ Also thanks for pointing out the assumptions. It failed horribly when the field names include paranthesis \$\endgroup\$
    – OganM
    Jan 25, 2019 at 4:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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