File Coverage

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


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   73706 use v5.10;
  1         3  
4 1     1   4 use strict;
  1         2  
  1         6100  
5             # use warnings;
6             our $VERSION = "1.23";
7              
8             my ($GinFile, $GpageObjNr, $GrootNr, $Gpos, $GobjNr, $Gstream, $GoWid, $GoHei, $GmaxPages);
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]";
268 0 0         if ( %GpageXObject ) {
269 0           $resourceDict .= "/XObject<<";
270 0           $resourceDict .= "/$_ $GpageXObject{$_} 0 R" for sort keys %GpageXObject;
271 0           $resourceDict .= ">>";
272             }
273 0           $resourceDict .= "/ExtGState<>";
274 0           return $resourceDict;
275             }
276              
277              
278             ##########################################################
279             sub writePageResourceDict {
280             ##########################################################
281 0     0 0   my $resourceDict = $_[0];
282              
283 0           state %resources;
284              
285             # Found one identical, use it:
286 0 0         return $resources{$resourceDict} if exists $resources{$resourceDict};
287 0           ++$GobjNr;
288             # Save first 10 resources:
289 0 0         $resources{$resourceDict} = $GobjNr if keys(%resources) < 10;
290 0           $Gobject[$GobjNr] = $Gpos;
291 0           $resourceDict = "${GobjNr} 0 obj<<${resourceDict}>>endobj\n";
292 0           $Gpos += syswrite $OUT_FILE, $resourceDict;
293 0           return $GobjNr;
294             }
295              
296              
297             ##########################################################
298             sub writePageStream {
299             ##########################################################
300 0     0 0   ++$GobjNr;
301 0           $Gobject[$GobjNr] = $Gpos;
302 0           $Gpos += syswrite $OUT_FILE, "${GobjNr} 0 obj<
303             . ">>stream\n${Gstream}\nendstream\nendobj\n";
304 0           $Gobject[$GpageObjNr] = $Gpos;
305 0           $Gstream = "";
306 0           return;
307             }
308              
309              
310             ##########################################################
311             sub writePageResources {
312             ##########################################################
313 0     0 0   my ($parent, $resourceObject) = ($_[0], $_[1]);
314 0           $Gpos += syswrite $OUT_FILE, "${GpageObjNr} 0 obj<
315             . "R/Contents ${GobjNr} 0 R/Resources ${resourceObject} 0 R>>endobj\n";
316 0           push @{ $Gkids[0] }, $GpageObjNr;
  0            
317 0           return;
318             }
319              
320              
321             ##########################################################
322             sub writePage {
323             ##########################################################
324 0 0   0 0   if ( !$Gparents[0] ) {
325 0           ++$GobjNr;
326 0           $Gparents[0] = $GobjNr;
327             }
328 0           my $parent = $Gparents[0];
329 0           my $resourceObject = writePageResourceDict(createPageResourceDict());
330 0           writePageStream();
331 0           writePageResources($parent, $resourceObject);
332 0           ++$Gcounts[0];
333 0 0         writePageNodes(8) if $Gcounts[0] > 9;
334 0           return;
335             }
336              
337              
338             ##########################################################
339             sub closeOutputFile {
340             ##########################################################
341 0 0   0 0   return if !$Gpos;
342              
343 0 0         writePage() if $Gstream;
344 0           my $endNode = writeEndNode();
345              
346 0           my $out_line = "1 0 obj<>endobj\n";
347 0           $Gobject[1] = $Gpos;
348 0           $Gpos += syswrite $OUT_FILE, $out_line;
349 0           my $qty = $#Gobject;
350 0           my $startxref = $Gpos;
351 0           my $xrefQty = $qty + 1;
352 0           $out_line = "xref\n0 ${xrefQty}\n0000000000 65535 f \n";
353 0           $out_line .= sprintf "%.10d 00000 n \n", $_ for @Gobject[1..$qty];
354 0           $out_line .= "trailer\n<<\n/Size ${xrefQty}\n/Root 1 0 R\n"
355             . ">>\nstartxref\n${startxref}\n%%EOF\n";
356              
357 0           syswrite $OUT_FILE, $out_line;
358 0           close $OUT_FILE;
359              
360 0           $Gpos = 0;
361 0           return;
362             }
363              
364              
365             ##########################################################
366             sub writePageNodes {
367             ##########################################################
368 0     0 0   my $qtyChildren = $_[0];
369 0           my $i = 0;
370 0           my $j = 1;
371 0           my $nodeObj;
372              
373 0           while ( $qtyChildren < $#{ $Gkids[$i] } ) {
  0            
374             # Imprimir padre actual y pasar al siguiente nivel:
375 0 0         if ( !$Gparents[$j] ) {
376 0           ++$GobjNr;
377 0           $Gparents[$j] = $GobjNr;
378             }
379              
380             $nodeObj =
381 0           "${Gparents[$i]} 0 obj<
382 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
383 0           $nodeObj .= "]\n/Count ${Gcounts[$i]}>>endobj\n";
384 0           $Gobject[ $Gparents[$i] ] = $Gpos;
385 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
386              
387 0           $Gcounts[$j] += $Gcounts[$i];
388 0           $Gcounts[$i] = 0;
389 0           $Gkids[$i] = [];
390 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
391 0           undef $Gparents[$i];
392 0           ++$i;
393 0           ++$j;
394             }
395 0           return;
396             }
397              
398              
399             ##########################################################
400             sub writeEndNode {
401             ##########################################################
402 0     0 0   my $nodeObj;
403 0           my $endNode = $Gparents[-1]; # content of the last element
404 0           my $si = $#Gparents; # index of the last element
405              
406 0 0         my $min = defined $Gparents[0] ? 0 : 1;
407             # for ( my $i = $min ; $Gparents[$i] ne $endNode; ++$i ) {
408 0           for ( my $i = $min; $i < $si; ++$i ) {
409 0 0         if ( defined $Gparents[$i] ) { # Only defined if there are kids
410             # Find parent of current parent:
411 0           my $node;
412 0           for ( my $j = $i + 1 ; ( !$node ) ; ++$j ) {
413 0 0         if ( $Gparents[$j] ) {
414 0           $node = $Gparents[$j];
415 0           $Gcounts[$j] += $Gcounts[$i];
416 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
417             }
418             }
419              
420 0           $nodeObj = "${Gparents[$i]} 0 obj<
421 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
422 0           $nodeObj .= "]/Count ${Gcounts[$i]}>>endobj\n";
423 0           $Gobject[ $Gparents[$i] ] = $Gpos;
424 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
425             }
426             }
427              
428             # Arrange and print the end node:
429 0           $nodeObj = "${endNode} 0 obj<
430 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$si] };
  0            
