File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 479 1.0
branch 0 202 0.0
condition 0 12 0.0
subroutine 2 38 5.2
pod 0 36 0.0
total 7 767 0.9


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   74134 use v5.10;
  1         3  
4 1     1   5 use strict;
  1         1  
  1         5172  
5             # use warnings;
6             $^W = 0;
7             our $VERSION = "1.36";
8              
9             my ($GinFile, $GpageObjNr, $GrootNr, $Gpos, $GobjNr, $Gstream, $GoWid, $GoHei);
10             my (@Gkids, @Gcounts, @GmediaBox, @Gobject, @Gparents, @Gto_be_created);
11             my (%GpageXObject, %GObjects, %Gpaper);
12              
13             my $cr = '\s*(?:\015|\012|(?:\015\012))';
14             my ( $IN_FILE, $OUT_FILE );
15              
16             # ISO 216 paper sizes in pt (four decimals will do):
17             my $JH = 1190.5512; # [J] A3 ~ 420 mm (H)
18             my $JW = 841.8898; # [J] A3 ~ 297 mm (W)
19             my $AH = $JW; # [A] A4 ~ 297 mm (H)
20             my $AW = 595.2756; # [A] A4 ~ 210 mm (W)
21             my $BH = $AW; # [B] A5 ~ 210 mm (H)
22             my $BW = 419.5276; # [B] A5 ~ 148 mm (W)
23             my $CH = $BW; # [C] A6 ~ 148 mm (H)
24             my $CW = 297.6378; # [C] A6 ~ 105 mm (W)
25             # + 1 mm (2.8346 pt) to account for rounding in ISO 216 (148+148=296):
26             my $CX = 422.3622; # [C] A6 $CH + 1 mm (H)
27             my $BX = $CX; # [B] A5 $BW + 1 mm (W)
28              
29             # US paper sizes in pt:
30             my $DH = 792; # [D] US Letter Full (H)
31             my $DW = 612; # [D] US Letter Full (W)
32             my $EH = $DW; # [E] US Letter Half (H)
33             my $EW = 396; # [E] US Letter Half (W)
34             my $FH = $EW; # [F] US Letter Quarter (H)
35             my $FW = 306; # [F] US Letter Quarter (W)
36             my $GH = 1008; # [G] US Legal Full (H)
37             my $GW = $DW; # [G] US Legal Full (W)
38             my $HH = $DW; # [H] US Legal Half (H)
39             my $HW = 504; # [H] US Legal Half (W)
40             my $IH = $HW; # [I] US Legal Quarter (H)
41             my $IW = $FW; # [I] US Legal Quarter (W)
42             my $KH = 1224; # [K] US Tabloid (H)
43             my $KW = $DH; # [K] US Tabloid (W)
44              
45             # Paper surfaces in square pts (expressed as HxW in pts):
46             %Gpaper = (
47             QuarterLetter => $FH * $FW, # = 121_176
48             A6 => $CH * $CW, # ~ 124_867
49             QuarterLegal => $IH * $IW, # = 154_224
50             HalfLetter => $EH * $EW, # = 242_352
51             A5 => $BH * $BW, # ~ 249_735
52             HalfLegal => $HH * $HW, # = 308_448
53             Letter => $DH * $DW, # = 484_704
54             A4 => $AH * $AW, # ~ 501_156
55             Legal => $GH * $GW, # = 616_896
56             Tabloid => $KH * $KW, # = 969_408
57             A3 => $JH * $JW, # ~ 1_002_312
58             );
59              
60             ##########################################################
61             sub main {
62             ##########################################################
63 0     0 0   my $input = $ARGV[0];
64              
65             # Page reordering and position offset schemas for "4 up":
66 0           my @P_4UP_13PLUS = (16,1,13,4,2,15,3,14,12,5,9,8,6,11,7,10);
67 0           my @P_4UP_9PLUS = (12,1,9,4,2,11,3,10,6,7,9999,9999,8,5);
68 0           my @P_4UP_5PLUS = (8,1,5,4,2,7,3,6);
69 0           my @P_4UP_1PLUS = (4,1,9999,9999,2,3);
70 0           my @X_A6_ON_A4 = (000,$CW,$CW,$AW) x 4;
71 0           my @Y_A6_ON_A4 = ($CX,$CX,$CH,$CH) x 4;
72 0           my @X_QT_ON_LT = (000,$FW,$FW,$DW) x 4;
73 0           my @Y_QT_ON_LT = ($FH,$FH,$FH,$FH) x 4;
74 0           my @X_QG_ON_LG = (000,$IW,$IW,$GW) x 4;
75 0           my @Y_QG_ON_LG = ($IH,$IH,$IH,$IH) x 4;
76              
77             # Page reordering and position offset schemas for "2 up":
78 0           my @P_2UP_13PLUS = (1,16,2,15,3,14,4,13,5,12,6,11,7,10,8,9);
79 0           my @P_2UP_9PLUS = (1,12,2,11,3,10,4,9,5,8,6,7);
80 0           my @P_2UP_5PLUS = (1,8,2,7,3,6,4,5);
81 0           my @P_2UP_1PLUS = (1,4,2,3);
82 0           my @X_A5_ON_A4 = ($BH,$BH,000,000) x 4;
83 0           my @Y_A5_ON_A4 = ($BX,000,$AH,$BX) x 4;
84 0           my @X_HT_ON_LT = ($EH,$EH,000,000) x 4;
85 0           my @Y_HT_ON_LT = ($EW,000,$DH,$EW) x 4;
86 0           my @X_HG_ON_LG = ($HH,$HH,000,000) x 4;
87 0           my @Y_HG_ON_LG = ($HW,000,$GH,$HW) x 4;
88 0           my @X_LT_ON_TA = ($DH,$DH,000,000) x 4;
89 0           my @Y_LT_ON_TA = ($DW,000,$KH,$DW) x 4;
90 0           my @X_A4_ON_A3 = ($AH,$AH,000,000) x 4;
91 0           my @Y_A4_ON_A3 = ($AW,000,$JH,$AW) x 4;
92              
93 0           my ($inpPgNum, $inpPgSize);
94 0           my $numPagImposed = 0;
95 0           my $sayUsage = "Usage: paperback file.pdf (will produce 'file-paperback.pdf').";
96 0           my $sayVers = "This is paperback v${VERSION}, (c) 2022 Hector M. Monacci.";
97 0           my $sayHelp = <<"END_MESSAGE";
98             ${sayUsage}
99              
100             All pages in the input PDF file will be imposed on a new PDF with
101             bigger paper size, ready to be duplex-printed, folded and put together
102             into signatures, according to its original page size. Input PDF is
103             always assumed to be composed of vertical pages of the same size.
104              
105             Input page sizes allowed are A4, A5, A6, Letter, Half Letter, Quarter
106             Letter, Half Legal, Quarter Legal. Other sizes give an error message.
107              
108             Only PDF v1.4 is supported as input, although many higher-labeled
109             PDF files are correctly handled since they are essentially v1.4 PDF
110             files stamped as something more modern. Encrypted PDFs are not supported.
111              
112             ISO 216 normalised (international) page sizes:
113              
114             Input page sizes A6 (105 x 148 mm) and A5 (148 x 210 mm) produce an
115             output page size of A4 (210 x 297 mm). Input page size A4 (210 x 297 mm)
116             produces an output page size of A3 (297 x 420 mm). Four A6 pages will
117             be put on each A4 page, two A5 pages will be put on each A4 page, or
118             two A4 pages will be put on each A3 page. Before that, input pages will
119             be reordered and reoriented so as to produce a final PDF fit for duplex
120             'long-edge-flip' printing.
121              
122             ANSI normalised (US) page sizes:
123              
124             Input page sizes Quarter Letter (4.25 x 5.5 in) and Half Letter (5.5
125             x 8.5 in) produce a Letter output page size (8.5 x 11 in). Input
126             page sizes Quarter Legal (4.25 x 7 in) and Half Legal (7 x 8.5 in)
127             produce a Legal output page size (8.5 x 14 in). Input page size Letter
128             (8.5 x 11 in) produces a Tabloid output page size (11 x 17 in).
129              
130             Four Quarter-Letter pages will be put on each Letter page, two Half-Letter
131             pages will be put on each Letter page, four Quarter-Legal pages will be
132             put on each Legal page, two Half-Legal pages will be put on each Legal page,
133             or two Letter pages will be put on each Tabloid page. Before that, input
134             pages will be reordered and reoriented so as to produce a final PDF fit for
135             duplex 'long-edge-flip' printing.
136              
137             For further details, please try 'perldoc paperback'.
138              
139             ${sayVers}
140             END_MESSAGE
141              
142 0 0         die "[!] ${sayUsage}\n" if ! defined $input;
143 0 0 0       do {print STDERR "${sayHelp}"; exit}
  0            
  0            
144             if $input =~ "^-h\$" or $input =~ "^--help\$";
145 0 0 0       do {print STDERR "${sayVers}\n"; exit}
  0            
  0            
146             if $input =~ "^-v\$" or $input =~ "^--version\$";
147 0           ($inpPgNum, $inpPgSize) = openInputFile($input);
148              
149 0           my ($pgPerOutputPage, @x, @y);
150 0           for ($inpPgSize) {
151 0 0         if ($_ eq "A6") { $pgPerOutputPage = 4; @x = @X_A6_ON_A4; @y = @Y_A6_ON_A4; }
  0 0          
  0 0          
  0 0          
    0          
    0          
    0          
    0          
152 0           elsif ($_ eq "A5") { $pgPerOutputPage = 2; @x = @X_A5_ON_A4; @y = @Y_A5_ON_A4; }
  0            
  0            
153 0           elsif ($_ eq "QT") { $pgPerOutputPage = 4; @x = @X_QT_ON_LT; @y = @Y_QT_ON_LT; }
  0            
  0            
154 0           elsif ($_ eq "QG") { $pgPerOutputPage = 4; @x = @X_QG_ON_LG; @y = @Y_QG_ON_LG; }
  0            
  0            
155 0           elsif ($_ eq "HT") { $pgPerOutputPage = 2; @x = @X_HT_ON_LT; @y = @Y_HT_ON_LT; }
  0            
  0            
156 0           elsif ($_ eq "HG") { $pgPerOutputPage = 2; @x = @X_HG_ON_LG; @y = @Y_HG_ON_LG; }
  0            
  0            
157 0           elsif ($_ eq "LT") { $pgPerOutputPage = 2; @x = @X_LT_ON_TA; @y = @Y_LT_ON_TA; }
  0            
  0            
158 0           elsif ($_ eq "A4") { $pgPerOutputPage = 2; @x = @X_A4_ON_A3; @y = @Y_A4_ON_A3; }
  0            
  0            
159 0           else {die "[!] Bad page size (${_}). Try 'paperback -h' for more info.\n"}
160             }
161              
162 0           my ($name) = $input =~ /(.+)\.[^.]+$/;
163 0           openOutputFile("${name}-paperback.pdf");
164 0           my ($rot_extra, @p);
165 0 0         if ($pgPerOutputPage == 4) {
166 0           $rot_extra = 0;
167 0 0         @p = $inpPgNum >= 13 ? @P_4UP_13PLUS :
    0          
    0          
168             $inpPgNum >= 9 ? @P_4UP_9PLUS :
169             $inpPgNum >= 5 ? @P_4UP_5PLUS : @P_4UP_1PLUS;
170             } else {
171 0           $rot_extra = 90;
172 0 0         @p = $inpPgNum >= 13 ? @P_2UP_13PLUS :
    0          
    0          
173             $inpPgNum >= 9 ? @P_2UP_9PLUS :
174             $inpPgNum >= 5 ? @P_2UP_5PLUS : @P_2UP_1PLUS;
175             }
176 0           my $lastSignature = $inpPgNum >> 4;
177 0           my ($rotation, $target_page);
178 0           for (my $thisSignature = 0; $thisSignature <= $lastSignature; ++$thisSignature) {
179 0           for (0 .. 15) {
180 0 0         &newPageInOutputFile if $_ % $pgPerOutputPage == 0;
181 0           $target_page = $p[$_] + 16 * $thisSignature;
182 0 0         next if $target_page > $inpPgNum;
183              
184 0 0         $rotation = $_ % 4 > 1 ? $rot_extra + 180 : $rot_extra;
185 0           copyPageFromInputToOutput ({page => $target_page,
186             rotate => $rotation, x => $x[$_], y => $y[$_]});
187 0           ++$numPagImposed;
188 0 0         last if $numPagImposed == $inpPgNum;
189             }
190             }
191 0           &closeInputFile;
192 0           &closeOutputFile;
193             }
194              
195             &main if not caller();
196              
197              
198             ##########################################################
199             sub newPageInOutputFile {
200             ##########################################################
201 0 0   0 0   die "[!] No output file, you must call openOutputFile first.\n" if ! $Gpos;
202 0 0         &writePage if $Gstream;
203              
204 0           ++$GobjNr;
205 0           $GpageObjNr = $GobjNr;
206 0           undef %GpageXObject;
207              
208 0           return;
209             }
210              
211              
212             ##########################################################
213             sub copyPageFromInputToOutput {
214             ##########################################################
215 0 0   0 0   die "[!] No output file, you have to call openOutputFile first.\n" if ! $Gpos;
216 0           my $param = $_[0];
217 0 0         my $pagenumber = $param->{'page'} or 1;
218 0 0         my $x = $param->{'x'} or 0;
219 0 0         my $y = $param->{'y'} or 0;
220 0 0         my $rotate = $param->{'rotate'} or 0;
221              
222 0           state $formNr; # Este uso de "state" requiere v5.10 (que salió en 2007)
223 0           ++$formNr;
224              
225 0           my $name = "Fm${formNr}";
226 0           my ($formRes, $formCont) = getPage($pagenumber);
227 0           my $refNr = writeRes($formRes, $formCont);
228 0 0         die "[!] Page ${pagenumber} in ${GinFile} can't be used. Concatenate streams!\n"
229             if !defined $refNr;
230 0 0         die "[!] Page ${pagenumber} doesn't exist in file ${GinFile}.\n" if !$refNr;
231 0           &writePageObjectsToOutputFile;
232              
233 0           $Gstream .= "q\n". calcRotateMatrix($x, $y, $rotate) ."\n/Gs0 gs\n/${name} Do\nQ\n";
234 0           $GpageXObject{$name} = $refNr;
235              
236 0           return;
237             }
238              
239              
240             ##########################################################
241             sub setInitGrState {
242             ##########################################################
243 0     0 0   ++$GobjNr;
244              
245 0           $Gobject[$GobjNr] = $Gpos;
246 0           $Gpos += syswrite $OUT_FILE,
247             "${GobjNr} 0 obj<>endobj\n";
248 0           return;
249             }
250              
251              
252             ##########################################################
253             sub createPageResourceDict {
254             ##########################################################
255 0     0 0   my $resourceDict = "/ProcSet[/PDF/Text]/XObject<<";
256 0           $resourceDict .= "/${_} ${GpageXObject{${_}}} 0 R" for keys %GpageXObject;
257 0           $resourceDict .= ">>/ExtGState<>";
258             # return $resourceDict;
259             }
260              
261              
262             ##########################################################
263             sub writePageResourceDict {
264             ##########################################################
265 0     0 0   my $resourceDict = $_[0];
266              
267 0           state %resources;
268              
269             # Found one identical, use it:
270 0 0         return $resources{$resourceDict} if exists $resources{$resourceDict};
271 0           ++$GobjNr;
272             # Save first 10 resources:
273 0 0         $resources{$resourceDict} = $GobjNr if keys(%resources) < 10;
274 0           $Gobject[$GobjNr] = $Gpos;
275 0           $resourceDict = "${GobjNr} 0 obj<<${resourceDict}>>endobj\n";
276 0           $Gpos += syswrite $OUT_FILE, $resourceDict;
277 0           return $GobjNr;
278             }
279              
280              
281             ##########################################################
282             sub writePageStream {
283             ##########################################################
284 0     0 0   ++$GobjNr;
285 0           $Gobject[$GobjNr] = $Gpos;
286 0           $Gpos += syswrite $OUT_FILE, "${GobjNr} 0 obj<
287             . ">>stream\n${Gstream}\nendstream\nendobj\n";
288 0           $Gobject[$GpageObjNr] = $Gpos;
289 0           $Gstream = "";
290 0           return;
291             }
292              
293              
294             ##########################################################
295             sub writePageResources {
296             ##########################################################
297 0     0 0   my ($parent, $resourceObject) = ($_[0], $_[1]);
298 0           $Gpos += syswrite $OUT_FILE, "${GpageObjNr} 0 obj<
299             . "R/Contents ${GobjNr} 0 R/Resources ${resourceObject} 0 R>>endobj\n";
300 0           push @{ $Gkids[0] }, $GpageObjNr;
  0            
301 0           return;
302             }
303              
304              
305             ##########################################################
306             sub writePage {
307             ##########################################################
308 0 0   0 0   $Gparents[0] = ++$GobjNr if ! $Gparents[0];
309              
310 0           my $parent = $Gparents[0];
311 0           my $resourceObjectNr = writePageResourceDict(&createPageResourceDict);
312 0           &writePageStream;
313 0           writePageResources($parent, $resourceObjectNr);
314 0           ++$Gcounts[0];
315 0 0         writePageNodes(8) if $Gcounts[0] > 9;
316 0           return;
317             }
318              
319              
320             ##########################################################
321             sub closeOutputFile {
322             ##########################################################
323 0 0   0 0   return if !$Gpos;
324              
325 0 0         &writePage if $Gstream;
326 0           my $endNode = &writeEndNode;
327              
328 0           my $out_line = "1 0 obj<>endobj\n";
329 0           $Gobject[1] = $Gpos;
330 0           $Gpos += syswrite $OUT_FILE, $out_line;
331 0           my $qty = $#Gobject;
332 0           my $startxref = $Gpos;
333 0           my $xrefQty = $qty + 1;
334 0           $out_line = "xref\n0 ${xrefQty}\n0000000000 65535 f \n";
335 0           $out_line .= sprintf "%.10d 00000 n \n", $_ for @Gobject[1 .. $qty];
336 0           $out_line .= "trailer\n<<\n/Size ${xrefQty}\n/Root 1 0 R\n"
337             . ">>\nstartxref\n${startxref}\n%%EOF\n";
338              
339 0           syswrite $OUT_FILE, $out_line;
340 0           close $OUT_FILE;
341              
342 0           $Gpos = 0;
343 0           return;
344             }
345              
346              
347             ##########################################################
348             sub writePageNodes {
349             ##########################################################
350 0     0 0   my $qtyChildren = $_[0];
351 0           my $i = 0;
352 0           my $j = 1;
353 0           my $nodeObj;
354              
355 0           while ( $qtyChildren < $#{ $Gkids[$i] } ) {
  0            
356             # Imprimir padre actual y pasar al siguiente nivel:
357 0 0         $Gparents[$j] = ++$GobjNr if ! $Gparents[$j] ;
358              
359 0           $nodeObj =
360             "${Gparents[$i]} 0 obj<
361 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
362 0           $nodeObj .= "]\n/Count ${Gcounts[$i]}>>endobj\n";
363 0           $Gobject[ $Gparents[$i] ] = $Gpos;
364 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
365              
366 0           $Gcounts[$j] += $Gcounts[$i];
367 0           $Gcounts[$i] = 0;
368 0           $Gkids[$i] = [];
369 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
370 0           undef $Gparents[$i];
371 0           ++$i;
372 0           ++$j;
373             }
374 0           return;
375             }
376              
377              
378             ##########################################################
379             sub writeEndNode {
380             ##########################################################
381 0     0 0   my $nodeObj;
382 0           my $endNode = $Gparents[-1]; # content of the last element
383 0           my $si = $#Gparents; # index of the last element
384              
385 0 0         my $min = defined $Gparents[0] ? 0 : 1;
386 0           for ( my $i = $min; $i < $si; ++$i ) {
387 0 0         if ( defined $Gparents[$i] ) { # Only defined if there are kids
388             # Find parent of current parent:
389 0           my $node;
390 0           for ( my $j = $i + 1 ; ( !$node ) ; ++$j ) {
391 0 0         if ( $Gparents[$j] ) {
392 0           $node = $Gparents[$j];
393 0           $Gcounts[$j] += $Gcounts[$i];
394 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
395             }
396             }
397              
398 0           $nodeObj = "${Gparents[$i]} 0 obj<
399 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
400 0           $nodeObj .= "]/Count ${Gcounts[$i]}>>endobj\n";
401 0           $Gobject[ $Gparents[$i] ] = $Gpos;
402 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
403             }
404             }
405              
406             # Arrange and print the end node:
407 0           $nodeObj = "${endNode} 0 obj<
408 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$si] };
  0            
