File Coverage

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


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