File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 453 1.1
branch 0 196 0.0
condition 0 36 0.0
subroutine 2 35 5.7
pod 0 32 0.0
total 7 752 0.9


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