409 0           $nodeObj .= "]/Count ${Gcounts[$si]}/MediaBox [0 0 ${GoWid} ${GoHei}]>>endobj\n";
410 0           $Gobject[$endNode] = $Gpos;
411 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
412 0           return $endNode;
413             }
414              
415              
416             ##########################################################
417             sub calcRotateMatrix {
418             ##########################################################
419 0     0 0   my $str = "1 0 0 1 ${_[0]} ${_[1]} cm\n";
420 0           my $rotate = $_[2];
421              
422 0 0         return $str if ! $rotate;
423              
424 0           my $upperX = 0; my $upperY = 0;
  0            
425 0           my $radian = sprintf( "%.6f", $rotate / 57.2957795 ); # approx.
426 0           my $Cos = sprintf( "%.6f", cos($radian) );
427 0           my $Sin = sprintf( "%.6f", sin($radian) );
428 0           $str .= "${Cos} ${Sin} -${Sin} ${Cos} ${upperX} ${upperY} cm\n";
429             # return $str;
430             }
431              
432              
433             ##########################################################
434             sub getRootAndMapGobjects {
435             ##########################################################
436 0     0 0   my ( $xref, $tempRoot, $buf, $buf2 );
437              
438 0           sysseek $IN_FILE, -150, 2;
439 0           sysread $IN_FILE, $buf, 200;
440 0 0         die "[!] File '${GinFile}' is encrypted, cannot be used. Aborting.\n"
441             if $buf =~ m'Encrypt';
442              
443 0 0         if ($buf =~ m'/Prev\s+\d') { # "Versioned" PDF file (several xref sections)
    0          
444 0           while ($buf =~ m'/Prev\s+(\d+)') {
445 0           $xref = $1;
446 0           sysseek $IN_FILE, $xref, 0;
447 0           sysread $IN_FILE, $buf, 200;
448             # Reading 200 bytes may NOT be enough. Read on till we find 1st %%EOF:
449 0           until ($buf =~ m'%%EOF') {
450 0           sysread $IN_FILE, $buf2, 200;
451 0           $buf .= $buf2;
452             }
453             }
454             } elsif ( $buf =~ m'\bstartxref\s+(\d+)' ) { # Non-versioned PDF file
455 0           $xref = $1;
456             } else {
457 0           return 0;
458             }
459              
460 0 0         die "[!] Invalid XREF. Aborting.\n" if $xref > &getInputFileWeight;
461 0           populateGobjects($xref);
462 0           $tempRoot = &getRootFromTraditionalXrefSection;
463 0 0         return 0 unless $tempRoot; # No Root object in ${GinFile}, aborting
464 0           return $tempRoot;
465             }
466              
467              
468             ##########################################################
469             sub mapGobjectsFromTraditionalXref {
470             ##########################################################
471 0     0 0   my ( $idx, $qty, $readBytes );
472 0           sysseek $IN_FILE, $_[0], 0;
473 0           ($qty, $idx) = &extractXrefSection;
474 0           while ($qty) {
475 0           for (1 .. $qty) {
476 0           sysread $IN_FILE, $readBytes, 20;
477 0 0         $GObjects{$idx} = $1 if $readBytes =~ m'^\s?(\d{10}) \d{5} n';
478 0           ++$idx;
479             }
480 0           ($qty, $idx) = &extractXrefSection;
481             }
482 0           return;
483             }
484              
485              
486             ##########################################################
487             sub populateGobjects {
488             ##########################################################
489 0     0 0   my $xrefPos = $_[0];
490 0           my $readBytes;
491              
492 0           sysseek $IN_FILE, $xrefPos, 0;
493 0           sysread $IN_FILE, $readBytes, 22;
494              
495 0 0         if ($readBytes =~ /^(xref$cr)/) { # Input PDF is v1.4 or lower
    0          
496 0           mapGobjectsFromTraditionalXref($xrefPos + length($1));
497             } elsif ($readBytes =~ m'^\d+\s+\d+\s+obj') { # Input PDF is v1.5 or higher
498 0           die "[!] File '${GinFile}' uses xref streams (not a v1.4 PDF file).\n";
499             } else {
500 0           die "[!] File '${GinFile}' has a malformed xref table.\n";
501             }
502              
503 0           &addSizeToGObjects;
504 0           return;
505             }
506              
507              
508             ##########################################################
509             sub getRootFromTraditionalXrefSection {
510             ##########################################################
511 0     0 0   my $readBytes = " ";
512 0           my $buf;
513 0           while ($readBytes) {
514 0           sysread $IN_FILE, $readBytes, 200;
515 0           $buf .= $readBytes;
516 0 0         return $1 if $buf =~ m'\/Root\s+(\d+)\s+\d+\s+R';
517             }
518 0           return;
519             }
520              
521              
522             ##########################################################
523             sub getContentOfObjectNr {
524             ##########################################################
525 0     0 0   my $index = $_[0];
526              
527 0 0         return 0 if ! defined $GObjects{$index}; # A non-1.4 PDF
528 0           my ($offset, $size) = @{ $GObjects{$index} };
  0            
529 0           sysseek $IN_FILE, $offset, 0;
530 0           sysread $IN_FILE, my $buf, $size;
531 0           return $buf;
532             }
533              
534              
535             ##########################################################
536             sub writePageObjectsToOutputFile {
537             ##########################################################
538 0     0 0   my ($objectContent, $out_line, $part, $strPos, $old_one, $new_one);
539              
540 0           for (@Gto_be_created) {
541 0           $old_one = $_->[0];
542 0           $new_one = $_->[1];
543 0           $objectContent = getContentOfObjectNr($old_one);
544 0 0         if ( $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s ) {
545 0           $part = $2;
546 0           $strPos = length($1) + length($2) + length($3);
547 0           update_references_and_populate_to_be_created($part);
548 0           $out_line = "${new_one} 0 obj\n<<${part}>>stream";
549 0           $out_line .= substr( $objectContent, $strPos );
550             } else {
551 0 0         $objectContent = substr( $objectContent, length($1) )
552             if $objectContent =~ m'^(\d+ \d+ obj)\b';
553 0           update_references_and_populate_to_be_created($objectContent);
554 0           $out_line = "${new_one} 0 obj ${objectContent}";
555             }
556 0           $Gobject[$new_one] = $Gpos;
557 0           $Gpos += syswrite $OUT_FILE, $out_line;
558             }
559 0           undef @Gto_be_created;
560 0           return;
561             }
562              
563              
564             ##########################################################
565             sub setOutputPageDimensionAndSchema {
566             ##########################################################
567 0 0   0 0   die "[!] File '${GinFile}' is not a valid v1.4 PDF.\n"
568             unless &getPageSizeAndSetMediabox;
569              
570 0           my $surface = $GmediaBox[2] * $GmediaBox[3];
571 0           my $measuresInMm =
572             int($GmediaBox[2] / 72 * 25.4) . " x " . int($GmediaBox[3] / 72 * 25.4) . " mm";
573              
574 0           for ($surface) {
575 0 0         if (alike($_, $Gpaper{QuarterLetter})) {$GoWid = $DW; $GoHei = $DH; return "QT"};
  0            
  0            
  0            
576 0 0         if (alike($_, $Gpaper{A6})) {$GoWid = $AW; $GoHei = $AH; return "A6"};
  0            
  0            
  0            
577 0 0         if (alike($_, $Gpaper{HalfLetter})) {$GoWid = $DW; $GoHei = $DH; return "HT"};
  0            
  0            
  0            
578 0 0         if (alike($_, $Gpaper{QuarterLegal})) {$GoWid = $GW; $GoHei = $GH; return "QG"};
  0            
  0            
  0            
579 0 0         if (alike($_, $Gpaper{A5})) {$GoWid = $AW; $GoHei = $AH; return "A5"};
  0            
  0            
  0            
580 0 0         if (alike($_, $Gpaper{HalfLegal})) {$GoWid = $GW; $GoHei = $GH; return "HG"};
  0            
  0            
  0            
581 0 0         if (alike($_, $Gpaper{Letter})) {$GoWid = $KW; $GoHei = $KH; return "LT"};
  0            
  0            
  0            
582 0 0         if (alike($_, $Gpaper{A4})) {$GoWid = $JW; $GoHei = $JH; return "A4"};
  0            
  0            
  0            
583 0 0         if (alike($_, $Gpaper{Legal})) {return "USlegal, ${measuresInMm}"};
  0            
584 0 0         if (alike($_, $Gpaper{Tabloid})) {return "UStabloid, ${measuresInMm}"};
  0            
585 0 0         if (alike($_, $Gpaper{A3})) {return "A3, ${measuresInMm}"};
  0            
586             }
587 0           return "unknown, ${measuresInMm}";
588             }
589              
590              
591             ##########################################################
592             sub alike {
593             ##########################################################
594 0     0 0   my $num1 = $_[0]; my $num2 = $_[1];
  0            
595 0           my $tolerance = 1500;
596 0 0 0       return 0 if $num1 > $num2 + $tolerance or $num1 < $num2 - $tolerance;
597 0           return 1;
598             }
599              
600              
601             ##########################################################
602             sub getPage {
603             ##########################################################
604 0     0 0   my $pagenumber = $_[0];
605 0 0         die "[!] Page requested (${pagenumber}) does not exist. Aborting.\n"
606             if $pagenumber > &getInputPageCount;
607 0           my ($formRes, $formCont);
608              
609             # Find root:
610 0           my $objectContent = getContentOfObjectNr($GrootNr);
611              
612             # Find pages:
613 0 0         die "[!] Didn't find Pages section in '${GinFile}'. Aborting.\n"
614             unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R';
615 0           $objectContent = getContentOfObjectNr($1);
616 0           $objectContent = xformObjForThisPage($objectContent, $pagenumber);
617 0           ($formRes, $formCont) = parseAsResourcesAndContentRef($objectContent);
618             # return ($formRes, $formCont);
619             }
620              
621              
622             ##########################################################
623             sub writeRes {
624             ##########################################################
625 0     0 0   my ($formRes, $objNr) = ($_[0], $_[1]);
626              
627 0           my $objectContent = getContentOfObjectNr($objNr);
628 0           $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
629 0           my $strPos = length($1) + length($2) + length($3);
630 0           my $newPart = "<
631             . "/BBox [@{GmediaBox}] ${2}";
632              
633 0           ++$GobjNr;
634 0           $Gobject[$GobjNr] = $Gpos;
635 0           my $reference = $GobjNr;
636 0           update_references_and_populate_to_be_created($newPart);
637 0           my $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
638 0           $out_line .= substr( $objectContent, $strPos );
639 0           $Gpos += syswrite $OUT_FILE, $out_line;
640 0           return $reference;
641             }
642              
643              
644             ##########################################################
645             sub xformObjForThisPage {
646             ##########################################################
647 0     0 0   my ($objectContent, $pagenumber) = ($_[0], $_[1]);
648 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
649              
650 0 0         return 0 unless $objectContent =~ m'/Kids\s*\[([^\]]+)';
651 0           $vector = $1;
652              
653 0           $pageAccumulator = 0;
654              
655 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'g;
656 0           while ( $pageAccumulator < $pagenumber ) {
657 0           @pageObjBackup = @pageObj;
658 0           undef @pageObj;
659 0 0         last if ! @pageObjBackup; # $pagenumber is > than number of pages in PDF
660 0           for (@pageObjBackup) {
661 0           $objectContent = getContentOfObjectNr($_);
662 0 0         if ( $objectContent =~ m'/Count\s+(\d+)' ) {
663 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
664 0           $pageAccumulator += $1;
665             } else {
666 0 0         $vector = $1 if $objectContent =~ m'/Kids\s*\[([^\]]+)' ;
667 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'g;
668 0           last;
669             }
670             } else {
671 0           ++$pageAccumulator;
672             }
673 0 0         last if $pageAccumulator == $pagenumber;
674             }
675             }
676 0           return $objectContent;
677             }
678              
679              
680             ##########################################################
681             sub getPageSizeAndSetMediabox {
682             ##########################################################
683             # Find root:
684 0     0 0   my $objectContent = getContentOfObjectNr($GrootNr);
685              
686             # Find pages:
687 0 0         return 0 unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R';
688 0           $objectContent = getContentOfObjectNr($1);
689 0 0         $objectContent = xformObjForThisPage($objectContent, 1)
690             unless $objectContent =~ m'MediaBox';
691              
692             # Assume all input PDF pages have the same dimensions as first MediaBox found:
693 0 0         if (! @GmediaBox) {
694 0           for ($objectContent) {
695 0 0         if (m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
    0          
696 0           @GmediaBox = ($1, $2, $3, $4);
697             } elsif (m'MediaBox\s*(\d+)\s+\d+\s+R\b') { # Pagesize to be found in reference
698 0           my $ref = getContentOfObjectNr($1);
699 0 0         if ($ref =~ m'\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
700 0           @GmediaBox = ($1, $2, $3, $4)
701             } else {
702 0           return 0; # Meaning "failure"
703             }
704             } else {
705 0           return 0; # Meaning "failure"
706             }
707             }
708             }
709 0           return 1; # Meaning "success"
710             }
711              
712              
713             ##########################################################
714             sub parseAsResourcesAndContentRef {
715             ##########################################################
716 0     0 0   my $objContent = $_[0];
717 0           my ($resources, $formCont);
718              
719 0 0 0       $formCont = $1 if $objContent =~ m'/Contents\s+(\d+)'
720             or $objContent =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]';
721              
722 0           $resources = getResourcesFromObj($objContent);
723 0           return ($resources, $formCont);
724             }
725              
726              
727             ##########################################################
728             sub getResourcesFromObj {
729             ##########################################################
730 0     0 0   my $objContent = $_[0];
731 0           my $resources;
732              
733 0 0         return $1 if $objContent =~ m'Resources\s+(\d+\s+\d+\s+R)'; # Reference (95%)
734 0 0         if ( $objContent =~ m'^.+/Resources's ) {
735             # The resources are a dictionary. The whole is copied (morfologia.pdf):
736 0           my $k;
737 0           ( undef, $objContent ) = split /\/Resources/, $objContent;
738 0           $objContent =~ s/<
739 0           $objContent =~ s/>>/>>#/g;
740 0           for ( split /#/, $objContent ) {
741 0 0         if ( m'\S' ) {
742 0           $resources .= $_;
743 0 0         ++$k if m'<<';
744 0 0         --$k if m'>>';
745 0 0         last if $k == 0;
746             }
747             }
748             }
749 0           return $resources;
750             }
751              
752              
753             ##########################################################
754             sub getInputPageCount {
755             ##########################################################
756 0     0 0   state $maxPages;
757 0 0         return $maxPages if defined $maxPages;
758 0           my $objectContent;
759              
760 0 0         return 0 unless eval { $objectContent = getContentOfObjectNr($GrootNr); 1; };
  0            
  0            
761 0 0         if ( $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R' ) {
762 0           $objectContent = getContentOfObjectNr($1);
763 0 0         $maxPages = $1 if $objectContent =~ m'/Count\s+(\d+)';
764             }
765 0           return $maxPages;
766             }
767              
768              
769             ##########################################################
770             sub openInputFile {
771             ##########################################################
772 0     0 0   $GinFile = $_[0];
773 0           my ( $inputPageSize, $inputPageCount, $c );
774 0 0         die "[!] File '${GinFile}' is empty.\n" if ! &getInputFileWeight;
775              
776 0 0         open($IN_FILE, q{<}, $GinFile) or die "[!] Couldn't open '${GinFile}'.\n";
777 0           binmode $IN_FILE;
778              
779 0           sysread $IN_FILE, $c, 5;
780 0 0         die "[!] File '${GinFile}' is not a valid PDF file.\n" if $c ne "%PDF-";
781              
782             # Find root
783 0           $GrootNr = &getRootAndMapGobjects;
784 0 0         die "[!] File '${GinFile}' is not a valid v1.4 PDF file.\n" unless $GrootNr > 0;
785              
786 0           $inputPageSize = &setOutputPageDimensionAndSchema;
787 0           $inputPageCount = &getInputPageCount;
788              
789 0           return ($inputPageCount, $inputPageSize);
790             }
791              
792              
793             ##########################################################
794             sub getInputFileWeight {
795             ##########################################################
796 0     0 0   state $known;
797 0 0         return $known if $known;
798 0           $known = (stat($GinFile))[7];
799             }
800              
801              
802             ##########################################################
803             sub addSizeToGObjects {
804             ##########################################################
805 0     0 0   my $size = &getInputFileWeight;
806             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
807             # according to their offset in the file: last first
808 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
809 0           my $pos;
810              
811 0           for (@offset) {
812 0           $pos = $GObjects{$_};
813 0           $size -= $pos;
814 0           $GObjects{$_} = [ $pos, $size ];
815 0           $size = $pos;
816             }
817 0           return;
818             }
819              
820              
821             ##########################################################
822             sub update_references_and_populate_to_be_created {
823             ##########################################################
824 0     0 0   $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&xform . " 0 R"/eg;
  0            
825 0           return;
826             }
827              
828              
829             # xform translates an old object reference to a new one
830             # and populates a table with what objects must be created
831             ##########################################################
832             sub xform {
833             ##########################################################
834 0     0 0   state %known;
835 0 0         return $known{$1} if exists $known{$1};
836 0           push @Gto_be_created, [ $1, ++$GobjNr ];
837 0           $known{$1} = $GobjNr; # implicit return value (faster)
838             }
839              
840              
841             ##########################################################
842             sub extractXrefSection {
843             ##########################################################
844 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
845              
846 0           sysread $IN_FILE, $c, 1;
847 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s';
848 0           while ( $c =~ /[\d ]/ ) {
849 0           $readBytes .= $c;
850 0           sysread $IN_FILE, $c, 1;
851             }
852 0 0         ($qty, $idx) = ($2, $1) if $readBytes =~ m'^(\d+)\s+(\d+)';
853             # return ($qty, $idx);
854             }
855              
856              
857             ##########################################################
858             sub openOutputFile {
859             ##########################################################
860 0 0   0 0   &closeOutputFile if $Gpos;
861              
862 0           my $outputfile = $_[0];
863 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep it far from file beginning!
864              
865 0 0         open( $OUT_FILE, q{>}, $outputfile )
866             or die "[!] Couldn't open file '${outputfile}'.\n";
867 0           binmode $OUT_FILE;
868 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
869              
870 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
871 0           $Gparents[0] = 2;
872              
873 0           &setInitGrState;
874 0           return;
875             }
876              
877              
878             ##########################################################
879             sub closeInputFile {
880             ##########################################################
881 0     0 0   close $IN_FILE;
882             }
883              
884             1;
885              
886             __END__