File Coverage

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


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