File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 481 1.0
branch 0 202 0.0
condition 0 12 0.0
subroutine 2 39 5.1
pod 0 37 0.0
total 7 771 0.9


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   68496 use v5.10;
  1         4  
4 1     1   6 use strict;
  1         1  
  1         6143  
5             # use warnings;
6             $^W = 0;
7             our $VERSION = "1.39";
8              
9             my ($GinFile, $GpageObjNr, $GrootNr, $Gpos, $GobjNr, $Gstream, $GoWid, $GoHei,
10             $GpagesObjContent);
11             my (@Gkids, @Gcounts, @GmediaBox, @Gobject, @Gparents, @Gto_be_created);
12             my (%GpageXObject, %GObjects, %Gpaper);
13              
14             my $cr = '\s*(?:\015|\012|(?:\015\012))';
15             my ( $IN_FILE, $OUT_FILE );
16              
17             # ISO 216 paper sizes in pt (four decimals will do):
18             my $JH = 1190.5512; # [J] A3 ~ 420 mm (H)
19             my $JW = 841.8898; # [J] A3 ~ 297 mm (W)
20             my $AH = $JW; # [A] A4 ~ 297 mm (H)
21             my $AW = 595.2756; # [A] A4 ~ 210 mm (W)
22             my $BH = $AW; # [B] A5 ~ 210 mm (H)
23             my $BW = 419.5276; # [B] A5 ~ 148 mm (W)
24             my $CH = $BW; # [C] A6 ~ 148 mm (H)
25             my $CW = 297.6378; # [C] A6 ~ 105 mm (W)
26             # + 1 mm (2.8346 pt) to account for rounding in ISO 216 (148+148=296):
27             my $CX = 422.3622; # [C] A6 $CH + 1 mm (H)
28             my $BX = $CX; # [B] A5 $BW + 1 mm (W)
29              
30             # US paper sizes in pt:
31             my $DH = 792; # [D] US Letter Full (H)
32             my $DW = 612; # [D] US Letter Full (W)
33             my $EH = $DW; # [E] US Letter Half (H)
34             my $EW = 396; # [E] US Letter Half (W)
35             my $FH = $EW; # [F] US Letter Quarter (H)
36             my $FW = 306; # [F] US Letter Quarter (W)
37             my $GH = 1008; # [G] US Legal Full (H)
38             my $GW = $DW; # [G] US Legal Full (W)
39             my $HH = $DW; # [H] US Legal Half (H)
40             my $HW = 504; # [H] US Legal Half (W)
41             my $IH = $HW; # [I] US Legal Quarter (H)
42             my $IW = $FW; # [I] US Legal Quarter (W)
43             my $KH = 1224; # [K] US Tabloid (H)
44             my $KW = $DH; # [K] US Tabloid (W)
45              
46             # Paper surfaces in square pts (expressed as HxW in pts):
47             %Gpaper = (
48             QuarterLetter => $FH * $FW, # = 121_176
49             A6 => $CH * $CW, # ~ 124_867
50             QuarterLegal => $IH * $IW, # = 154_224
51             HalfLetter => $EH * $EW, # = 242_352
52             A5 => $BH * $BW, # ~ 249_735
53             HalfLegal => $HH * $HW, # = 308_448
54             Letter => $DH * $DW, # = 484_704
55             A4 => $AH * $AW, # ~ 501_156
56             Legal => $GH * $GW, # = 616_896
57             Tabloid => $KH * $KW, # = 969_408
58             A3 => $JH * $JW, # ~ 1_002_312
59             );
60              
61             ##########################################################
62             sub main {
63             ##########################################################
64 0     0 0   my $input = $ARGV[0];
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 "[!] File '${GinFile}' has bad page size (${_}). Try 'paperback -h' for more info.\n"}
161             }
162              
163 0           my ($name) = $input =~ /(.+)\.[^.]+$/;
164 0           createOutputFile("${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 createOutputFile 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 createOutputFile 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             # 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   $Gparents[0] = ++$GobjNr if ! $Gparents[0];
310              
311 0           my $parent = $Gparents[0];
312 0           my $resourceObjectNr = writePageResourceDict(&createPageResourceDict);
313 0           &writePageStream;
314 0           writePageResources($parent, $resourceObjectNr);
315 0           ++$Gcounts[0];
316 0 0         writePageNodes(8) if $Gcounts[0] > 9;
317 0           return;
318             }
319              
320              
321             ##########################################################
322             sub closeOutputFile {
323             ##########################################################
324 0 0   0 0   return if !$Gpos;
325              
326 0 0         &writePage if $Gstream;
327 0           my $endNode = &writeEndNode;
328              
329 0           my $out_line = "1 0 obj<>endobj\n";
330 0           $Gobject[1] = $Gpos;
331 0           $Gpos += syswrite $OUT_FILE, $out_line;
332 0           my $qty = $#Gobject;
333 0           my $startxref = $Gpos;
334 0           my $xrefQty = $qty + 1;
335 0           $out_line = "xref\n0 ${xrefQty}\n0000000000 65535 f \n";
336 0           $out_line .= sprintf "%.10d 00000 n \n", $_ for @Gobject[1 .. $qty];
337 0           $out_line .= "trailer\n<<\n/Size ${xrefQty}\n/Root 1 0 R\n"
338             . ">>\nstartxref\n${startxref}\n%%EOF\n";
339              
340 0           syswrite $OUT_FILE, $out_line;
341 0           close $OUT_FILE;
342              
343 0           $Gpos = 0;
344 0           return;
345             }
346              
347              
348             ##########################################################
349             sub writePageNodes {
350             ##########################################################
351 0     0 0   my $qtyChildren = $_[0];
352 0           my $i = 0;
353 0           my $j = 1;
354 0           my $nodeObj;
355              
356 0           while ( $qtyChildren < $#{ $Gkids[$i] } ) {
  0            
357             # Imprimir padre actual y pasar al siguiente nivel:
358 0 0         $Gparents[$j] = ++$GobjNr if ! $Gparents[$j] ;
359              
360 0           $nodeObj =
361             "${Gparents[$i]} 0 obj<
362 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
363 0           $nodeObj .= "]\n/Count ${Gcounts[$i]}>>endobj\n";
364 0           $Gobject[ $Gparents[$i] ] = $Gpos;
365 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
366              
367 0           $Gcounts[$j] += $Gcounts[$i];
368 0           $Gcounts[$i] = 0;
369 0           $Gkids[$i] = [];
370 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
371 0           undef $Gparents[$i];
372 0           ++$i;
373 0           ++$j;
374             }
375 0           return;
376             }
377              
378              
379             ##########################################################
380             sub writeEndNode {
381             ##########################################################
382 0     0 0   my $nodeObj;
383 0           my $endNode = $Gparents[-1]; # content of the last element
384 0           my $si = $#Gparents; # index of the last element
385              
386 0 0         my $min = defined $Gparents[0] ? 0 : 1;
387 0           for ( my $i = $min; $i < $si; ++$i ) {
388 0 0         if ( defined $Gparents[$i] ) { # Only defined if there are kids
389             # Find parent of current parent:
390 0           my $node;
391 0           for ( my $j = $i + 1 ; ( !$node ) ; ++$j ) {
392 0 0         if ( $Gparents[$j] ) {
393 0           $node = $Gparents[$j];
394 0           $Gcounts[$j] += $Gcounts[$i];
395 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
396             }
397             }
398              
399 0           $nodeObj = "${Gparents[$i]} 0 obj<
400 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
401 0           $nodeObj .= "]/Count ${Gcounts[$i]}>>endobj\n";
402 0           $Gobject[ $Gparents[$i] ] = $Gpos;
403 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
404             }
405             }
406              
407             # Arrange and print the end node:
408 0           $nodeObj = "${endNode} 0 obj<
409 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$si] };
  0            
