File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 456 1.1
branch 0 198 0.0
condition 0 12 0.0
subroutine 2 36 5.5
pod 0 33 0.0
total 7 735 0.9


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