File Coverage

blib/lib/CAM/PDF/PageText.pm
Criterion Covered Total %
statement 44 77 57.1
branch 20 42 47.6
condition 2 27 7.4
subroutine 5 9 55.5
pod 1 1 100.0
total 72 156 46.1


line stmt bran cond sub pod time code
1             package CAM::PDF::PageText;
2              
3 1     1   32 use 5.006;
  1         3  
  1         40  
4 1     1   5 use warnings;
  1         2  
  1         43  
5 1     1   6 use strict;
  1         2  
  1         1085  
6              
7             our $VERSION = '1.60';
8              
9             =head1 NAME
10              
11             CAM::PDF::PageText - Extract text from PDF page tree
12              
13             =head1 SYNOPSIS
14              
15             my $pdf = CAM::PDF->new($filename);
16             my $pageone_tree = $pdf->getPageContentTree(1);
17             print CAM::PDF::PageText->render($pageone_tree);
18              
19             =head1 DESCRIPTION
20              
21             This module attempts to extract sequential text from a PDF page. This
22             is not a robust process, as PDF text is graphically laid out in
23             arbitrary order. This module uses a few heuristics to try to guess
24             what text goes next to what other text, but may be fooled easily by,
25             say, subscripts, non-horizontal text, changes in font, form fields
26             etc.
27              
28             All those disclaimers aside, it is useful for a quick dump of text
29             from a simple PDF file.
30              
31             =head1 LICENSE
32              
33             Same as L
34              
35             =head1 FUNCTIONS
36              
37             =over
38              
39             =item $pkg->render($pagetree)
40              
41             =item $pkg->render($pagetree, $verbose)
42              
43             Turn a page content tree into a string. This is a class method that
44             should be called like:
45              
46             CAM::PDF::PageText->render($pagetree);
47              
48             =cut
49              
50             sub render
51             {
52 6     6 1 11 my $pkg = shift;
53 6         12 my $pagetree = shift;
54 6         13 my $verbose = shift;
55              
56 6         12 my $str = q{};
57 6         11 my @stack = ([@{$pagetree->{blocks}}]);
  6         39  
58 6         14 my $in_textblock = 0;
59              
60             ## The stack is a list of blocks. We do depth-first on blocks, but
61             ## we must be sure to traverse the children of the blocks in their
62             ## original order.
63              
64 6         24 while (@stack > 0)
65             {
66             # keep grabbing the same node until it's empty
67 3705         4581 my $node = $stack[-1];
68 3705 100       6046 if (ref $node)
69             {
70 3541 100       3643 if (@{$node} > 0) # Still has children?
  3541         6590  
71             {
72 3162         2825 my $block = shift @{$node}; # grab the next child
  3162         4509  
73 3162 100       11078 if ($block->{type} eq 'block')
    100          
74             {
75 373 100       893 if ($block->{name} eq 'BT')
76             {
77             # Insert a flag on the stack to say when we leave the BT block
78 164         235 push @stack, 'BT';
79 164         193 $in_textblock = 1;
80             }
81 373         418 push @stack, [@{$block->{value}}]; # descend
  373         2374  
82             }
83             elsif ($in_textblock)
84             {
85 1758 50       3914 if ($block->{type} ne 'op')
86             {
87 0         0 die 'misconception';
88             }
89 1758         1684 my @args = @{$block->{args}};
  1758         5214  
90              
91 1758 50       10179 $str = $block->{name} eq 'TJ' ? _TJ( $str, \@args )
    50          
    50          
    50          
    50          
    100          
    50          
92             : $block->{name} eq 'Tj' ? _Tj( $str, \@args )
93             : $block->{name} eq q{\'} ? _Tquote( $str, \@args )
94             : $block->{name} eq q{\"} ? _Tquote( $str, \@args )
95             : $block->{name} eq 'Td' ? _Td( $str, \@args )
96             : $block->{name} eq 'TD' ? _Td( $str, \@args )
97             : $block->{name} eq 'T*' ? _Tstar( $str )
98             : $str;
99             }
100             }
101             else
102             {
103             # Node is now empty, clear it from the stack
104 379         979 pop @stack;
105             }
106             }
107             else
108             {
109             # This is the 'BT' flag we pushed on the stack above
110 164         206 pop @stack;
111 164         191 $in_textblock = 0;
112              
113             # Add a line break to divide the text
114 164         13607 $str =~ s/ [ ]* \z /\n/xms;
115             }
116             }
117 6         11502 return $str;
118             }
119              
120             sub _TJ
121             {
122 0     0   0 my $str = shift;
123 0         0 my $args_ref = shift;
124              
125 0 0 0     0 if (@{$args_ref} != 1 || $args_ref->[0]->{type} ne 'array')
  0         0  
126             {
127 0         0 die 'Bad TJ';
128             }
129              
130 0         0 $str =~ s/ (\S) \z /$1 /xms;
131 0         0 foreach my $node (@{$args_ref->[0]->{value}})
  0         0  
132             {
133 0 0 0     0 if ($node->{type} eq 'string' || $node->{type} eq 'hexstring')
    0          
134             {
135 0         0 $str .= $node->{value};
136             }
137             elsif ($node->{type} eq 'number')
138             {
139             # Heuristic:
140             # "offset of more than a quarter unit forward"
141             # means significant positive spacing
142 0 0       0 if ($node->{value} < -250)
143             {
144 0         0 $str =~ s/ (\S) \z /$1 /xms;
145             }
146             }
147             }
148 0         0 return $str;
149             }
150              
151             sub _Tj
152             {
153 795     795   1172 my $str = shift;
154 795         832 my $args_ref = shift;
155              
156 795 50 33     785 if (@{$args_ref} < 1 ||
  795   33     3451  
157             ($args_ref->[-1]->{type} ne 'string' && $args_ref->[-1]->{type} ne 'hexstring'))
158             {
159 0         0 die 'Bad Tj';
160             }
161              
162 795         8925 $str =~ s/ (\S) \z /$1 /xms;
163              
164 795         4782 return $str . $args_ref->[-1]->{value};
165             }
166              
167             sub _Tquote
168             {
169 0     0     my $str = shift;
170 0           my $args_ref = shift;
171              
172 0 0 0       if (@{$args_ref} < 1 ||
  0   0        
173             ($args_ref->[-1]->{type} ne 'string' && $args_ref->[-1]->{type} ne 'hexstring'))
174             {
175 0           die 'Bad Tquote';
176             }
177              
178 0           $str =~ s/ [ ]* \z /\n/xms;
179              
180 0           return $str . $args_ref->[-1]->{value};
181             }
182              
183             sub _Td
184             {
185 0     0     my $str = shift;
186 0           my $args_ref = shift;
187              
188 0 0 0       if (@{$args_ref} != 2 ||
  0   0        
189             $args_ref->[0]->{type} ne 'number' ||
190             $args_ref->[1]->{type} ne 'number')
191             {
192 0           die 'Bad Td/TD';
193             }
194              
195             # Heuristic:
196             # "move down in Y, and Y motion a large fraction of the X motion"
197             # means new line
198 0 0 0       if ($args_ref->[1]->{value} < 0 &&
199             2 * (abs $args_ref->[1]->{value}) > abs $args_ref->[0]->{value})
200             {
201 0           $str =~ s/ [ ]* \z /\n/xms;
202             }
203              
204 0           return $str;
205             }
206              
207             sub _Tstar
208             {
209 0     0     my $str = shift;
210              
211 0           $str =~ s/ [ ]* \z /\n/xms;
212              
213 0           return $str;
214             }
215              
216             1;
217             __END__