File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 455 1.1
branch 0 196 0.0
condition 0 12 0.0
subroutine 2 36 5.5
pod 0 33 0.0
total 7 732 0.9


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