File Coverage

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


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