File Coverage

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


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   66718 use v5.10;
  1         3  
4 1     1   6 use strict;
  1         2  
  1         5540  
5             # use warnings;
6             $^W = 0;
7             our $VERSION = "1.37";
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 ($objectContent, $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           $objectContent = getContentOfObjectNr($old_one);
545 0 0         if ( $objectContent =~ 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( $objectContent, $strPos );
551             } else {
552 0 0         $objectContent = substr( $objectContent, length($1) )
553             if $objectContent =~ m'^(\d+ \d+ obj)\b';
554 0           update_references_and_populate_to_be_created($objectContent);
555 0           $out_line = "${new_one} 0 obj ${objectContent}";
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             # Find root:
611 0           my $objectContent = getContentOfObjectNr($GrootNr);
612              
613             # Find pages:
614 0 0         die "[!] Didn't find Pages section in '${GinFile}'. Aborting.\n"
615             unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R';
616 0           $objectContent = getContentOfObjectNr($1);
617 0           $objectContent = xformObjForThisPage($objectContent, $pagenumber);
618 0           ($formRes, $formCont) = parseAsResourcesAndContentRef($objectContent);
619             # return ($formRes, $formCont);
620             }
621              
622              
623             ##########################################################
624             sub writeRes {
625             ##########################################################
626 0     0 0   my ($formRes, $objNr) = ($_[0], $_[1]);
627              
628 0           my $objectContent = getContentOfObjectNr($objNr);
629 0           $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
630 0           my $strPos = length($1) + length($2) + length($3);
631 0           my $newPart = "<
632             . "/BBox [@{GmediaBox}] ${2}";
633              
634 0           ++$GobjNr;
635 0           $Gobject[$GobjNr] = $Gpos;
636 0           my $reference = $GobjNr;
637 0           update_references_and_populate_to_be_created($newPart);
638 0           my $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
639 0           $out_line .= substr( $objectContent, $strPos );
640 0           $Gpos += syswrite $OUT_FILE, $out_line;
641 0           return $reference;
642             }
643              
644              
645             ##########################################################
646             sub xformObjForThisPage {
647             ##########################################################
648 0     0 0   my ($objectContent, $pagenumber) = ($_[0], $_[1]);
649 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
650              
651 0 0         return 0 unless $objectContent =~ m'/Kids\s*\[([^\]]+)';
652 0           $vector = $1;
653              
654 0           $pageAccumulator = 0;
655              
656 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'g;
657 0           while ( $pageAccumulator < $pagenumber ) {
658 0           @pageObjBackup = @pageObj;
659 0           undef @pageObj;
660 0 0         last if ! @pageObjBackup; # $pagenumber is > than number of pages in PDF
661 0           for (@pageObjBackup) {
662 0           $objectContent = getContentOfObjectNr($_);
663 0 0         if ( $objectContent =~ m'/Count\s+(\d+)' ) {
664 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
665 0           $pageAccumulator += $1;
666             } else {
667 0 0         $vector = $1 if $objectContent =~ m'/Kids\s*\[([^\]]+)' ;
668 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'g;
669 0           last;
670             }
671             } else {
672 0           ++$pageAccumulator;
673             }
674 0 0         last if $pageAccumulator == $pagenumber;
675             }
676             }
677 0           return $objectContent;
678             }
679              
680              
681             ##########################################################
682             sub getPageSizeAndSetMediabox {
683             ##########################################################
684             # Find root:
685 0     0 0   my $objectContent = getContentOfObjectNr($GrootNr);
686              
687             # Find pages:
688 0 0         return 0 unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R';
689 0           $objectContent = getContentOfObjectNr($1);
690 0 0         $objectContent = xformObjForThisPage($objectContent, 1)
691             unless $objectContent =~ m'MediaBox';
692              
693             # Assume all input PDF pages have the same dimensions as first MediaBox found:
694 0 0         if (! @GmediaBox) {
695 0           for ($objectContent) {
696 0 0         if (m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
    0          
697 0           @GmediaBox = ($1, $2, $3, $4);
698             } elsif (m'MediaBox\s*(\d+)\s+\d+\s+R\b') { # Pagesize to be found in reference
699 0           my $ref = getContentOfObjectNr($1);
700 0 0         if ($ref =~ m'\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
701 0           @GmediaBox = ($1, $2, $3, $4)
702             } else {
703 0           return 0; # Meaning "failure"
704             }
705             } else {
706 0           return 0; # Meaning "failure"
707             }
708             }
709             }
710 0           return 1; # Meaning "success"
711             }
712              
713              
714             ##########################################################
715             sub parseAsResourcesAndContentRef {
716             ##########################################################
717 0     0 0   my $objContent = $_[0];
718 0           my ($resources, $formCont);
719              
720 0 0 0       $formCont = $1 if $objContent =~ m'/Contents\s+(\d+)'
721             or $objContent =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]';
722              
723 0           $resources = getResourcesFromObj($objContent);
724 0           return ($resources, $formCont);
725             }
726              
727              
728             ##########################################################
729             sub getResourcesFromObj {
730             ##########################################################
731 0     0 0   my $objContent = $_[0];
732 0           my $resources;
733              
734 0 0         return $1 if $objContent =~ m'Resources\s+(\d+\s+\d+\s+R)'; # Reference (95%)
735 0 0         if ( $objContent =~ m'^.+/Resources's ) {
736             # The resources are a dictionary. The whole is copied (morfologia.pdf):
737 0           my $k;
738 0           ( undef, $objContent ) = split /\/Resources/, $objContent;
739 0           $objContent =~ s/<
740 0           $objContent =~ s/>>/>>#/g;
741 0           for ( split /#/, $objContent ) {
742 0 0         if ( m'\S' ) {
743 0           $resources .= $_;
744 0 0         ++$k if m'<<';
745 0 0         --$k if m'>>';
746 0 0         last if $k == 0;
747             }
748             }
749             }
750 0           return $resources;
751             }
752              
753              
754             ##########################################################
755             sub getInputPageCount {
756             ##########################################################
757 0     0 0   state $maxPages;
758 0 0         return $maxPages if defined $maxPages;
759 0           my $objectContent;
760              
761 0 0         return 0 unless eval { $objectContent = getContentOfObjectNr($GrootNr); 1; };
  0            
  0            
762 0 0         if ( $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R' ) {
763 0           $objectContent = getContentOfObjectNr($1);
764 0 0         $maxPages = $1 if $objectContent =~ m'/Count\s+(\d+)';
765             }
766 0           return $maxPages;
767             }
768              
769              
770             ##########################################################
771             sub openInputFile {
772             ##########################################################
773 0     0 0   $GinFile = $_[0];
774 0           my ( $inputPageSize, $inputPageCount, $c );
775 0 0         die "[!] File '${GinFile}' is empty.\n" if ! &getInputFileWeight;
776              
777 0 0         open($IN_FILE, q{<}, $GinFile) or die "[!] Couldn't open '${GinFile}'.\n";
778 0           binmode $IN_FILE;
779              
780 0           sysread $IN_FILE, $c, 5;
781 0 0         die "[!] File '${GinFile}' is not a valid PDF file.\n" if $c ne "%PDF-";
782              
783             # Find root
784 0           $GrootNr = &getRootAndMapGobjects;
785 0 0         die "[!] File '${GinFile}' is not a valid v1.4 PDF file.\n" unless $GrootNr > 0;
786              
787 0           $inputPageSize = &setOutputPageDimensionAndSchema;
788 0           $inputPageCount = &getInputPageCount;
789              
790 0           return ($inputPageCount, $inputPageSize);
791             }
792              
793              
794             ##########################################################
795             sub getInputFileWeight {
796             ##########################################################
797 0     0 0   state $known;
798 0 0         return $known if $known;
799 0           $known = (stat($GinFile))[7];
800             }
801              
802              
803             ##########################################################
804             sub addSizeToGObjects {
805             ##########################################################
806 0     0 0   my $size = &getInputFileWeight;
807             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
808             # according to their offset in the file: last first
809 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
810 0           my $pos;
811              
812 0           for (@offset) {
813 0           $pos = $GObjects{$_};
814 0           $size -= $pos;
815 0           $GObjects{$_} = [ $pos, $size ];
816 0           $size = $pos;
817             }
818 0           return;
819             }
820              
821              
822             ##########################################################
823             sub update_references_and_populate_to_be_created {
824             ##########################################################
825 0     0 0   $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&xform . " 0 R"/eg;
  0            
826 0           return;
827             }
828              
829              
830             # xform translates an old object reference to a new one
831             # and populates a table with what objects must be created
832             ##########################################################
833             sub xform {
834             ##########################################################
835 0     0 0   state %known;
836 0 0         return $known{$1} if exists $known{$1};
837 0           push @Gto_be_created, [ $1, ++$GobjNr ];
838 0           $known{$1} = $GobjNr; # implicit return value (faster)
839             }
840              
841              
842             ##########################################################
843             sub extractXrefSection {
844             ##########################################################
845 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
846              
847 0           sysread $IN_FILE, $c, 1;
848 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s';
849 0           while ( $c =~ /[\d ]/ ) {
850 0           $readBytes .= $c;
851 0           sysread $IN_FILE, $c, 1;
852             }
853 0 0         ($qty, $idx) = ($2, $1) if $readBytes =~ m'^(\d+)\s+(\d+)';
854             # return ($qty, $idx);
855             }
856              
857              
858             ##########################################################
859             sub openOutputFile {
860             ##########################################################
861 0 0   0 0   &closeOutputFile if $Gpos;
862              
863 0           my $outputfile = $_[0];
864 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep it far from file beginning!
865              
866 0 0         open( $OUT_FILE, q{>}, $outputfile )
867             or die "[!] Couldn't open file '${outputfile}'.\n";
868 0           binmode $OUT_FILE;
869 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
870              
871 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
872 0           $Gparents[0] = 2;
873              
874 0           &setInitGrState;
875 0           return;
876             }
877              
878              
879             ##########################################################
880             sub closeInputFile {
881             ##########################################################
882 0     0 0   close $IN_FILE;
883             }
884              
885             1;
886              
887             __END__