File Coverage

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


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   78099 use v5.10;
  1         2  
4 1     1   4 use strict;
  1         2  
  1         5963  
5             # use warnings;
6             our $VERSION = "1.24";
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 ($formRes, $formCont) = getPage($pagenumber);
239 0           my $refNr = writeRes($formRes, $formCont);
240 0 0         die "[!] Page ${pagenumber} in ${GinFile} can't be used. Concatenate streams!\n"
241             if !defined $refNr;
242 0 0         die "[!] Page ${pagenumber} doesn't exist in file ${GinFile}.\n" if !$refNr;
243 0           writePageObjectsToOutputFile();
244              
245 0           $Gstream .= "q\n". calcRotateMatrix($x, $y, $rotate) ."\n/Gs0 gs\n/${name} Do\nQ\n";
246 0           $GpageXObject{$name} = $refNr;
247              
248 0           return;
249             }
250              
251              
252             ##########################################################
253             sub setInitGrState {
254             ##########################################################
255 0     0 0   ++$GobjNr;
256              
257 0           $Gobject[$GobjNr] = $Gpos;
258 0           $Gpos += syswrite $OUT_FILE,
259             "${GobjNr} 0 obj<>endobj\n";
260 0           return;
261             }
262              
263              
264             ##########################################################
265             sub createPageResourceDict {
266             ##########################################################
267 0     0 0   my $resourceDict = "/ProcSet[/PDF/Text]/XObject<<";
268 0           $resourceDict .= "/${_} ${GpageXObject{${_}}} 0 R" for keys %GpageXObject;
269 0           $resourceDict .= ">>/ExtGState<>";
270 0           return $resourceDict;
271             }
272              
273              
274             ##########################################################
275             sub writePageResourceDict {
276             ##########################################################
277 0     0 0   my $resourceDict = $_[0];
278              
279 0           state %resources;
280              
281             # Found one identical, use it:
282 0 0         return $resources{$resourceDict} if exists $resources{$resourceDict};
283 0           ++$GobjNr;
284             # Save first 10 resources:
285 0 0         $resources{$resourceDict} = $GobjNr if keys(%resources) < 10;
286 0           $Gobject[$GobjNr] = $Gpos;
287 0           $resourceDict = "${GobjNr} 0 obj<<${resourceDict}>>endobj\n";
288 0           $Gpos += syswrite $OUT_FILE, $resourceDict;
289 0           return $GobjNr;
290             }
291              
292              
293             ##########################################################
294             sub writePageStream {
295             ##########################################################
296 0     0 0   ++$GobjNr;
297 0           $Gobject[$GobjNr] = $Gpos;
298 0           $Gpos += syswrite $OUT_FILE, "${GobjNr} 0 obj<
299             . ">>stream\n${Gstream}\nendstream\nendobj\n";
300 0           $Gobject[$GpageObjNr] = $Gpos;
301 0           $Gstream = "";
302 0           return;
303             }
304              
305              
306             ##########################################################
307             sub writePageResources {
308             ##########################################################
309 0     0 0   my ($parent, $resourceObject) = ($_[0], $_[1]);
310 0           $Gpos += syswrite $OUT_FILE, "${GpageObjNr} 0 obj<
311             . "R/Contents ${GobjNr} 0 R/Resources ${resourceObject} 0 R>>endobj\n";
312 0           push @{ $Gkids[0] }, $GpageObjNr;
  0            
313 0           return;
314             }
315              
316              
317             ##########################################################
318             sub writePage {
319             ##########################################################
320 0 0   0 0   if ( !$Gparents[0] ) {
321 0           ++$GobjNr;
322 0           $Gparents[0] = $GobjNr;
323             }
324 0           my $parent = $Gparents[0];
325 0           my $resourceObjectNr = writePageResourceDict(createPageResourceDict());
326 0           writePageStream();
327 0           writePageResources($parent, $resourceObjectNr);
328 0           ++$Gcounts[0];
329 0 0         writePageNodes(8) if $Gcounts[0] > 9;
330 0           return;
331             }
332              
333              
334             ##########################################################
335             sub closeOutputFile {
336             ##########################################################
337 0 0   0 0   return if !$Gpos;
338              
339 0 0         writePage() if $Gstream;
340 0           my $endNode = writeEndNode();
341              
342 0           my $out_line = "1 0 obj<>endobj\n";
343 0           $Gobject[1] = $Gpos;
344 0           $Gpos += syswrite $OUT_FILE, $out_line;
345 0           my $qty = $#Gobject;
346 0           my $startxref = $Gpos;
347 0           my $xrefQty = $qty + 1;
348 0           $out_line = "xref\n0 ${xrefQty}\n0000000000 65535 f \n";
349 0           $out_line .= sprintf "%.10d 00000 n \n", $_ for @Gobject[1..$qty];
350 0           $out_line .= "trailer\n<<\n/Size ${xrefQty}\n/Root 1 0 R\n"
351             . ">>\nstartxref\n${startxref}\n%%EOF\n";
352              
353 0           syswrite $OUT_FILE, $out_line;
354 0           close $OUT_FILE;
355              
356 0           $Gpos = 0;
357 0           return;
358             }
359              
360              
361             ##########################################################
362             sub writePageNodes {
363             ##########################################################
364 0     0 0   my $qtyChildren = $_[0];
365 0           my $i = 0;
366 0           my $j = 1;
367 0           my $nodeObj;
368              
369 0           while ( $qtyChildren < $#{ $Gkids[$i] } ) {
  0            
370             # Imprimir padre actual y pasar al siguiente nivel:
371 0 0         if ( !$Gparents[$j] ) {
372 0           ++$GobjNr;
373 0           $Gparents[$j] = $GobjNr;
374             }
375              
376             $nodeObj =
377 0           "${Gparents[$i]} 0 obj<
378 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
379 0           $nodeObj .= "]\n/Count ${Gcounts[$i]}>>endobj\n";
380 0           $Gobject[ $Gparents[$i] ] = $Gpos;
381 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
382              
383 0           $Gcounts[$j] += $Gcounts[$i];
384 0           $Gcounts[$i] = 0;
385 0           $Gkids[$i] = [];
386 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
387 0           undef $Gparents[$i];
388 0           ++$i;
389 0           ++$j;
390             }
391 0           return;
392             }
393              
394              
395             ##########################################################
396             sub writeEndNode {
397             ##########################################################
398 0     0 0   my $nodeObj;
399 0           my $endNode = $Gparents[-1]; # content of the last element
400 0           my $si = $#Gparents; # index of the last element
401              
402 0 0         my $min = defined $Gparents[0] ? 0 : 1;
403             # for ( my $i = $min ; $Gparents[$i] ne $endNode; ++$i ) {
404 0           for ( my $i = $min; $i < $si; ++$i ) {
405 0 0         if ( defined $Gparents[$i] ) { # Only defined if there are kids
406             # Find parent of current parent:
407 0           my $node;
408 0           for ( my $j = $i + 1 ; ( !$node ) ; ++$j ) {
409 0 0         if ( $Gparents[$j] ) {
410 0           $node = $Gparents[$j];
411 0           $Gcounts[$j] += $Gcounts[$i];
412 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
413             }
414             }
415              
416 0           $nodeObj = "${Gparents[$i]} 0 obj<
417 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
418 0           $nodeObj .= "]/Count ${Gcounts[$i]}>>endobj\n";
419 0           $Gobject[ $Gparents[$i] ] = $Gpos;
420 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
421             }
422             }
423              
424             # Arrange and print the end node:
425 0           $nodeObj = "${endNode} 0 obj<
426 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$si] };
  0            
