File Coverage

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


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