File Coverage

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


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   90097 use v5.10;
  1         3  
4 1     1   6 use strict;
  1         1  
  1         5582  
5             # use warnings;
6             our $VERSION = "1.20";
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.5500; # [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             );
56              
57             # Page reordering and position offset schemas for "4 up":
58             my @P_4UP_13PLUS = (16,1,13,4,2,15,3,14,12,5,9,8,6,11,7,10);
59             my @P_4UP_9PLUS = (12,1,9,4,2,11,3,10,6,7,9999,9999,8,5);
60             my @P_4UP_5PLUS = (8,1,5,4,2,7,3,6);
61             my @P_4UP_1PLUS = (4,1,9999,9999,2,3);
62             my @X_A6_ON_A4 = (0,$CW,$CW,$AW,0,$CW,$CW,$AW,0,$CW,$CW,$AW,0,$CW,$CW,$AW);
63             my @Y_A6_ON_A4 = ($CG,$CG,$CH,$CH,$CG,$CG,$CH,$CH,$CG,$CG,$CH,$CH,$CG,$CG,$CH,$CH);
64             my @X_QT_ON_LT = (0,$FW,$FW,$DW,0,$FW,$FW,$DW,0,$FW,$FW,$DW,0,$FW,$FW,$DW);
65             my @Y_QT_ON_LT = ($FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH,$FH);
66             my @X_QG_ON_LG = (0,$IW,$IW,$GW,0,$IW,$IW,$GW,0,$IW,$IW,$GW,0,$IW,$IW,$GW);
67             my @Y_QG_ON_LG = ($IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH,$IH);
68              
69             # Page reordering and position offset schemas for "2 up":
70             my @P_2UP_13PLUS = (1,16,2,15,3,14,4,13,5,12,6,11,7,10,8,9);
71             my @P_2UP_9PLUS = (1,12,2,11,3,10,4,9,5,8,6,7);
72             my @P_2UP_5PLUS = (1,8,2,7,3,6,4,5);
73             my @P_2UP_1PLUS = (1,4,2,3);
74             my @X_A5_ON_A4 = ($BH,$BH,0,0,$BH,$BH,0,0,$BH,$BH,0,0,$BH,$BH,0,0);
75             my @Y_A5_ON_A4 = ($BG,0,$AH,$BG,$BG,0,$AH,$BG,$BG,0,$AH,$BG,$BG,0,$AH,$BG);
76             my @X_HT_ON_LT = ($EH,$EH,0,0,$EH,$EH,0,0,$EH,$EH,0,0,$EH,$EH,0,0);
77             my @Y_HT_ON_LT = ($EW,0,$DH,$EW,$EW,0,$DH,$EW,$EW,0,$DH,$EW,$EW,0,$DH,$EW);
78             my @X_HG_ON_LG = ($HH,$HH,0,0,$HH,$HH,0,0,$HH,$HH,0,0,$HH,$HH,0,0);
79             my @Y_HG_ON_LG = ($HW,0,$GH,$HW,$HW,0,$GH,$HW,$HW,0,$GH,$HW,$HW,0,$GH,$HW);
80             my @X_LT_ON_TA = ($DH,$DH,0,0,$DH,$DH,0,0,$DH,$DH,0,0,$DH,$DH,0,0);
81             my @Y_LT_ON_TA = ($DW,0,$KH,$DW,$DW,0,$KH,$DW,$DW,0,$KH,$DW,$DW,0,$KH,$DW);
82             my @X_A4_ON_A3 = ($AH,$AH,0,0,$AH,$AH,0,0,$AH,$AH,0,0,$AH,$AH,0,0);
83             my @Y_A4_ON_A3 = ($AW,0,$JH,$AW,$AW,0,$JH,$AW,$AW,0,$JH,$AW,$AW,0,$JH,$AW);
84              
85             my ( $IN_FILE, $OUT_FILE );
86              
87              
88             ##########################################################
89             sub main {
90             ##########################################################
91 0     0 0   my $input = $ARGV[0];
92 0           my ($inpPgNum, $inpPgSize);
93 0           my $numPagImposed = 0;
94 0           my $sayUsage = "Usage: paperback file.pdf (will produce 'file-paperback.pdf').";
95 0           my $sayVersion = "This is paperback v${VERSION}, (c) 2022 Hector M. Monacci.";
96 0           my $sayHelp = <<"END_MESSAGE";
97              
98             ${sayUsage}
99              
100             All pages in the input PDF file will be imposed on a new PDF with
101             bigger paper size, ready to be duplex-printed, folded and put together
102             into signatures, according to its original page size. Input PDF is
103             always assumed to be composed of vertical pages of the same size.
104              
105             Input page sizes allowed are A4, A5, A6, Letter, Half Letter, Quarter
106             Letter, Half Legal and Quarter Legal. Other sizes give an error message.
107              
108             Only PDF v1.4 is supported as input, although many higher-labeled
109             PDF files are correctly handled since they are essentially v1.4 PDF
110             files stamped as something more modern.
111              
112             ISO 216 normalised (international) page sizes:
113              
114             Input page sizes A6 (105 x 148 mm) and A5 (148 x 210 mm) produce an
115             output page size of A4 (210 x 297 mm). Input page size A4 (210 x 297 mm)
116             produces an output page size of A3 (297 x 420 mm). Four A6 pages will
117             be put on each A4 page, two A5 pages will be put on each A4 page, or
118             two A4 pages will be put on each A3 page. Before that, input pages will
119             be reordered and reoriented so as to produce a final PDF fit for duplex
120             'long-edge-flip' printing.
121              
122             ANSI normalised (US) page sizes:
123              
124             Input page sizes Quarter Letter (4.25 x 5.5 in) and Half Letter (5.5
125             x 8.5 in) produce a Letter output page size (8.5 x 11 in). Input
126             page sizes Quarter Legal (4.25 x 7 in) and Half Legal (7 x 8.5 in)
127             produce a Legal output page size (8.5 x 14 in). Input page size Letter
128             (8.5 x 11 in) produces a Tabloid output page size (11 x 17 in).
129              
130             Four Quarter-Letter pages will be put on each Letter page, two Half-Letter
131             pages will be put on each Letter page, four Quarter-Legal pages will be
132             put on each Legal page, two Half-Legal pages will be put on each Legal page,
133             or two Letter pages will be put on each Tabloid page. Before that, input
134             pages will be reordered and reoriented so as to produce a final PDF fit for
135             duplex 'long-edge-flip' printing.
136              
137             For further details, please try 'perldoc paperback'.
138              
139             ${sayVersion}
140             END_MESSAGE
141              
142 0 0         die "[!] ${sayUsage}\n"
143             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 0         die "[!] File '${input}' can't be found or read.\n"
149             unless -r $input;
150 0           ($inpPgNum, $inpPgSize) = openInputFile($input);
151 0 0         die "[!] File '${input}' is not a valid v1.4 PDF file.\n"
152             if $inpPgNum == 0;
153              
154 0           my ($pgPerOutputPage, @x, @y);
155 0           for ($inpPgSize) {
156 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          
157 0           elsif ($_ eq "A5") { $pgPerOutputPage = 2; @x = @X_A5_ON_A4; @y = @Y_A5_ON_A4; }
  0            
  0            
158 0           elsif ($_ eq "QT") { $pgPerOutputPage = 4; @x = @X_QT_ON_LT; @y = @Y_QT_ON_LT; }
  0            
  0            
159 0           elsif ($_ eq "QG") { $pgPerOutputPage = 4; @x = @X_QG_ON_LG; @y = @Y_QG_ON_LG; }
  0            
  0            
160 0           elsif ($_ eq "HT") { $pgPerOutputPage = 2; @x = @X_HT_ON_LT; @y = @Y_HT_ON_LT; }
  0            
  0            
161 0           elsif ($_ eq "HG") { $pgPerOutputPage = 2; @x = @X_HG_ON_LG; @y = @Y_HG_ON_LG; }
  0            
  0            
162 0           elsif ($_ eq "LT") { $pgPerOutputPage = 2; @x = @X_LT_ON_TA; @y = @Y_LT_ON_TA; }
  0            
  0            
163 0           elsif ($_ eq "A4") { $pgPerOutputPage = 2; @x = @X_A4_ON_A3; @y = @Y_A4_ON_A3; }
  0            
  0            
164 0           else {die "[!] Bad page size (${inpPgSize}). Try 'paperback -h' for more info.\n"}
165             }
166              
167 0           my ($name) = $input =~ /(.+)\.[^.]+$/;
168 0           openOutputFile("${name}-paperback.pdf");
169 0           my ($rot_extra, @p);
170 0 0         if ($pgPerOutputPage == 4) {
171 0           $rot_extra = 0;
172 0           for ($inpPgNum) {
173 0 0         if ($_ >= 13) { @p = @P_4UP_13PLUS; }
  0 0          
    0          
174 0           elsif ($_ >= 9 ) { @p = @P_4UP_9PLUS; }
175 0           elsif ($_ >= 5 ) { @p = @P_4UP_5PLUS; }
176 0           else { @p = @P_4UP_1PLUS; }
177             }
178             } else {
179 0           $rot_extra = 90;
180 0           for ($inpPgNum) {
181 0 0         if ($_ >= 13) { @p = @P_2UP_13PLUS; }
  0 0          
    0          
182 0           elsif ($_ >= 9 ) { @p = @P_2UP_9PLUS; }
183 0           elsif ($_ >= 5 ) { @p = @P_2UP_5PLUS; }
184 0           else { @p = @P_2UP_1PLUS; }
185             }
186             }
187 0           my $lastSignature = $inpPgNum >> 4;
188 0           my ($rotation, $target_page);
189 0           for (my $thisSignature = 0; $thisSignature <= $lastSignature; ++$thisSignature) {
190 0           for (0..15) {
191 0 0         newPageInOutputFile() if $_ % $pgPerOutputPage == 0;
192 0           $target_page = $p[$_] + 16 * $thisSignature;
193 0 0         next if $target_page > $inpPgNum;
194              
195 0 0         $rotation = $_ % 4 > 1 ? $rot_extra + 180 : $rot_extra;
196 0           copyPageFromInputToOutput ({page => $target_page,
197             rotate => $rotation, x => $x[$_], y => $y[$_]});
198 0           ++$numPagImposed;
199 0 0         last if $numPagImposed == $inpPgNum;
200             }
201             }
202 0           closeInputFile();
203 0           closeOutputFile();
204             }
205              
206             main() if not caller();
207              
208              
209             ##########################################################
210             sub newPageInOutputFile {
211             ##########################################################
212 0 0   0 0   die "[!] No output file, you must call openOutputFile first.\n" if !$Gpos;
213 0 0         writePage() if $Gstream;
214              
215 0           ++$GobjNr;
216 0           $GpageObjNr = $GobjNr;
217 0           undef %GpageXObject;
218              
219 0           return;
220             }
221              
222              
223             ##########################################################
224             sub copyPageFromInputToOutput {
225             ##########################################################
226 0 0   0 0   die "[!] No output file, you have to call openOutputFile first.\n" if !$Gpos;
227 0           my $param = $_[0];
228 0 0         my $pagenumber = $param->{'page'} or 1;
229 0 0         my $x = $param->{'x'} or 0;
230 0 0         my $y = $param->{'y'} or 0;
231 0 0         my $rotate = $param->{'rotate'} or 0;
232              
233 0           state $formNr; # Este uso de "state" requiere v5.10 (que salió en 2007)
234 0           ++$formNr;
235              
236 0           my $name = "Fm${formNr}";
237 0           my $refNr = getPage( $pagenumber );
238 0 0         die "[!] Page ${pagenumber} in ${GinFile} can't be used. Concatenate streams!\n"
239             if !defined $refNr;
240 0 0         die "[!] Page ${pagenumber} doesn't exist in file ${GinFile}.\n" if !$refNr;
241              
242 0           $Gstream .= "q\n" . calcRotateMatrix($x, $y, $rotate) ."\n/Gs0 gs\n/${name} Do\nQ\n";
243 0           $GpageXObject{$name} = $refNr;
244              
245 0           return;
246             }
247              
248              
249             ##########################################################
250             sub setInitGrState {
251             ##########################################################
252 0     0 0   ++$GobjNr;
253              
254 0           $Gobject[$GobjNr] = $Gpos;
255 0           $Gpos += syswrite $OUT_FILE,
256             "${GobjNr} 0 obj<>endobj\n";
257 0           return;
258             }
259              
260              
261             ##########################################################
262             sub createPageResourceDict {
263             ##########################################################
264 0     0 0   my $resourceDict = "/ProcSet[/PDF/Text]";
265 0 0         if ( %GpageXObject ) {
266 0           $resourceDict .= "/XObject<<";
267 0           $resourceDict .= "/$_ $GpageXObject{$_} 0 R" for sort keys %GpageXObject;
268 0           $resourceDict .= ">>";
269             }
270 0           $resourceDict .= "/ExtGState<>";
271 0           return $resourceDict;
272             }
273              
274              
275             ##########################################################
276             sub writePageResourceDict {
277             ##########################################################
278 0     0 0   my $resourceDict = $_[0];
279              
280 0           state %resources;
281              
282             # Found one identical, use it:
283 0 0         return $resources{$resourceDict} if exists $resources{$resourceDict};
284 0           ++$GobjNr;
285             # Save first 10 resources:
286 0 0         $resources{$resourceDict} = $GobjNr if keys(%resources) < 10;
287 0           $Gobject[$GobjNr] = $Gpos;
288 0           $resourceDict = "${GobjNr} 0 obj<<${resourceDict}>>endobj\n";
289 0           $Gpos += syswrite $OUT_FILE, $resourceDict;
290 0           return $GobjNr;
291             }
292              
293              
294             ##########################################################
295             sub writePageStream {
296             ##########################################################
297 0     0 0   ++$GobjNr;
298 0           $Gobject[$GobjNr] = $Gpos;
299 0           $Gpos += syswrite $OUT_FILE, "${GobjNr} 0 obj<
300             . ">>stream\n${Gstream}\nendstream\nendobj\n";
301 0           $Gobject[$GpageObjNr] = $Gpos;
302 0           $Gstream = "";
303 0           return;
304             }
305              
306              
307             ##########################################################
308             sub writePageResources {
309             ##########################################################
310 0     0 0   my ($parent, $resourceObject) = ($_[0], $_[1]);
311 0           $Gpos += syswrite $OUT_FILE, "${GpageObjNr} 0 obj<
312             . "R/Contents ${GobjNr} 0 R/Resources ${resourceObject} 0 R>>endobj\n";
313 0           push @{ $Gkids[0] }, $GpageObjNr;
  0            
314 0           return;
315             }
316              
317              
318             ##########################################################
319             sub writePage {
320             ##########################################################
321 0 0   0 0   if ( !$Gparents[0] ) {
322 0           ++$GobjNr;
323 0           $Gparents[0] = $GobjNr;
324             }
325 0           my $parent = $Gparents[0];
326 0           my $resourceObject = writePageResourceDict(createPageResourceDict());
327 0           writePageStream();
328 0           writePageResources($parent, $resourceObject);
329 0           ++$Gcounts[0];
330 0 0         writePageNodes(8) if $Gcounts[0] > 9;
331 0           return;
332             }
333              
334              
335             ##########################################################
336             sub closeOutputFile {
337             ##########################################################
338 0 0   0 0   return if !$Gpos;
339              
340 0 0         writePage() if $Gstream;
341 0           my $endNode = writeEndNode();
342              
343 0           my $out_line = "1 0 obj<>endobj\n";
344 0           $Gobject[1] = $Gpos;
345 0           $Gpos += syswrite $OUT_FILE, $out_line;
346 0           my $qty = $#Gobject;
347 0           my $startxref = $Gpos;
348 0           my $xrefQty = $qty + 1;
349 0           $out_line = "xref\n0 ${xrefQty}\n0000000000 65535 f \n";
350 0           $out_line .= sprintf "%.10d 00000 n \n", $_ for @Gobject[1..$qty];
351 0           $out_line .= "trailer\n<<\n/Size ${xrefQty}\n/Root 1 0 R\n"
352             . ">>\nstartxref\n${startxref}\n%%EOF\n";
353              
354 0           syswrite $OUT_FILE, $out_line;
355 0           close $OUT_FILE;
356              
357 0           $Gpos = 0;
358 0           return;
359             }
360              
361              
362             ##########################################################
363             sub writePageNodes {
364             ##########################################################
365 0     0 0   my $qtyChildren = $_[0];
366 0           my $i = 0;
367 0           my $j = 1;
368 0           my $nodeObj;
369              
370 0           while ( $qtyChildren < $#{ $Gkids[$i] } ) {
  0            
371             # Imprimir padre actual y pasar al siguiente nivel:
372 0 0         if ( !$Gparents[$j] ) {
373 0           ++$GobjNr;
374 0           $Gparents[$j] = $GobjNr;
375             }
376              
377             $nodeObj =
378 0           "${Gparents[$i]} 0 obj<
379 0           $nodeObj .= "${_} 0 R " for @{ $Gkids[$i] };
  0            
380 0           $nodeObj .= "]\n/Count ${Gcounts[$i]}>>endobj\n";
381 0           $Gobject[ $Gparents[$i] ] = $Gpos;
382 0           $Gpos += syswrite $OUT_FILE, $nodeObj;
383              
384 0           $Gcounts[$j] += $Gcounts[$i];
385 0           $Gcounts[$i] = 0;
386 0           $Gkids[$i] = [];
387 0           push @{ $Gkids[$j] }, $Gparents[$i];
  0            
388 0           undef $Gparents[$i];
389 0           ++$i;
390 0           ++$j;
391             }
392 0           return;
393             }
394              
395              
396             ##########################################################
397             sub writeEndNode {
398             ##########################################################
399 0     0 0   my $nodeObj;
400 0           my $endNode = $Gparents[-1]; # content of the last element
401 0           my $si = $#Gparents; # index of the last element
402              
403 0 0         my $min = defined $Gparents[0] ? 0 : 1;
404 0           for ( my $i = $min ; $Gparents[$i] ne $endNode ; ++$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 $rotate = $_[2];
438 0           my $str = "1 0 0 1 ${_[0]} ${_[1]} cm\n";
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 getObjectContent {
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, $buf2);
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 writeToBeCreated {
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 = getObjectContent($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\s*)'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 = getObjectContent($GrootNr);
590            
591             # Find pages:
592 0 0         return "unknown" unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
593 0           $objectContent = getObjectContent($1);
594 0 0         $objectContent = xformObjForThisPage($objectContent, 1)
595             unless $objectContent =~ m'MediaBox's;
596 0           (undef, undef) = getPageResources( $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 = $DW; $GoHei = $DH; 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             }
615 0           return "unknown, ${measuresInMm}";
616             }
617              
618              
619             ##########################################################
620             sub alike {
621             ##########################################################
622 0     0 0   my $hxw = $_[0]; my $namedHxw = $_[1];
  0            
623 0           my $tolerance = 1500;
624 0 0 0       return 0 if $hxw > $namedHxw + $tolerance or $hxw < $namedHxw - $tolerance;
625 0           return 1;
626             }
627              
628              
629             ##########################################################
630             sub getPage {
631             ##########################################################
632 0     0 0   my $pagenumber = $_[0];
633 0           my ( $reference, $formRes, $formCont );
634              
635             # Find root:
636 0           my $objectContent = getObjectContent($GrootNr);
637              
638             # Find pages:
639 0 0         die "[!] Didn't find Pages section in '${GinFile}', aborting.\n"
640             unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
641 0           $objectContent = getObjectContent($1);
642 0           $objectContent = xformObjForThisPage($objectContent, $pagenumber);
643 0           ($formRes, $formCont) = getPageResources( $objectContent );
644              
645 0           $reference = writeRes($formRes, $formCont);
646              
647 0           writeToBeCreated();
648              
649 0           return $reference;
650             }
651              
652              
653             ##########################################################
654             sub writeRes {
655             ##########################################################
656 0     0 0   my ($formRes, $objNr) = ($_[0], $_[1]);
657              
658 0           my $objectContent = getObjectContent($objNr);
659 0           $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
660 0           my $strPos = length($1) + length($2) + length($3);
661 0           my $newPart = "<
662             . "/BBox [@{GmediaBox}] ${2}";
663              
664 0           ++$GobjNr;
665 0           $Gobject[$GobjNr] = $Gpos;
666 0           my $reference = $GobjNr;
667 0           update_references_and_populate_to_be_created($newPart);
668 0           my $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
669 0           $out_line .= substr( $objectContent, $strPos );
670 0           $Gpos += syswrite $OUT_FILE, $out_line;
671 0           return $reference;
672             }
673              
674              
675             ##########################################################
676             sub xformObjForThisPage {
677             ##########################################################
678 0     0 0   my ($objectContent, $pagenumber) = ($_[0], $_[1]);
679 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
680              
681 0 0         return 0 unless $objectContent =~ m'/Kids\s*\[([^\]]+)'s;
682 0           $vector = $1;
683              
684 0           $pageAccumulator = 0;
685              
686 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
687 0           while ( $pageAccumulator < $pagenumber ) {
688 0           @pageObjBackup = @pageObj;
689 0           undef @pageObj;
690 0           for (@pageObjBackup) {
691 0           $objectContent = getObjectContent($_);
692 0 0         if ( $objectContent =~ m'/Count\s+(\d+)'s ) {
693 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
694 0           $pageAccumulator += $1;
695             } else {
696 0 0         $vector = $1 if $objectContent =~ m'/Kids\s*\[([^\]]+)'s ;
697 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
698 0           last;
699             }
700             } else {
701 0           ++$pageAccumulator;
702             }
703 0 0         last if $pageAccumulator == $pagenumber;
704             }
705             }
706 0           return $objectContent;
707             }
708              
709              
710             ##########################################################
711             sub getPageResources {
712             ##########################################################
713 0     0 0   my $objContent = $_[0];
714 0           my ($resources, $formCont);
715              
716             # Assume all input PDF pages have the same dimensions as first MediaBox found:
717 0 0         if (! @GmediaBox) {
718 0           for ($objContent) {
719 0 0         if (m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]'s) {
    0          
720 0           @GmediaBox = ($1, $2, $3, $4);
721             } elsif (m'MediaBox\s*(\d+)\s+\d+\s+R\b's) { # Size to be found in reference
722 0           my $ref = getObjectContent($1);
723 0 0         @GmediaBox = ($1, $2, $3, $4)
724             if ($ref =~ m'\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]'s);
725             }
726             }
727             }
728              
729 0 0         if ( $objContent =~ m'/Contents\s+(\d+)'s ) {
    0          
730 0           $formCont = $1;
731             } elsif ( $objContent =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]'s ) {
732 0           $formCont = $1;
733             }
734              
735 0           $resources = getResourcesFromObj($objContent);
736              
737 0           return ($resources, $formCont);
738             }
739              
740              
741             ##########################################################
742             sub getResourcesFromObj {
743             ##########################################################
744 0     0 0   my $objContent = $_[0];
745 0           my $resources;
746              
747 0 0         if ( $objContent =~ m'^(.+/Resources)'s ) {
748 0 0         return $1 if $objContent =~ m'Resources\s+(\d+\s+\d+\s+R)'s; # Reference (95%)
749             # The resources are a dictionary. The whole is copied (morfologia.pdf):
750 0           my $k;
751 0           ( undef, $objContent ) = split /\/Resources/, $objContent;
752 0           $objContent =~ s/<
753 0           $objContent =~ s/>>/>>#/gs;
754 0           for ( split /#/, $objContent ) {
755 0 0         if ( m'\S's ) {
756 0           $resources .= $_;
757 0 0         ++$k if m'<<'s;
758 0 0         --$k if m'>>'s;
759 0 0         last if $k == 0;
760             }
761             }
762             }
763 0           return $resources;
764             }
765              
766              
767             ##########################################################
768             sub getInputPageCount {
769             ##########################################################
770 0     0 0   my ($objectContent, $inputPageCount);
771              
772 0 0         return 0 unless eval { $objectContent = getObjectContent($GrootNr); 1; };
  0            
  0            
773 0 0         if ( $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's ) {
774 0           $objectContent = getObjectContent($1);
775 0 0         return $1 if $objectContent =~ m'/Count\s+(\d+)'s;
776             }
777 0           return 0;
778             }
779              
780              
781             ##########################################################
782             sub openInputFile {
783             ##########################################################
784 0     0 0   $GinFile = $_[0];
785 0           my ( $objectContent, $inputPageSize, $inputPageCount, $c );
786              
787 0 0         open( $IN_FILE, q{<}, $GinFile )
788             or die "[!] Couldn't open '${GinFile}'.\n";
789 0           binmode $IN_FILE;
790              
791 0           sysread $IN_FILE, $c, 5;
792 0 0         return 0 if $c ne "%PDF-";
793              
794             # Find root
795 0           $GrootNr = getRootAndMapGobjects();
796 0 0         return 0 unless $GrootNr > 0;
797              
798 0           $inputPageSize = getInputPageDimensions();
799 0           $inputPageCount = getInputPageCount();
800 0           return ($inputPageCount, $inputPageSize);
801             }
802              
803              
804             ##########################################################
805             sub addSizeToGObjects {
806             ##########################################################
807 0     0 0   my $size = (stat($GinFile))[7]; # stat[7] = filesize
808             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
809             # according to their offset in the file: last first
810 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
811 0           my $pos;
812              
813 0           for (@offset) {
814 0           $pos = $GObjects{$_};
815 0           $size -= $pos;
816 0           $GObjects{$_} = [ $pos, $size ];
817 0           $size = $pos;
818             }
819             }
820              
821              
822             ##########################################################
823             sub update_references_and_populate_to_be_created {
824             ##########################################################
825             # $xform translates an old object reference to a new one
826             # and populates a table with what must be created
827 0     0 0   state %known;
828             my $xform = sub {
829 0 0   0     return $known{$1} if exists $known{$1};
830 0           push @Gto_be_created, [ $1, ++$GobjNr ];
831 0           return $known{$1} = $GobjNr;
832 0           };
833 0           $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&$xform . ' 0 R'/eg;
  0            
834 0           return;
835             }
836              
837              
838             ##########################################################
839             sub extractXrefSection {
840             ##########################################################
841 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
842              
843 0           sysread $IN_FILE, $c, 1;
844 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s's;
845 0           while ( $c =~ /[\d ]/ ) {
846 0           $readBytes .= $c;
847 0           sysread $IN_FILE, $c, 1;
848             }
849 0 0         ($idx, $qty) = ($1, $2) if $readBytes =~ m'^(\d+)\s+(\d+)';
850 0           return ($qty, $idx);
851             }
852              
853              
854             ##########################################################
855             sub openOutputFile {
856             ##########################################################
857 0 0   0 0   closeOutputFile() if $Gpos;
858              
859 0           my $outputfile = $_[0];
860 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep it far from file beginning!
861              
862 0 0         open( $OUT_FILE, q{>}, $outputfile )
863             or die "[!] Couldn't open file '${outputfile}'.\n";
864 0           binmode $OUT_FILE;
865 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
866              
867 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
868 0           $Gparents[0] = 2;
869              
870 0           setInitGrState();
871 0           return;
872             }
873              
874              
875             ##########################################################
876             sub closeInputFile {
877             ##########################################################
878 0     0 0   close $IN_FILE;
879             }
880              
881             1;
882              
883             __END__