427 0           $nodeObj .= "]/Count ${Gcounts[$si]}/MediaBox [0 0 ${GoWid} ${GoHei}]>>endobj\n";
428 0           $Gobject[$endNode] = $Gpos;
429 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
430 0           return $endNode;
431             }
432              
433              
434             ##########################################################
435             sub calcRotateMatrix {
436             ##########################################################
437 0     0 0   my $str = "1 0 0 1 ${_[0]} ${_[1]} cm\n";
438 0           my $rotate = $_[2];
439              
440 0 0         if ($rotate) {
441 0           my $upperX = 0; my $upperY = 0;
  0            
442 0           my $radian = sprintf( "%.6f", $rotate / 57.2957795 ); # approx.
443 0           my $Cos = sprintf( "%.6f", cos($radian) );
444 0           my $Sin = sprintf( "%.6f", sin($radian) );
445 0           my $negSin = $Sin * -1;
446 0           $str .= "${Cos} ${Sin} ${negSin} ${Cos} ${upperX} ${upperY} cm\n";
447             }
448 0           return $str;
449             }
450              
451              
452             ##########################################################
453             sub getRootAndMapGobjects {
454             ##########################################################
455 0     0 0   my ( $xref, $tempRoot, $buf, $buf2 );
456              
457 0           sysseek $IN_FILE, -150, 2;
458 0           sysread $IN_FILE, $buf, 200;
459 0 0         die "[!] File ${GinFile} is encrypted, cannot be used. Aborting.\n"
460             if $buf =~ m'Encrypt';
461              
462 0 0         if ($buf =~ m'/Prev\s+\d') { # "Versioned" PDF file (several xref sections)
    0          
463 0           while ($buf =~ m'/Prev\s+(\d+)') {
464 0           $xref = $1;
465 0           sysseek $IN_FILE, $xref, 0;
466 0           sysread $IN_FILE, $buf, 200;
467             # Reading 200 bytes may NOT be enough. Read on till we find 1st %%EOF:
468 0           until ($buf =~ m'%%EOF') {
469 0           sysread $IN_FILE, $buf2, 200;
470 0           $buf .= $buf2;
471             }
472             }
473             } elsif ( $buf =~ m'\bstartxref\s+(\d+)' ) { # Non-versioned PDF file
474 0           $xref = $1;
475             } else {
476 0           return 0;
477             }
478              
479             # stat[7] = filesize
480 0 0         die "[!] Invalid XREF. Aborting.\n" if $xref > (stat($GinFile))[7];
481 0           populateGobjects($xref);
482 0           $tempRoot = getRootFromTraditionalXrefSection();
483 0 0         return 0 unless $tempRoot; # No Root object in ${GinFile}, aborting
484 0           return $tempRoot;
485             }
486              
487              
488             ##########################################################
489             sub mapGobjectsFromTraditionalXref {
490             ##########################################################
491 0     0 0   my ( $idx, $qty, $readBytes );
492 0           sysseek $IN_FILE, $_[0], 0;
493 0           ($qty, $idx) = extractXrefSection();
494 0           while ($qty) {
495 0           for (1..$qty) {
496 0           sysread $IN_FILE, $readBytes, 20;
497 0 0         $GObjects{$idx} = $1 if $readBytes =~ m'^\s?(\d{10}) \d{5} n';
498 0           ++$idx;
499             }
500 0           ($qty, $idx) = extractXrefSection();
501             }
502 0           return;
503             }
504              
505              
506             ##########################################################
507             sub populateGobjects {
508             ##########################################################
509 0     0 0   my $xrefPos = $_[0];
510 0           my $readBytes;
511              
512 0           sysseek $IN_FILE, $xrefPos, 0;
513 0           sysread $IN_FILE, $readBytes, 22;
514              
515 0 0         if ($readBytes =~ /^(xref$cr)/) { # Input PDF is v1.4 or lower
    0          
516 0           mapGobjectsFromTraditionalXref($xrefPos + length($1));
517             } elsif ($readBytes =~ m'^\d+\s+\d+\s+obj'i) { # Input PDF is v1.5 or higher
518 0           die "[!] File '${GinFile}' uses xref streams (not a v1.4 PDF file).\n";
519             } else {
520 0           die "[!] File '${GinFile}' has a malformed xref table.\n";
521             }
522              
523 0           addSizeToGObjects();
524 0           return;
525             }
526              
527              
528             ##########################################################
529             sub getRootFromTraditionalXrefSection {
530             ##########################################################
531 0     0 0   my $readBytes = " ";
532 0           my $buf;
533 0           while ($readBytes) {
534 0           sysread $IN_FILE, $readBytes, 200;
535 0           $buf .= $readBytes;
536 0 0         return $1 if $buf =~ m'\/Root\s+(\d+)\s+\d+\s+R's;
537             }
538 0           return;
539             }
540              
541              
542             ##########################################################
543             sub getContentOfObjectNr {
544             ##########################################################
545 0     0 0   my $index = $_[0];
546              
547 0 0         return 0 if (! defined $GObjects{$index}); # A non-1.4 PDF
548 0           my $buf;
549 0           my ($offset, $size) = @{ $GObjects{$index} };
  0            
550 0           sysseek $IN_FILE, $offset, 0;
551 0           sysread $IN_FILE, $buf, $size;
552 0           return $buf;
553             }
554              
555              
556             ##########################################################
557             sub writePageObjectsToOutputFile {
558             ##########################################################
559 0     0 0   my ($objectContent, $out_line, $part, $strPos, $old_one, $new_one);
560              
561 0           for (@Gto_be_created) {
562 0           $old_one = $_->[0];
563 0           $new_one = $_->[1];
564 0           $objectContent = getContentOfObjectNr($old_one);
565 0 0         if ( $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s ) {
566 0           $part = $2;
567 0           $strPos = length($1) + length($2) + length($3);
568 0           update_references_and_populate_to_be_created($part);
569 0           $out_line = "${new_one} 0 obj\n<<${part}>>stream";
570 0           $out_line .= substr( $objectContent, $strPos );
571             } else {
572 0 0         $objectContent = substr( $objectContent, length($1) )
573             if $objectContent =~ m'^(\d+ \d+ obj)\b's;
574 0           update_references_and_populate_to_be_created($objectContent);
575 0           $out_line = "${new_one} 0 obj ${objectContent}";
576             }
577 0           $Gobject[$new_one] = $Gpos;
578 0           $Gpos += syswrite $OUT_FILE, $out_line;
579             }
580 0           undef @Gto_be_created;
581 0           return;
582             }
583              
584              
585             ##########################################################
586             sub getInputPageDimensions {
587             ##########################################################
588             # Find root:
589 0     0 0   my $objectContent = getContentOfObjectNr($GrootNr);
590              
591             # Find pages:
592 0 0         return "unknown" unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
593 0           $objectContent = getContentOfObjectNr($1);
594 0 0         $objectContent = xformObjForThisPage($objectContent, 1)
595             unless $objectContent =~ m'MediaBox's;
596 0           (undef, undef) = parseAsResourcesAndContentRef( $objectContent );
597 0 0 0       return "unknown" if ! defined $GmediaBox[2] or ! defined $GmediaBox[3];
598              
599 0           my $surface = $GmediaBox[2]*$GmediaBox[3];
600 0           my $measuresInMm = int($GmediaBox[2] / 72 * 25.4) . " x "
601             . int($GmediaBox[3] / 72 * 25.4) . " mm";
602              
603 0           for ($surface) {
604 0 0         if (alike($_, $Gpaper{QuarterLetter})) {$GoWid = $DW; $GoHei = $DH; return "QT"};
  0            
  0            
  0            
605 0 0         if (alike($_, $Gpaper{A6})) {$GoWid = $AW; $GoHei = $AH; return "A6"};
  0            
  0            
  0            
606 0 0         if (alike($_, $Gpaper{HalfLetter})) {$GoWid = $DW; $GoHei = $DH; return "HT"};
  0            
  0            
  0            
607 0 0         if (alike($_, $Gpaper{QuarterLegal})) {$GoWid = $GW; $GoHei = $GH; return "QG"};
  0            
  0            
  0            
608 0 0         if (alike($_, $Gpaper{A5})) {$GoWid = $AW; $GoHei = $AH; return "A5"};
  0            
  0            
  0            
609 0 0         if (alike($_, $Gpaper{HalfLegal})) {$GoWid = $GW; $GoHei = $GH; return "HG"};
  0            
  0            
  0            
610 0 0         if (alike($_, $Gpaper{Letter})) {$GoWid = $KW; $GoHei = $KH; return "LT"};
  0            
  0            
  0            
611 0 0         if (alike($_, $Gpaper{A4})) {$GoWid = $JW; $GoHei = $JH; return "A4"};
  0            
  0            
  0            
612 0 0         if (alike($_, $Gpaper{Legal})) {return "USlegal, ${measuresInMm}"};
  0            
613 0 0         if (alike($_, $Gpaper{Tabloid})) {return "UStabloid, ${measuresInMm}"};
  0            
614 0 0         if (alike($_, $Gpaper{A3})) {return "A3, ${measuresInMm}"};
  0            
615             }
616 0           return "unknown, ${measuresInMm}";
617             }
618              
619              
620             ##########################################################
621             sub alike {
622             ##########################################################
623 0     0 0   my $hxw = $_[0]; my $namedHxw = $_[1];
  0            
624 0           my $tolerance = 1500;
625 0 0 0       return 0 if $hxw > $namedHxw + $tolerance or $hxw < $namedHxw - $tolerance;
626 0           return 1;
627             }
628              
629              
630             ##########################################################
631             sub getPage {
632             ##########################################################
633 0     0 0   my $pagenumber = $_[0];
634 0 0         die "[!] Page requested (${pagenumber}) does not exist. Aborting.\n"
635             if $pagenumber > getInputPageCount();
636 0           my ($formRes, $formCont);
637              
638             # Find root:
639 0           my $objectContent = getContentOfObjectNr($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 = getContentOfObjectNr($1);
645 0           $objectContent = xformObjForThisPage($objectContent, $pagenumber);
646 0           ($formRes, $formCont) = parseAsResourcesAndContentRef($objectContent);
647 0           return ($formRes, $formCont);
648             }
649              
650              
651             ##########################################################
652             sub writeRes {
653             ##########################################################
654 0     0 0   my ($formRes, $objNr) = ($_[0], $_[1]);
655              
656 0           my $objectContent = getContentOfObjectNr($objNr);
657 0           $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
658 0           my $strPos = length($1) + length($2) + length($3);
659 0           my $newPart = "<
660             . "/BBox [@{GmediaBox}] ${2}";
661              
662 0           ++$GobjNr;
663 0           $Gobject[$GobjNr] = $Gpos;
664 0           my $reference = $GobjNr;
665 0           update_references_and_populate_to_be_created($newPart);
666 0           my $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
667 0           $out_line .= substr( $objectContent, $strPos );
668 0           $Gpos += syswrite $OUT_FILE, $out_line;
669 0           return $reference;
670             }
671              
672              
673             ##########################################################
674             sub xformObjForThisPage {
675             ##########################################################
676 0     0 0   my ($objectContent, $pagenumber) = ($_[0], $_[1]);
677 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
678              
679 0 0         return 0 unless $objectContent =~ m'/Kids\s*\[([^\]]+)'s;
680 0           $vector = $1;
681              
682 0           $pageAccumulator = 0;
683              
684 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
685 0           while ( $pageAccumulator < $pagenumber ) {
686 0           @pageObjBackup = @pageObj;
687 0           undef @pageObj;
688 0 0         last if ! @pageObjBackup; # $pagenumber is > than number of pages in PDF
689 0           for (@pageObjBackup) {
690 0           $objectContent = getContentOfObjectNr($_);
691 0 0         if ( $objectContent =~ m'/Count\s+(\d+)'s ) {
692 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
693 0           $pageAccumulator += $1;
694             } else {
695 0 0         $vector = $1 if $objectContent =~ m'/Kids\s*\[([^\]]+)'s ;
696 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
697 0           last;
698             }
699             } else {
700 0           ++$pageAccumulator;
701             }
702 0 0         last if $pageAccumulator == $pagenumber;
703             }
704             }
705 0           return $objectContent;
706             }
707              
708              
709             ##########################################################
710             sub parseAsResourcesAndContentRef {
711             ##########################################################
712 0     0 0   my $objContent = $_[0];
713 0           my ($resources, $formCont);
714              
715             # Assume all input PDF pages have the same dimensions as first MediaBox found:
716 0 0         if (! @GmediaBox) {
717 0           for ($objContent) {
718 0 0         if (m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]'s) {
    0          
719 0           @GmediaBox = ($1, $2, $3, $4);
720             } elsif (m'MediaBox\s*(\d+)\s+\d+\s+R\b's) { # Size to be found in reference
721 0           my $ref = getContentOfObjectNr($1);
722 0 0         @GmediaBox = ($1, $2, $3, $4)
723             if ($ref =~ m'\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]'s);
724             }
725             }
726             }
727              
728 0 0         if ( $objContent =~ m'/Contents\s+(\d+)'s ) {
    0          
729 0           $formCont = $1;
730             } elsif ( $objContent =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]'s ) {
731 0           $formCont = $1;
732             }
733              
734 0           $resources = getResourcesFromObj($objContent);
735              
736 0           return ($resources, $formCont);
737             }
738              
739              
740             ##########################################################
741             sub getResourcesFromObj {
742             ##########################################################
743 0     0 0   my $objContent = $_[0];
744 0           my $resources;
745              
746 0 0         if ( $objContent =~ m'^(.+/Resources)'s ) {
747 0 0         return $1 if $objContent =~ m'Resources\s+(\d+\s+\d+\s+R)'s; # Reference (95%)
748             # The resources are a dictionary. The whole is copied (morfologia.pdf):
749 0           my $k;
750 0           ( undef, $objContent ) = split /\/Resources/, $objContent;
751 0           $objContent =~ s/<
752 0           $objContent =~ s/>>/>>#/gs;
753 0           for ( split /#/, $objContent ) {
754 0 0         if ( m'\S's ) {
755 0           $resources .= $_;
756 0 0         ++$k if m'<<'s;
757 0 0         --$k if m'>>'s;
758 0 0         last if $k == 0;
759             }
760             }
761             }
762 0           return $resources;
763             }
764              
765              
766             ##########################################################
767             sub getInputPageCount {
768             ##########################################################
769 0     0 0   state $maxPages;
770 0 0         return $maxPages if defined $maxPages;
771 0           my $objectContent;
772              
773 0 0         return 0 unless eval { $objectContent = getContentOfObjectNr($GrootNr); 1; };
  0            
  0            
774 0 0         if ( $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's ) {
775 0           $objectContent = getContentOfObjectNr($1);
776 0 0         $maxPages = $1 if $objectContent =~ m'/Count\s+(\d+)'s;
777             }
778 0           return $maxPages;
779             }
780              
781              
782             ##########################################################
783             sub openInputFile {
784             ##########################################################
785 0     0 0   $GinFile = $_[0];
786 0           my ( $objectContent, $inputPageSize, $inputPageCount, $c );
787              
788 0 0         open( $IN_FILE, q{<}, $GinFile )
789             or die "[!] Couldn't open '${GinFile}'.\n";
790 0           binmode $IN_FILE;
791              
792 0           sysread $IN_FILE, $c, 5;
793 0 0         return 0 if $c ne "%PDF-";
794              
795             # Find root
796 0           $GrootNr = getRootAndMapGobjects();
797 0 0         return 0 unless $GrootNr > 0;
798              
799 0           $inputPageSize = getInputPageDimensions();
800 0           $inputPageCount = getInputPageCount();
801 0           return ($inputPageCount, $inputPageSize);
802             }
803              
804              
805             ##########################################################
806             sub addSizeToGObjects {
807             ##########################################################
808 0     0 0   my $size = (stat($GinFile))[7]; # stat[7] = filesize
809             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
810             # according to their offset in the file: last first
811 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
812 0           my $pos;
813              
814 0           for (@offset) {
815 0           $pos = $GObjects{$_};
816 0           $size -= $pos;
817 0           $GObjects{$_} = [ $pos, $size ];
818 0           $size = $pos;
819             }
820             }
821              
822              
823             ##########################################################
824             sub update_references_and_populate_to_be_created {
825             ##########################################################
826             # $xform translates an old object reference to a new one
827             # and populates a table with what objects must be created
828 0     0 0   state %known;
829             my $xform = sub {
830 0 0   0     return $known{$1} if exists $known{$1};
831 0           push @Gto_be_created, [ $1, ++$GobjNr ];
832 0           return $known{$1} = $GobjNr;
833 0           };
834 0           $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&$xform . ' 0 R'/eg;
  0            
835 0           return;
836             }
837              
838              
839             ##########################################################
840             sub extractXrefSection {
841             ##########################################################
842 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
843              
844 0           sysread $IN_FILE, $c, 1;
845 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s's;
846 0           while ( $c =~ /[\d ]/ ) {
847 0           $readBytes .= $c;
848 0           sysread $IN_FILE, $c, 1;
849             }
850 0 0         ($idx, $qty) = ($1, $2) if $readBytes =~ m'^(\d+)\s+(\d+)';
851 0           return ($qty, $idx);
852             }
853              
854              
855             ##########################################################
856             sub openOutputFile {
857             ##########################################################
858 0 0   0 0   closeOutputFile() if $Gpos;
859              
860 0           my $outputfile = $_[0];
861 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep it far from file beginning!
862              
863 0 0         open( $OUT_FILE, q{>}, $outputfile )
864             or die "[!] Couldn't open file '${outputfile}'.\n";
865 0           binmode $OUT_FILE;
866 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
867              
868 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
869 0           $Gparents[0] = 2;
870              
871 0           setInitGrState();
872 0           return;
873             }
874              
875              
876             ##########################################################
877             sub closeInputFile {
878             ##########################################################
879 0     0 0   close $IN_FILE;
880             }
881              
882             1;
883              
884             __END__