410 0           $nodeObj .= "]/Count ${Gcounts[$si]}/MediaBox [0 0 ${GoWid} ${GoHei}]>>endobj\n";
411 0           $Gobject[$endNode] = $Gpos;
412 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
413 0           return $endNode;
414             }
415              
416              
417             ##########################################################
418             sub calcRotateMatrix {
419             ##########################################################
420 0     0 0   my $str = "1 0 0 1 ${_[0]} ${_[1]} cm\n";
421 0           my $rotate = $_[2];
422              
423 0 0         return $str if ! $rotate;
424              
425 0           my $upperX = 0; my $upperY = 0;
  0            
426 0           my $radian = sprintf( "%.6f", $rotate / 57.2957795 ); # approx.
427 0           my $Cos = sprintf( "%.6f", cos($radian) );
428 0           my $Sin = sprintf( "%.6f", sin($radian) );
429 0           my $negSin = $Sin * -1;
430 0           $str .= "${Cos} ${Sin} ${negSin} ${Cos} ${upperX} ${upperY} cm\n";
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: uses xref streams
500 0           &dieInvalid;
501             } else { # Input PDF has a malformed xref table
502 0           &dieInvalid;
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 ($objContent, $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           $objContent = getContentOfObjectNr($old_one);
546 0 0         if ( $objContent =~ 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( $objContent, $strPos );
552             } else {
553 0 0         $objContent = substr( $objContent, length($1) )
554             if $objContent =~ m'^(\d+ \d+ obj)\b';
555 0           update_references_and_populate_to_be_created($objContent);
556 0           $out_line = "${new_one} 0 obj ${objContent}";
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 dieInvalid {
568             ##########################################################
569 0     0 0   die "[!] File '${GinFile}' is not a valid v1.4 PDF file.\n"
570             }
571              
572              
573             ##########################################################
574             sub setOutputPageDimensionAndSchema {
575             ##########################################################
576 0 0   0 0   &dieInvalid unless &getPageSizeAndSetMediabox;
577              
578 0           my $surface = $GmediaBox[2] * $GmediaBox[3];
579 0           my $measuresInMm =
580             int($GmediaBox[2] / 72 * 25.4) . " x " . int($GmediaBox[3] / 72 * 25.4) . " mm";
581              
582 0           for ($surface) {
583 0 0         if (alike($_, $Gpaper{QuarterLetter})) {$GoWid = $DW; $GoHei = $DH; return "QT"};
  0            
  0            
  0            
584 0 0         if (alike($_, $Gpaper{A6})) {$GoWid = $AW; $GoHei = $AH; return "A6"};
  0            
  0            
  0            
585 0 0         if (alike($_, $Gpaper{HalfLetter})) {$GoWid = $DW; $GoHei = $DH; return "HT"};
  0            
  0            
  0            
586 0 0         if (alike($_, $Gpaper{QuarterLegal})) {$GoWid = $GW; $GoHei = $GH; return "QG"};
  0            
  0            
  0            
587 0 0         if (alike($_, $Gpaper{A5})) {$GoWid = $AW; $GoHei = $AH; return "A5"};
  0            
  0            
  0            
588 0 0         if (alike($_, $Gpaper{HalfLegal})) {$GoWid = $GW; $GoHei = $GH; return "HG"};
  0            
  0            
  0            
589 0 0         if (alike($_, $Gpaper{Letter})) {$GoWid = $KW; $GoHei = $KH; return "LT"};
  0            
  0            
  0            
590 0 0         if (alike($_, $Gpaper{A4})) {$GoWid = $JW; $GoHei = $JH; return "A4"};
  0            
  0            
  0            
591 0 0         if (alike($_, $Gpaper{Legal})) {return "USlegal, ${measuresInMm}"};
  0            
592 0 0         if (alike($_, $Gpaper{Tabloid})) {return "UStabloid, ${measuresInMm}"};
  0            
593 0 0         if (alike($_, $Gpaper{A3})) {return "A3, ${measuresInMm}"};
  0            
594             }
595 0           return "unknown, ${measuresInMm}";
596             }
597              
598              
599             ##########################################################
600             sub alike {
601             ##########################################################
602 0     0 0   my $num1 = $_[0]; my $num2 = $_[1];
  0            
603 0           my $tolerance = 1500;
604 0 0 0       return 0 if $num1 > $num2 + $tolerance or $num1 < $num2 - $tolerance;
605 0           return 1;
606             }
607              
608              
609             ##########################################################
610             sub getPage {
611             ##########################################################
612 0     0 0   my $pagenumber = $_[0];
613 0 0         die "[!] Page requested (${pagenumber}) does not exist. Aborting.\n"
614             if $pagenumber > &getInputPageCount;
615 0           my ($formRes, $formCont);
616              
617 0           my $thisPageObjContent = xformObjForThisPage($pagenumber);
618 0           ($formRes, $formCont) = parseAsResourcesAndContentRef($thisPageObjContent);
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 $objContent = getContentOfObjectNr($objNr);
629 0           $objContent =~ 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( $objContent, $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 $pagenumber = $_[0];
649 0           my ($objContent, $vector, @pageObj, @pageObjBackup, $pageAccumulator);
650              
651 0 0         return 0 unless $GpagesObjContent =~ 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           $objContent = getContentOfObjectNr($_);
663 0 0         if ( $objContent =~ m'/Count\s+(\d+)' ) {
664 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
665 0           $pageAccumulator += $1;
666             } else {
667 0 0         $vector = $1 if $objContent =~ 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 $objContent;
678             }
679              
680              
681             ##########################################################
682             sub getPageSizeAndSetMediabox {
683             ##########################################################
684 0     0 0   my $objContent;
685 0 0         if ($GpagesObjContent =~ m'MediaBox') {
686 0           $objContent = $GpagesObjContent;
687             } else {
688 0           $objContent = xformObjForThisPage(1);
689 0 0         $objContent =~ m'MediaBox' or return 0; # Meaning "failure"
690             }
691            
692             # Assume all input PDF pages have the same dimensions as first MediaBox found:
693 0 0         if (! @GmediaBox) {
694 0           for ($objContent) {
695 0 0         if (m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
    0          
696 0           @GmediaBox = ($1, $2, $3, $4);
697             } elsif (m'MediaBox\s*(\d+)\s+\d+\s+R\b') { # Pagesize to be found in reference
698 0           my $ref = getContentOfObjectNr($1);
699 0 0         if ($ref =~ m'\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]') {
700 0           @GmediaBox = ($1, $2, $3, $4)
701             } else {
702 0           return 0; # Meaning "failure"
703             }
704             }
705             }
706             }
707 0           return 1; # Meaning "success"
708             }
709              
710              
711             ##########################################################
712             sub parseAsResourcesAndContentRef {
713             ##########################################################
714 0     0 0   my $objContent = $_[0];
715 0           my ($resources, $formCont);
716              
717 0 0 0       $formCont = $1 if $objContent =~ m'/Contents\s+(\d+)'
718             or $objContent =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]';
719              
720 0           $resources = getResourcesFromObj($objContent);
721 0           return ($resources, $formCont);
722             }
723              
724              
725             ##########################################################
726             sub getResourcesFromObj {
727             ##########################################################
728 0     0 0   my $objContent = $_[0];
729 0           my $resources;
730              
731 0 0         return $1 if $objContent =~ m'Resources\s+(\d+\s+\d+\s+R)'; # Reference (95%)
732 0 0         if ( $objContent =~ m'^.+/Resources's ) {
733             # The resources are a dictionary. The whole is copied (morfologia.pdf):
734 0           my $k;
735 0           ( undef, $objContent ) = split /\/Resources/, $objContent;
736 0           $objContent =~ s/<
737 0           $objContent =~ s/>>/>>#/g;
738 0           for ( split /#/, $objContent ) {
739 0 0         if ( m'\S' ) {
740 0           $resources .= $_;
741 0 0         ++$k if m'<<';
742 0 0         --$k if m'>>';
743 0 0         last if $k == 0;
744             }
745             }
746             }
747 0           return $resources;
748             }
749              
750              
751             ##########################################################
752             sub getInputPageCount {
753             ##########################################################
754 0     0 0   state $maxPages;
755 0 0         return $maxPages if defined $maxPages;
756 0           my $objContent;
757              
758 0 0         return 0 unless eval { $objContent = getContentOfObjectNr($GrootNr); 1; };
  0            
  0            
759 0 0         if ( $objContent =~ m'/Pages\s+(\d+)\s+\d+\s+R' ) {
760 0           $objContent = getContentOfObjectNr($1);
761 0 0         $maxPages = $1 if $objContent =~ m'/Count\s+(\d+)';
762             }
763 0           return $maxPages;
764             }
765              
766              
767             ##########################################################
768             sub openInputFile {
769             ##########################################################
770 0     0 0   $GinFile = $_[0];
771 0           my ( $inputPageSize, $inputPageCount, $c );
772 0 0         die "[!] File '${GinFile}' is empty.\n" if ! &getInputFileWeight;
773              
774 0 0         open($IN_FILE, q{<}, $GinFile) or die "[!] File '${GinFile}' cannot be read.\n";
775 0           binmode $IN_FILE;
776              
777 0           sysread $IN_FILE, $c, 5;
778 0 0         &dieInvalid if $c ne "%PDF-";
779              
780             # Find root
781 0           $GrootNr = &getRootAndMapGobjects;
782 0 0         &dieInvalid unless $GrootNr > 0;
783             # Find $rootObjContent
784 0           my $rootObjContent = getContentOfObjectNr($GrootNr);
785 0 0         die "[!] File '${GinFile}' has no Pages section.\n"
786             unless $rootObjContent =~ m'/Pages\s+(\d+)\s+\d+\s+R';
787             # Find GpagesObjContent
788 0           $GpagesObjContent = getContentOfObjectNr($1);
789              
790 0           $inputPageSize = &setOutputPageDimensionAndSchema;
791 0           $inputPageCount = &getInputPageCount;
792              
793 0           return ($inputPageCount, $inputPageSize);
794             }
795              
796              
797             ##########################################################
798             sub getInputFileWeight {
799             ##########################################################
800 0     0 0   state $known;
801 0 0         return $known if $known;
802 0           $known = (stat($GinFile))[7];
803             }
804              
805              
806             ##########################################################
807             sub addSizeToGObjects {
808             ##########################################################
809 0     0 0   my $size = &getInputFileWeight;
810             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
811             # according to their offset in the file: last first
812 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
813 0           my $pos;
814              
815 0           for (@offset) {
816 0           $pos = $GObjects{$_};
817 0           $size -= $pos;
818 0           $GObjects{$_} = [ $pos, $size ];
819 0           $size = $pos;
820             }
821 0           return;
822             }
823              
824              
825             ##########################################################
826             sub update_references_and_populate_to_be_created {
827             ##########################################################
828 0     0 0   $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&xform . " 0 R"/eg;
  0            
829 0           return;
830             }
831              
832              
833             # xform translates an old object reference to a new one
834             # and populates a table with what objects must be created
835             ##########################################################
836             sub xform {
837             ##########################################################
838 0     0 0   state %known;
839 0 0         return $known{$1} if exists $known{$1};
840 0           push @Gto_be_created, [ $1, ++$GobjNr ];
841 0           $known{$1} = $GobjNr; # implicit return value (faster)
842             }
843              
844              
845             ##########################################################
846             sub extractXrefSection {
847             ##########################################################
848 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
849              
850 0           sysread $IN_FILE, $c, 1;
851 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s';
852 0           while ( $c =~ /[\d ]/ ) {
853 0           $readBytes .= $c;
854 0           sysread $IN_FILE, $c, 1;
855             }
856 0 0         ($qty, $idx) = ($2, $1) if $readBytes =~ m'^(\d+)\s+(\d+)';
857             # return ($qty, $idx);
858             }
859              
860              
861             ##########################################################
862             sub createOutputFile {
863             ##########################################################
864 0 0   0 0   &closeOutputFile if $Gpos;
865              
866 0           my $outputfile = $_[0];
867 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep this far from file beginning!
868              
869 0 0         open( $OUT_FILE, q{>}, $outputfile )
870             or die "[!] File '${outputfile}' cannot be created.\n";
871 0           binmode $OUT_FILE;
872 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
873              
874 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
875 0           $Gparents[0] = 2;
876              
877 0           &setInitGrState;
878 0           return;
879             }
880              
881              
882             ##########################################################
883             sub closeInputFile {
884             ##########################################################
885 0     0 0   close $IN_FILE;
886             }
887              
888             1;
889              
890             __END__