File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 469 1.0
branch 0 204 0.0
condition 0 12 0.0
subroutine 2 36 5.5
pod 0 33 0.0
total 7 754 0.9


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