File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 447 1.1
branch 0 190 0.0
condition 0 36 0.0
subroutine 2 34 5.8
pod 0 31 0.0
total 7 738 0.9


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   74392 use v5.10;
  1         3  
4 1     1   4 use strict;
  1         2  
  1         4973  
5             # use warnings;
6             our $VERSION = "1.16";
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 0           $objectContent = getObjectContent($1);
568              
569 0           $objectContent = xformObjForThisPage($objectContent, 1);
570 0           (undef, undef) = getPageResources( $objectContent );
571 0 0 0       return 0 if ! defined $GformBox[2] or ! defined $GformBox[3];
572 0           my $multi = int($GformBox[2]) * int($GformBox[3]);
573 0           my $measuresInMm = int($GformBox[2] / 72 * 25.4) . " x "
574             . int($GformBox[3] / 72 * 25.4) . " mm";
575              
576 0           for ($multi) {
577             # US 1/4 letter: 120780
578 0 0 0       if ($_ > 119_780 and $_ < 121_780) {$GoWid = $DW; $GoHei = $DH; return "QT";}
  0            
  0            
  0            
579             # ISO A6: 124443
580 0 0 0       if ($_ > 123_443 and $_ < 125_443) {$GoWid = $AW; $GoHei = $AH; return "A6";}
  0            
  0            
  0            
581             # US 1/4 legal: 153720
582 0 0 0       if ($_ > 152_720 and $_ < 154_720) {$GoWid = $GW; $GoHei = $GH; return "QG";}
  0            
  0            
  0            
583             # US 1/2 Letter ("statement"): 242352
584 0 0 0       if ($_ > 241_352 and $_ < 243_352) {$GoWid = $DW; $GoHei = $DH; return "HT";}
  0            
  0            
  0            
585             # ISO A5: 249305
586 0 0 0       if ($_ > 248_305 and $_ < 250_305) {$GoWid = $AW; $GoHei = $AH; return "A5";}
  0            
  0            
  0            
587             # US 1/2 legal: 308448
588 0 0 0       if ($_ > 307_448 and $_ < 309_448) {$GoWid = $GW; $GoHei = $GH; return "HG";}
  0            
  0            
  0            
589             # US letter: 484704
590 0 0 0       if ($_ > 483_704 and $_ < 485_704) {return "USletter, ${measuresInMm}"; }
  0            
591             # ISO A4: 500395
592 0 0 0       if ($_ > 499_395 and $_ < 501_395) {return "A4, ${measuresInMm}"; }
  0            
593             # US legal: 616896
594 0 0 0       if ($_ > 615_896 and $_ < 617_896) {return "USlegal, ${measuresInMm}";}
  0            
595             }
596 0           return "unknown, ${measuresInMm}";
597             }
598              
599              
600             ##########################################################
601             sub getPage {
602             ##########################################################
603 0     0 0   my $pagenumber = $_[0];
604 0           my ( $reference, $formRes, $formCont );
605              
606             # Find root:
607 0           my $objectContent = getObjectContent($GrootNr);
608              
609             # Find pages:
610 0 0         die "[!] Didn't find Pages section in '${GinFile}', aborting.\n"
611             unless $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
612 0           $objectContent = getObjectContent($1);
613              
614 0           $objectContent = xformObjForThisPage($objectContent, $pagenumber);
615 0           ($formRes, $formCont) = getPageResources( $objectContent );
616              
617 0           $reference = writeRes($formRes, $formCont);
618              
619 0           writeToBeCreated();
620              
621 0           return $reference;
622             }
623              
624              
625             ##########################################################
626             sub writeRes {
627             ##########################################################
628 0     0 0   my ($formRes, $objNr) = ($_[0], $_[1]);
629              
630 0           my $objectContent = getObjectContent($objNr);
631 0           $objectContent =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
632 0           my $strPos = length($1) + length($2) + length($3);
633 0           my $newPart = "<
634             . "/BBox [@{GformBox}] ${2}";
635              
636 0           ++$GobjNr;
637 0           $Gobject[$GobjNr] = $Gpos;
638 0           my $reference = $GobjNr;
639 0           update_references_and_populate_to_be_created($newPart);
640 0           my $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
641 0           $out_line .= substr( $objectContent, $strPos );
642 0           $Gpos += syswrite $OUT_FILE, $out_line;
643 0           return $reference;
644             }
645              
646              
647             ##########################################################
648             sub xformObjForThisPage {
649             ##########################################################
650 0     0 0   my ($objectContent, $pagenumber) = ($_[0], $_[1]);
651 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
652              
653 0 0         return 0 unless $objectContent =~ m'/Kids\s*\[([^\]]+)'s;
654 0           $vector = $1;
655              
656 0           $pageAccumulator = 0;
657              
658 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
659 0           while ( $pageAccumulator < $pagenumber ) {
660 0           @pageObjBackup = @pageObj;
661 0           undef @pageObj;
662 0           for (@pageObjBackup) {
663 0           $objectContent = getObjectContent($_);
664 0 0         if ( $objectContent =~ m'/Count\s+(\d+)'s ) {
665 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
666 0           $pageAccumulator += $1;
667             } else {
668 0 0         $vector = $1 if $objectContent =~ m'/Kids\s*\[([^\]]+)'s ;
669 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
670 0           last;
671             }
672             } else {
673 0           ++$pageAccumulator;
674             }
675 0 0         last if $pageAccumulator == $pagenumber;
676             }
677             }
678 0           return $objectContent;
679             }
680              
681              
682             ##########################################################
683             sub getPageResources {
684             ##########################################################
685 0     0 0   my $objContent = $_[0];
686 0           my ($resources, $formCont);
687              
688             # Assume all input PDF pages have the same dimensions as first MediaBox found:
689 0 0         if (! @GformBox) {
690 0 0         if ($objContent =~ m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]'s) {
691 0           @GformBox = ($1, $2, $3, $4);
692             }
693             }
694              
695 0 0         if ( $objContent =~ m'/Contents\s+(\d+)'s ) {
    0          
696 0           $formCont = $1;
697             } elsif ( $objContent =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]'s ) {
698 0           $formCont = $1;
699             }
700              
701 0           $resources = getResourcesFromObj($objContent);
702              
703 0           return ($resources, $formCont);
704             }
705              
706              
707             ##########################################################
708             sub getResourcesFromObj {
709             ##########################################################
710 0     0 0   my $objContent = $_[0];
711 0           my $resources;
712              
713 0 0         if ( $objContent =~ m'^(.+/Resources)'s ) {
714 0 0         return $1 if $objContent =~ m'Resources\s+(\d+\s+\d+\s+R)'s; # Reference (95%)
715             # The resources are a dictionary. The whole is copied (morfologia.pdf):
716 0           my $k;
717 0           ( undef, $objContent ) = split /\/Resources/, $objContent;
718 0           $objContent =~ s/<
719 0           $objContent =~ s/>>/>>#/gs;
720 0           for ( split /#/, $objContent ) {
721 0 0         if ( m'\S's ) {
722 0           $resources .= $_;
723 0 0         ++$k if m'<<'s;
724 0 0         --$k if m'>>'s;
725 0 0         last if $k == 0;
726             }
727             }
728             }
729 0           return $resources;
730             }
731              
732              
733             ##########################################################
734             sub openInputFile {
735             ##########################################################
736 0     0 0   $GinFile = $_[0];
737 0           my ( $objectContent, $inputPageSize, $c );
738              
739 0 0         open( $IN_FILE, q{<}, $GinFile )
740             or die "[!] Couldn't open '${GinFile}'.\n";
741 0           binmode $IN_FILE;
742              
743 0           sysread $IN_FILE, $c, 5;
744 0 0         return 0 if $c ne "%PDF-";
745              
746             # Find root
747 0           $GrootNr = getRootAndMapGobjects();
748 0 0         return 0 unless $GrootNr > 0;
749             # Find input page size:
750 0           $inputPageSize = getInputPageDimensions();
751              
752             # Find pages
753 0 0         return 0 unless eval { $objectContent = getObjectContent($GrootNr); 1; };
  0            
  0            
754              
755 0 0         if ( $objectContent =~ m'/Pages\s+(\d+)\s+\d+\s+R's ) {
756 0           $objectContent = getObjectContent($1);
757 0 0         return ($1, $inputPageSize) if $objectContent =~ m'/Count\s+(\d+)'s;
758             }
759 0           return 0;
760             }
761              
762              
763             ##########################################################
764             sub addSizeToGObjects {
765             ##########################################################
766 0     0 0   my $size = (stat($GinFile))[7]; # stat[7] = filesize
767             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
768             # according to their offset in the file: last first
769 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
770 0           my $pos;
771              
772 0           for (@offset) {
773 0           $pos = $GObjects{$_};
774 0           $size -= $pos;
775 0           $GObjects{$_} = [ $pos, $size ];
776 0           $size = $pos;
777             }
778             }
779              
780              
781             ##########################################################
782             sub update_references_and_populate_to_be_created {
783             ##########################################################
784             # $xform translates an old object number to a new one
785             # and populates a table with what must be created
786 0     0 0   state %known;
787             my $xform = sub {
788 0 0   0     return $known{$1} if exists $known{$1};
789 0           push @Gto_be_created, [ $1, ++$GobjNr ];
790 0           return $known{$1} = $GobjNr;
791 0           };
792 0           $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&$xform . ' 0 R'/eg;
  0            
793 0           return;
794             }
795              
796              
797             ##########################################################
798             sub extractXrefSection {
799             ##########################################################
800 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
801              
802 0           sysread $IN_FILE, $c, 1;
803 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s's;
804 0           while ( $c =~ /[\d ]/ ) {
805 0           $readBytes .= $c;
806 0           sysread $IN_FILE, $c, 1;
807             }
808 0 0         ($idx, $qty) = ($1, $2) if $readBytes =~ m'^(\d+)\s+(\d+)';
809 0           return ($qty, $idx);
810             }
811              
812              
813             ##########################################################
814             sub openOutputFile {
815             ##########################################################
816 0 0   0 0   closeOutputFile() if $Gpos;
817              
818 0           my $outputfile = $_[0];
819 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep it far from file beginning!
820              
821 0 0         open( $OUT_FILE, q{>}, $outputfile )
822             or die "[!] Couldn't open file '${outputfile}'.\n";
823 0           binmode $OUT_FILE;
824 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
825              
826 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
827 0           $Gparents[0] = 2;
828              
829 0           setInitGrState();
830 0           return;
831             }
832              
833              
834             ##########################################################
835             sub closeInputFile {
836             ##########################################################
837 0     0 0   close $IN_FILE;
838             }
839              
840             1;
841              
842             __END__