File Coverage

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


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   67383 use v5.10;
  1         4  
4 1     1   6 use strict;
  1         2  
  1         6091  
5             # use warnings;
6             $^W = 0;
7             our $VERSION = "1.38";
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           my $negSin = $Sin * -1;
429 0           $str .= "${Cos} ${Sin} ${negSin} ${Cos} ${upperX} ${upperY} cm\n";
430             # return $str;
431             }
432              
433              
434             ##########################################################
435             sub getRootAndMapGobjects {
436             ##########################################################
437 0     0 0   my ( $xref, $tempRoot, $buf, $buf2 );
438              
439 0           sysseek $IN_FILE, -150, 2;
440 0           sysread $IN_FILE, $buf, 200;
441 0 0         die "[!] File '${GinFile}' is encrypted, cannot be used. Aborting.\n"
442             if $buf =~ m'Encrypt';
443              
444 0 0         if ($buf =~ m'/Prev\s+\d') { # "Versioned" PDF file (several xref sections)
    0          
445 0           while ($buf =~ m'/Prev\s+(\d+)') {
446 0           $xref = $1;
447 0           sysseek $IN_FILE, $xref, 0;
448 0           sysread $IN_FILE, $buf, 200;
449             # Reading 200 bytes may NOT be enough. Read on till we find 1st %%EOF:
450 0           until ($buf =~ m'%%EOF') {
451 0           sysread $IN_FILE, $buf2, 200;
452 0           $buf .= $buf2;
453             }
454             }
455             } elsif ( $buf =~ m'\bstartxref\s+(\d+)' ) { # Non-versioned PDF file
456 0           $xref = $1;
457             } else {
458 0           return 0;
459             }
460              
461 0 0         die "[!] Invalid XREF. Aborting.\n" if $xref > &getInputFileWeight;
462 0           populateGobjects($xref);
463 0           $tempRoot = &getRootFromTraditionalXrefSection;
464 0 0         return 0 unless $tempRoot; # No Root object in ${GinFile}, aborting
465 0           return $tempRoot;
466             }
467              
468              
469             ##########################################################
470             sub mapGobjectsFromTraditionalXref {
471             ##########################################################
472 0     0 0   my ( $idx, $qty, $readBytes );
473 0           sysseek $IN_FILE, $_[0], 0;
474 0           ($qty, $idx) = &extractXrefSection;
475 0           while ($qty) {
476 0           for (1 .. $qty) {
477 0           sysread $IN_FILE, $readBytes, 20;
478 0 0         $GObjects{$idx} = $1 if $readBytes =~ m'^\s?(\d{10}) \d{5} n';
479 0           ++$idx;
480             }
481 0           ($qty, $idx) = &extractXrefSection;
482             }
483 0           return;
484             }
485              
486              
487             ##########################################################
488             sub populateGobjects {
489             ##########################################################
490 0     0 0   my $xrefPos = $_[0];
491 0           my $readBytes;
492              
493 0           sysseek $IN_FILE, $xrefPos, 0;
494 0           sysread $IN_FILE, $readBytes, 22;
495              
496 0 0         if ($readBytes =~ /^(xref$cr)/) { # Input PDF is v1.4 or lower
    0          
497 0           mapGobjectsFromTraditionalXref($xrefPos + length($1));
498             } elsif ($readBytes =~ m'^\d+\s+\d+\s+obj') { # Input PDF is v1.5 or higher
499 0           die "[!] File '${GinFile}' uses xref streams (not a v1.4 PDF file).\n";
500             } else {
501 0           die "[!] File '${GinFile}' has a malformed xref table.\n";
502             }
503              
504 0           &addSizeToGObjects;
505 0           return;
506             }
507              
508              
509             ##########################################################
510             sub getRootFromTraditionalXrefSection {
511             ##########################################################
512 0     0 0   my $readBytes = " ";
513 0           my $buf;
514 0           while ($readBytes) {
515 0           sysread $IN_FILE, $readBytes, 200;
516 0           $buf .= $readBytes;
517 0 0         return $1 if $buf =~ m'\/Root\s+(\d+)\s+\d+\s+R';
518             }
519 0           return;
520             }
521              
522              
523             ##########################################################
524             sub getContentOfObjectNr {
525             ##########################################################
526 0     0 0   my $index = $_[0];
527              
528 0 0         return 0 if ! defined $GObjects{$index}; # A non-1.4 PDF
529 0           my ($offset, $size) = @{ $GObjects{$index} };
  0            
530 0           sysseek $IN_FILE, $offset, 0;
531 0           sysread $IN_FILE, my $buf, $size;
532 0           return $buf;
533             }
534              
535              
536             ##########################################################
537             sub writePageObjectsToOutputFile {
538             ##########################################################
539 0     0 0   my ($ObjContent, $out_line, $part, $strPos, $old_one, $new_one);
540              
541 0           for (@Gto_be_created) {
542 0           $old_one = $_->[0];
543 0           $new_one = $_->[1];
544 0           $ObjContent = getContentOfObjectNr($old_one);
545 0 0         if ( $ObjContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s ) {
546 0           $part = $2;
547 0           $strPos = length($1) + length($2) + length($3);
548 0           update_references_and_populate_to_be_created($part);
549 0           $out_line = "${new_one} 0 obj\n<<${part}>>stream";
550 0           $out_line .= substr( $ObjContent, $strPos );
551             } else {
552 0 0         $ObjContent = substr( $ObjContent, length($1) )
553             if $ObjContent =~ m'^(\d+ \d+ obj)\b';
554 0           update_references_and_populate_to_be_created($ObjContent);
555 0           $out_line = "${new_one} 0 obj ${ObjContent}";
556             }
557 0           $Gobject[$new_one] = $Gpos;
558 0           $Gpos += syswrite $OUT_FILE, $out_line;
559             }
560 0           undef @Gto_be_created;
561 0           return;
562             }
563              
564              
565             ##########################################################
566             sub setOutputPageDimensionAndSchema {
567             ##########################################################
568 0 0   0 0   die "[!] File '${GinFile}' is not a valid v1.4 PDF.\n"
569             unless &getPageSizeAndSetMediabox;
570              
571 0           my $surface = $GmediaBox[2] * $GmediaBox[3];
572 0           my $measuresInMm =
573             int($GmediaBox[2] / 72 * 25.4) . " x " . int($GmediaBox[3] / 72 * 25.4) . " mm";
574              
575 0           for ($surface) {
576 0 0         if (alike($_, $Gpaper{QuarterLetter})) {$GoWid = $DW; $GoHei = $DH; return "QT"};
  0            
  0            
  0            
577 0 0         if (alike($_, $Gpaper{A6})) {$GoWid = $AW; $GoHei = $AH; return "A6"};
  0            
  0            
  0            
578 0 0         if (alike($_, $Gpaper{HalfLetter})) {$GoWid = $DW; $GoHei = $DH; return "HT"};
  0            
  0            
  0            
579 0 0         if (alike($_, $Gpaper{QuarterLegal})) {$GoWid = $GW; $GoHei = $GH; return "QG"};
  0            
  0            
  0            
580 0 0         if (alike($_, $Gpaper{A5})) {$GoWid = $AW; $GoHei = $AH; return "A5"};
  0            
  0            
  0            
581 0 0         if (alike($_, $Gpaper{HalfLegal})) {$GoWid = $GW; $GoHei = $GH; return "HG"};
  0            
  0            
  0            
582 0 0         if (alike($_, $Gpaper{Letter})) {$GoWid = $KW; $GoHei = $KH; return "LT"};
  0            
  0            
  0            
583 0 0         if (alike($_, $Gpaper{A4})) {$GoWid = $JW; $GoHei = $JH; return "A4"};
  0            
  0            
  0            
584 0 0         if (alike($_, $Gpaper{Legal})) {return "USlegal, ${measuresInMm}"};
  0            
585 0 0         if (alike($_, $Gpaper{Tabloid})) {return "UStabloid, ${measuresInMm}"};
  0            
586 0 0         if (alike($_, $Gpaper{A3})) {return "A3, ${measuresInMm}"};
  0            
587             }
588 0           return "unknown, ${measuresInMm}";
589             }
590              
591              
592             ##########################################################
593             sub alike {
594             ##########################################################
595 0     0 0   my $num1 = $_[0]; my $num2 = $_[1];
  0            
596 0           my $tolerance = 1500;
597 0 0 0       return 0 if $num1 > $num2 + $tolerance or $num1 < $num2 - $tolerance;
598 0           return 1;
599             }
600              
601              
602             ##########################################################
603             sub getPage {
604             ##########################################################
605 0     0 0   my $pagenumber = $_[0];
606 0 0         die "[!] Page requested (${pagenumber}) does not exist. Aborting.\n"
607             if $pagenumber > &getInputPageCount;
608 0           my ($formRes, $formCont);
609              
610 0           state $rootObjContent;
611 0           state $pagesObjContent;
612 0 0         if ($rootObjContent == '') {
613             # Find root:
614 0           $rootObjContent = getContentOfObjectNr($GrootNr);
615             # Find "Pages" section in root object:
616 0 0         die "[!] Didn't find Pages section in '${GinFile}'. Aborting.\n"
617             unless $rootObjContent =~ m'/Pages\s+(\d+)\s+\d+\s+R';
618 0           $pagesObjContent = getContentOfObjectNr($1);
619             }
620 0           my $thisPageObjContent = xformObjForThisPage($pagesObjContent, $pagenumber);
621 0           ($formRes, $formCont) = parseAsResourcesAndContentRef($thisPageObjContent);
622             # return ($formRes, $formCont);
623             }
624              
625              
626             ##########################################################
627             sub writeRes {
628             ##########################################################
629 0     0 0   my ($formRes, $objNr) = ($_[0], $_[1]);
630              
631 0           my $ObjContent = getContentOfObjectNr($objNr);
632 0           $ObjContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
633 0           my $strPos = length($1) + length($2) + length($3);
634 0           my $newPart = "<
635             . "/BBox [@{GmediaBox}] ${2}";
636              
637 0           ++$GobjNr;
638 0           $Gobject[$GobjNr] = $Gpos;
639 0           my $reference = $GobjNr;
640 0           update_references_and_populate_to_be_created($newPart);
641 0           my $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
642 0           $out_line .= substr( $ObjContent, $strPos );
643 0           $Gpos += syswrite $OUT_FILE, $out_line;
644 0           return $reference;
645             }
646              
647              
648             ##########################################################
649             sub xformObjForThisPage {
650             ##########################################################
651 0     0 0   my ($ObjContent, $pagenumber) = ($_[0], $_[1]);
652 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
653              
654 0 0         return 0 unless $ObjContent =~ m'/Kids\s*\[([^\]]+)';
655 0           $vector = $1;
656              
657 0           $pageAccumulator = 0;
658              
659 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'g;
660 0           while ( $pageAccumulator < $pagenumber ) {
661 0           @pageObjBackup = @pageObj;
662 0           undef @pageObj;
663 0 0         last if ! @pageObjBackup; # $pagenumber is > than number of pages in PDF
664 0           for (@pageObjBackup) {
665 0           $ObjContent = getContentOfObjectNr($_);
666 0 0         if ( $ObjContent =~ m'/Count\s+(\d+)' ) {
667 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
668 0           $pageAccumulator += $1;
669             } else {
670 0 0         $vector = $1 if $ObjContent =~ m'/Kids\s*\[([^\]]+)' ;
671 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'g;
672 0           last;
673             }
674             } else {
675 0           ++$pageAccumulator;
676             }
677 0 0         last if $pageAccumulator == $pagenumber;
678             }
679             }
680 0           return $ObjContent;
681             }
682              
683              
684             ##########################################################
685             sub getPageSizeAndSetMediabox {
686             ##########################################################
687             # Find root:
688 0     0 0   my $ObjContent = getContentOfObjectNr($GrootNr);
689              
690             # Find pages:
691 0 0         return 0 unless $ObjContent =~ m'/Pages\s+(\d+)\s+\d+\s+R';
692 0           $ObjContent = getContentOfObjectNr($1);
693 0 0         $ObjContent = xformObjForThisPage($ObjContent, 1)
694             unless $ObjContent =~ m'MediaBox';
695              
696             # Assume all input PDF pages have the same dimensions as first MediaBox found:
697 0 0         if (! @GmediaBox) {
698 0           for ($ObjContent) {
699 0 0         if (m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
    0          
700 0           @GmediaBox = ($1, $2, $3, $4);
701             } elsif (m'MediaBox\s*(\d+)\s+\d+\s+R\b') { # Pagesize to be found in reference
702 0           my $ref = getContentOfObjectNr($1);
703 0 0         if ($ref =~ m'\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
704 0           @GmediaBox = ($1, $2, $3, $4)
705             } else {
706 0           return 0; # Meaning "failure"
707             }
708             } else {
709 0           return 0; # Meaning "failure"
710             }
711             }
712             }
713 0           return 1; # Meaning "success"
714             }
715              
716              
717             ##########################################################
718             sub parseAsResourcesAndContentRef {
719             ##########################################################
720 0     0 0   my $objContent = $_[0];
721 0           my ($resources, $formCont);
722              
723 0 0 0       $formCont = $1 if $objContent =~ m'/Contents\s+(\d+)'
724             or $objContent =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]';
725              
726 0           $resources = getResourcesFromObj($objContent);
727 0           return ($resources, $formCont);
728             }
729              
730              
731             ##########################################################
732             sub getResourcesFromObj {
733             ##########################################################
734 0     0 0   my $objContent = $_[0];
735 0           my $resources;
736              
737 0 0         return $1 if $objContent =~ m'Resources\s+(\d+\s+\d+\s+R)'; # Reference (95%)
738 0 0         if ( $objContent =~ m'^.+/Resources's ) {
739             # The resources are a dictionary. The whole is copied (morfologia.pdf):
740 0           my $k;
741 0           ( undef, $objContent ) = split /\/Resources/, $objContent;
742 0           $objContent =~ s/<
743 0           $objContent =~ s/>>/>>#/g;
744 0           for ( split /#/, $objContent ) {
745 0 0         if ( m'\S' ) {
746 0           $resources .= $_;
747 0 0         ++$k if m'<<';
748 0 0         --$k if m'>>';
749 0 0         last if $k == 0;
750             }
751             }
752             }
753 0           return $resources;
754             }
755              
756              
757             ##########################################################
758             sub getInputPageCount {
759             ##########################################################
760 0     0 0   state $maxPages;
761 0 0         return $maxPages if defined $maxPages;
762 0           my $ObjContent;
763              
764 0 0         return 0 unless eval { $ObjContent = getContentOfObjectNr($GrootNr); 1; };
  0            
  0            
765 0 0         if ( $ObjContent =~ m'/Pages\s+(\d+)\s+\d+\s+R' ) {
766 0           $ObjContent = getContentOfObjectNr($1);
767 0 0         $maxPages = $1 if $ObjContent =~ m'/Count\s+(\d+)';
768             }
769 0           return $maxPages;
770             }
771              
772              
773             ##########################################################
774             sub openInputFile {
775             ##########################################################
776 0     0 0   $GinFile = $_[0];
777 0           my ( $inputPageSize, $inputPageCount, $c );
778 0 0         die "[!] File '${GinFile}' is empty.\n" if ! &getInputFileWeight;
779              
780 0 0         open($IN_FILE, q{<}, $GinFile) or die "[!] Couldn't open '${GinFile}'.\n";
781 0           binmode $IN_FILE;
782              
783 0           sysread $IN_FILE, $c, 5;
784 0 0         die "[!] File '${GinFile}' is not a valid PDF file.\n" if $c ne "%PDF-";
785              
786             # Find root
787 0           $GrootNr = &getRootAndMapGobjects;
788 0 0         die "[!] File '${GinFile}' is not a valid v1.4 PDF file.\n" unless $GrootNr > 0;
789              
790 0           $inputPageSize = &setOutputPageDimensionAndSchema;
791 0           $inputPageCount = &getInputPageCount;
792              
793 0           return ($inputPageCount, $inputPageSize);
794             }
795              
796              
797             ##########################################################
798             sub getInputFileWeight {
799             ##########################################################
800 0     0 0   state $known;
801 0 0         return $known if $known;
802 0           $known = (stat($GinFile))[7];
803             }
804              
805              
806             ##########################################################
807             sub addSizeToGObjects {
808             ##########################################################
809 0     0 0   my $size = &getInputFileWeight;
810             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
811             # according to their offset in the file: last first
812 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
813 0           my $pos;
814              
815 0           for (@offset) {
816 0           $pos = $GObjects{$_};
817 0           $size -= $pos;
818 0           $GObjects{$_} = [ $pos, $size ];
819 0           $size = $pos;
820             }
821 0           return;
822             }
823              
824              
825             ##########################################################
826             sub update_references_and_populate_to_be_created {
827             ##########################################################
828 0     0 0   $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&xform . " 0 R"/eg;
  0            
829 0           return;
830             }
831              
832              
833             # xform translates an old object reference to a new one
834             # and populates a table with what objects must be created
835             ##########################################################
836             sub xform {
837             ##########################################################
838 0     0 0   state %known;
839 0 0         return $known{$1} if exists $known{$1};
840 0           push @Gto_be_created, [ $1, ++$GobjNr ];
841 0           $known{$1} = $GobjNr; # implicit return value (faster)
842             }
843              
844              
845             ##########################################################
846             sub extractXrefSection {
847             ##########################################################
848 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
849              
850 0           sysread $IN_FILE, $c, 1;
851 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s';
852 0           while ( $c =~ /[\d ]/ ) {
853 0           $readBytes .= $c;
854 0           sysread $IN_FILE, $c, 1;
855             }
856 0 0         ($qty, $idx) = ($2, $1) if $readBytes =~ m'^(\d+)\s+(\d+)';
857             # return ($qty, $idx);
858             }
859              
860              
861             ##########################################################
862             sub openOutputFile {
863             ##########################################################
864 0 0   0 0   &closeOutputFile if $Gpos;
865              
866 0           my $outputfile = $_[0];
867 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep it far from file beginning!
868              
869 0 0         open( $OUT_FILE, q{>}, $outputfile )
870             or die "[!] Couldn't open file '${outputfile}'.\n";
871 0           binmode $OUT_FILE;
872 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
873              
874 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
875 0           $Gparents[0] = 2;
876              
877 0           &setInitGrState;
878 0           return;
879             }
880              
881              
882             ##########################################################
883             sub closeInputFile {
884             ##########################################################
885 0     0 0   close $IN_FILE;
886             }
887              
888             1;
889              
890             __END__