431 0           $nodeObj .= "]/Count ${Gcounts[$si]}/MediaBox [0 0 ${GoWid} ${GoHei}]>>endobj\n";
432 0           $Gobject[$endNode] = $Gpos;
433 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
434 0           return $endNode;
435             }
436              
437              
438             ##########################################################
439             sub calcRotateMatrix {
440             ##########################################################
441 0     0 0   my $rotate = $_[2];
442 0           my $str = "1 0 0 1 ${_[0]} ${_[1]} cm\n";
443              
444 0 0         if ($rotate) {
445 0           my $upperX = 0; my $upperY = 0;
  0            
446 0           my $radian = sprintf( "%.6f", $rotate / 57.2957795 ); # approx.
447 0           my $Cos = sprintf( "%.6f", cos($radian) );
448 0           my $Sin = sprintf( "%.6f", sin($radian) );
449 0           my $negSin = $Sin * -1;
450 0           $str .= "${Cos} ${Sin} ${negSin} ${Cos} ${upperX} ${upperY} cm\n";
451             }
452 0           return $str;
453             }
454              
455              
456             ##########################################################
457             sub getRootAndMapGobjects {
458             ##########################################################
459 0     0 0   my ( $xref, $tempRoot, $buf, $buf2 );
460              
461 0           sysseek $IN_FILE, -150, 2;
462 0           sysread $IN_FILE, $buf, 200;
463 0 0         die "[!] File ${GinFile} is encrypted, cannot be used. Aborting.\n"
464             if $buf =~ m'Encrypt';
465              
466 0 0         if ($buf =~ m'/Prev\s+\d') { # "Versioned" PDF file (several xref sections)
    0          
467 0           while ($buf =~ m'/Prev\s+(\d+)') {
468 0           $xref = $1;
469 0           sysseek $IN_FILE, $xref, 0;
470 0           sysread $IN_FILE, $buf, 200;
471             # Reading 200 bytes may NOT be enough. Read on till we find 1st %%EOF:
472 0           until ($buf =~ m'%%EOF') {
473 0           sysread $IN_FILE, $buf2, 200;
474 0           $buf .= $buf2;
475             }
476             }
477             } elsif ( $buf =~ m'\bstartxref\s+(\d+)' ) { # Non-versioned PDF file
478 0           $xref = $1;
479             } else {
480 0           return 0;
481             }
482              
483             # stat[7] = filesize
484 0 0         die "[!] Invalid XREF. Aborting.\n" if $xref > (stat($GinFile))[7];
485 0           populateGobjects($xref);
486 0           $tempRoot = getRootFromTraditionalXrefSection();
487 0 0         return 0 unless $tempRoot; # No Root object in ${GinFile}, aborting
488 0           return $tempRoot;
489             }
490              
491              
492             ##########################################################
493             sub mapGobjectsFromTraditionalXref {
494             ##########################################################
495 0     0 0   my ( $idx, $qty, $readBytes );
496 0           sysseek $IN_FILE, $_[0], 0;
497 0           ($qty, $idx) = extractXrefSection();
498 0           while ($qty) {
499 0           for (1..$qty) {
500 0           sysread $IN_FILE, $readBytes, 20;
501 0 0         $GObjects{$idx} = $1 if $readBytes =~ m'^\s?(\d{10}) \d{5} n';
502 0           ++$idx;
503             }
504 0           ($qty, $idx) = extractXrefSection();
505             }
506 0           return;
507             }
508              
509              
510             ##########################################################
511             sub populateGobjects {
512             ##########################################################
513 0     0 0   my $xrefPos = $_[0];
514 0           my $readBytes;
515              
516 0           sysseek $IN_FILE, $xrefPos, 0;
517 0           sysread $IN_FILE, $readBytes, 22;
518              
519 0 0         if ($readBytes =~ /^(xref$cr)/) { # Input PDF is v1.4 or lower
    0          
520 0           mapGobjectsFromTraditionalXref($xrefPos + length($1));
521             } elsif ($readBytes =~ m'^\d+\s+\d+\s+obj'i) { # Input PDF is v1.5 or higher
522 0           die "[!] File '${GinFile}' uses xref streams (not a v1.4 PDF file).\n";
523             } else {
524 0           die "[!] File '${GinFile}' has a malformed xref table.\n";
525             }
526              
527 0           addSizeToGObjects();
528 0           return;
529             }
530              
531              
532             ##########################################################
533             sub getRootFromTraditionalXrefSection {
534             ##########################################################
535 0     0 0   my $readBytes = " ";
536 0           my $buf;
537 0           while ($readBytes) {
538 0           sysread $IN_FILE, $readBytes, 200;
539 0           $buf .= $readBytes;
540 0 0         return $1 if $buf =~ m'\/Root\s+(\d+)\s+\d+\s+R's;
541             }
542 0           return;
543             }
544              
545              
546             ##########################################################
547             sub getContentOfObjectNr {
548             ##########################################################
549 0     0 0   my $index = $_[0];
550              
551 0 0         return 0 if (! defined $GObjects{$index}); # A non-1.4 PDF
552 0           my $buf;
553 0           my ($offset, $size) = @{ $GObjects{$index} };
  0            
554 0           sysseek $IN_FILE, $offset, 0;
555 0           sysread $IN_FILE, $buf, $size;
556 0           return $buf;
557             }
558              
559              
560             ##########################################################
561             sub writePageObjectsToOutputFile {
562             ##########################################################
563 0     0 0   my ($objectContent, $out_line, $part, $strPos, $old_one, $new_one);
564              
565 0           for (@Gto_be_created) {
566 0           $old_one = $_->[0];
567 0           $new_one = $_->[1];
568 0           $objectContent = getContentOfObjectNr($old_one);
569 0 0         if ( $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s ) {
570 0           $part = $2;
571 0           $strPos = length($1) + length($2) + length($3);
572 0           update_references_and_populate_to_be_created($part);
573 0           $out_line = "${new_one} 0 obj\n<<${part}>>stream";
574 0           $out_line .= substr( $objectContent, $strPos );
575             } else {
576 0 0         $objectContent = substr( $objectContent, length($1) )
577             if $objectContent =~ m'^(\d+ \d+ obj\s*)'s;
578 0           update_references_and_populate_to_be_created($objectContent);
579 0           $out_line = "${new_one} 0 obj ${objectContent}";
580             }
581 0           $Gobject[$new_one] = $Gpos;
582 0           $Gpos += syswrite $OUT_FILE, $out_line;
583             }
584 0           undef @Gto_be_created;
585 0           return;
586             }
587              
588              
589             ##########################################################
590             sub getInputPageDimensions {
591             ##########################################################
592             # Find root:
593 0     0 0   my $objectContent = getContentOfObjectNr($GrootNr);
594              
595             # Find pages:
596 0 0         return "unknown" unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
597 0           $objectContent = getContentOfObjectNr($1);
598 0 0         $objectContent = xformObjForThisPage($objectContent, 1)
599             unless $objectContent =~ m'MediaBox's;
600 0           (undef, undef) = getPageResourcesAndContent( $objectContent );
601 0 0 0       return "unknown" if ! defined $GmediaBox[2] or ! defined $GmediaBox[3];
602              
603 0           my $surface = $GmediaBox[2]*$GmediaBox[3];
604 0           my $measuresInMm = int($GmediaBox[2] / 72 * 25.4) . " x "
605             . int($GmediaBox[3] / 72 * 25.4) . " mm";
606              
607 0           for ($surface) {
608 0 0         if (alike($_, $Gpaper{QuarterLetter})) {$GoWid = $DW; $GoHei = $DH; return "QT"};
  0            
  0            
  0            
609 0 0         if (alike($_, $Gpaper{A6})) {$GoWid = $AW; $GoHei = $AH; return "A6"};
  0            
  0            
  0            
610 0 0         if (alike($_, $Gpaper{HalfLetter})) {$GoWid = $DW; $GoHei = $DH; return "HT"};
  0            
  0            
  0            
611 0 0         if (alike($_, $Gpaper{QuarterLegal})) {$GoWid = $GW; $GoHei = $GH; return "QG"};
  0            
  0            
  0            
612 0 0         if (alike($_, $Gpaper{A5})) {$GoWid = $AW; $GoHei = $AH; return "A5"};
  0            
  0            
  0            
613 0 0         if (alike($_, $Gpaper{HalfLegal})) {$GoWid = $GW; $GoHei = $GH; return "HG"};
  0            
  0            
  0            
614 0 0         if (alike($_, $Gpaper{Letter})) {$GoWid = $KW; $GoHei = $KH; return "LT"};
  0            
  0            
  0            
615 0 0         if (alike($_, $Gpaper{A4})) {$GoWid = $JW; $GoHei = $JH; return "A4"};
  0            
  0            
  0            
616 0 0         if (alike($_, $Gpaper{Legal})) {return "USlegal, ${measuresInMm}"};
  0            
617 0 0         if (alike($_, $Gpaper{Tabloid})) {return "UStabloid, ${measuresInMm}"};
  0            
618 0 0         if (alike($_, $Gpaper{A3})) {return "A3, ${measuresInMm}"};
  0            
619             }
620 0           return "unknown, ${measuresInMm}";
621             }
622              
623              
624             ##########################################################
625             sub alike {
626             ##########################################################
627 0     0 0   my $hxw = $_[0]; my $namedHxw = $_[1];
  0            
628 0           my $tolerance = 1500;
629 0 0 0       return 0 if $hxw > $namedHxw + $tolerance or $hxw < $namedHxw - $tolerance;
630 0           return 1;
631             }
632              
633              
634             ##########################################################
635             sub getPage {
636             ##########################################################
637 0     0 0   my $pagenumber = $_[0];
638 0 0         die "[!] Page requested (${pagenumber}) does not exist. Aborting.\n"
639             if $pagenumber > $GmaxPages;
640 0           my ($reference, $formRes, $formCont);
641              
642             # Find root:
643 0           my $objectContent = getContentOfObjectNr($GrootNr);
644              
645             # Find pages:
646 0 0         die "[!] Didn't find Pages section in '${GinFile}'. Aborting.\n"
647             unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
648 0           $objectContent = getContentOfObjectNr($1);
649 0           $objectContent = xformObjForThisPage($objectContent, $pagenumber);
650 0           ($formRes, $formCont) = getPageResourcesAndContent($objectContent);
651 0           return ($formRes, $formCont);
652             }
653              
654              
655             ##########################################################
656             sub writeRes {
657             ##########################################################
658 0     0 0   my ($formRes, $objNr) = ($_[0], $_[1]);
659              
660 0           my $objectContent = getContentOfObjectNr($objNr);
661 0           $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
662 0           my $strPos = length($1) + length($2) + length($3);
663 0           my $newPart = "<
664             . "/BBox [@{GmediaBox}] ${2}";
665              
666 0           ++$GobjNr;
667 0           $Gobject[$GobjNr] = $Gpos;
668 0           my $reference = $GobjNr;
669 0           update_references_and_populate_to_be_created($newPart);
670 0           my $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
671 0           $out_line .= substr( $objectContent, $strPos );
672 0           $Gpos += syswrite $OUT_FILE, $out_line;
673 0           return $reference;
674             }
675              
676              
677             ##########################################################
678             sub xformObjForThisPage {
679             ##########################################################
680 0     0 0   my ($objectContent, $pagenumber) = ($_[0], $_[1]);
681 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
682              
683 0 0         return 0 unless $objectContent =~ m'/Kids\s*\[([^\]]+)'s;
684 0           $vector = $1;
685              
686 0           $pageAccumulator = 0;
687              
688 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
689 0           while ( $pageAccumulator < $pagenumber ) {
690 0           @pageObjBackup = @pageObj;
691 0           undef @pageObj;
692 0 0         last if ! @pageObjBackup; # $pagenumber is > than number of pages in PDF
693 0           for (@pageObjBackup) {
694 0           $objectContent = getContentOfObjectNr($_);
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 getPageResourcesAndContent {
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 = getContentOfObjectNr($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;
774              
775 0 0         return 0 unless eval { $objectContent = getContentOfObjectNr($GrootNr); 1; };
  0            
  0            
776 0 0         if ( $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's ) {
777 0           $objectContent = getContentOfObjectNr($1);
778 0 0         $GmaxPages = $1 if $objectContent =~ m'/Count\s+(\d+)'s;
779             }
780 0           return $GmaxPages;
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 objects 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__