File Coverage

blib/lib/App/paperback.pm
Criterion Covered Total %
statement 5 444 1.1
branch 0 188 0.0
condition 0 36 0.0
subroutine 2 33 6.0
pod 0 30 0.0
total 7 731 0.9


line stmt bran cond sub pod time code
1             package App::paperback;
2              
3 1     1   57163 use v5.10;
  1         4  
4 1     1   5 use strict;
  1         1  
  1         5041  
5             # use warnings;
6             our $VERSION = "1.15";
7              
8             my ($GinFile, $GpageObjNr, $Groot, $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 $pgSizeInput;
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, $pgSizeInput) = 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 ($pgSizeInput) {
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 (${pgSizeInput}). 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           $tempRoot = getRootFromXrefSection();
456 0 0         return 0 unless $tempRoot; # No Root object in ${GinFile}, aborting
457 0           return $tempRoot;
458             }
459              
460              
461             ##########################################################
462             sub populateGobjects {
463             ##########################################################
464 0     0 0   my $xref = $_[0];
465 0           my ( $idx, $qty, $readBytes );
466              
467 0           sysseek $IN_FILE, $xref, 0;
468 0           sysread $IN_FILE, $readBytes, 22;
469              
470 0 0         die "[!] File '${GinFile}' uses xref streams (not a v1.4 PDF file).\n"
471             if $readBytes =~ m/^\d+\s+\d+\s+obj/i; # Road to manage v1.5+ PDF files!
472 0 0         die "[!] File '${GinFile}' has a malformed xref table.\n"
473             unless $readBytes =~ m/^(xref$cr)/i;
474 0           my $extra = length($1);
475              
476 0           sysseek $IN_FILE, $xref += $extra, 0;
477 0           ($qty, $idx) = extractXrefSection();
478              
479 0           while ($qty) {
480 0           for (1..$qty) {
481 0           sysread $IN_FILE, $readBytes, 20;
482 0 0         $GObjects{$idx} = $1 if $readBytes =~ m'^\s?(\d{10}) \d{5} n';
483 0           ++$idx;
484             }
485 0           ($qty, $idx) = extractXrefSection();
486             }
487              
488 0           addSizeToGObjects();
489 0           return;
490             }
491              
492              
493             ##########################################################
494             sub getRootFromXrefSection {
495             ##########################################################
496 0     0 0   my $readBytes = " ";
497 0           my $buf;
498 0           while ($readBytes) {
499 0           sysread $IN_FILE, $readBytes, 200;
500 0           $buf .= $readBytes;
501 0 0         return $1 if $buf =~ m'\/Root\s+(\d+)\s+\d+\s+R's;
502             }
503 0           return;
504             }
505              
506              
507             ##########################################################
508             sub getObject {
509             ##########################################################
510 0     0 0   my $index = $_[0];
511              
512 0 0         return 0 if (! defined $GObjects{$index}); # A non-1.4 PDF
513 0           my $buf;
514 0           my ( $offs, $size ) = @{ $GObjects{$index} };
  0            
515              
516 0           sysseek $IN_FILE, $offs, 0;
517 0           sysread $IN_FILE, $buf, $size;
518              
519 0           return $buf;
520             }
521              
522              
523             ##########################################################
524             sub writeToBeCreated {
525             ##########################################################
526 0     0 0   my ($elObje, $out_line, $part, $strPos, $old_one, $new_one);
527              
528 0           for (@Gto_be_created) {
529 0           $old_one = $_->[0];
530 0           $new_one = $_->[1];
531 0           $elObje = getObject($old_one);
532 0 0         if ( $elObje =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s ) {
533 0           $part = $2;
534 0           $strPos = length($1) + length($2) + length($3);
535 0           update_references_and_populate_to_be_created($part);
536 0           $out_line = "${new_one} 0 obj\n<<${part}>>stream";
537 0           $out_line .= substr( $elObje, $strPos );
538             } else {
539 0 0         $elObje = substr( $elObje, length($1) ) if $elObje =~ m'^(\d+ \d+ obj\s*)'s;
540 0           update_references_and_populate_to_be_created($elObje);
541 0           $out_line = "${new_one} 0 obj ${elObje}";
542             }
543 0           $Gobject[$new_one] = $Gpos;
544 0           $Gpos += syswrite $OUT_FILE, $out_line;
545             }
546 0           undef @Gto_be_created;
547 0           return;
548             }
549              
550              
551             ##########################################################
552             sub getInputPageDimensions {
553             ##########################################################
554             # Find root:
555 0     0 0   my $elObje = getObject($Groot);
556              
557             # Find pages:
558 0 0         return "unknown" unless $elObje =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
559 0           $elObje = getObject($1);
560              
561 0           $elObje = xformObjForThisPage($elObje, 1);
562 0           (undef, undef) = getResources( $elObje );
563 0 0 0       return 0 if ! defined $GformBox[2] or ! defined $GformBox[3];
564 0           my $multi = int($GformBox[2]) * int($GformBox[3]);
565 0           my $measuresInMm = int($GformBox[2] / 72 * 25.4) . " x "
566             . int($GformBox[3] / 72 * 25.4) . " mm";
567              
568 0           for ($multi) {
569             # US 1/4 letter: 120780
570 0 0 0       if ($_ > 119_780 and $_ < 121_780) {$GoWid = $DW; $GoHei = $DH; return "QT";}
  0            
  0            
  0            
571             # ISO A6: 124443
572 0 0 0       if ($_ > 123_443 and $_ < 125_443) {$GoWid = $AW; $GoHei = $AH; return "A6";}
  0            
  0            
  0            
573             # US 1/4 legal: 153720
574 0 0 0       if ($_ > 152_720 and $_ < 154_720) {$GoWid = $GW; $GoHei = $GH; return "QG";}
  0            
  0            
  0            
575             # US 1/2 Letter ("statement"): 242352
576 0 0 0       if ($_ > 241_352 and $_ < 243_352) {$GoWid = $DW; $GoHei = $DH; return "HT";}
  0            
  0            
  0            
577             # ISO A5: 249305
578 0 0 0       if ($_ > 248_305 and $_ < 250_305) {$GoWid = $AW; $GoHei = $AH; return "A5";}
  0            
  0            
  0            
579             # US 1/2 legal: 308448
580 0 0 0       if ($_ > 307_448 and $_ < 309_448) {$GoWid = $GW; $GoHei = $GH; return "HG";}
  0            
  0            
  0            
581             # US letter: 484704
582 0 0 0       if ($_ > 483_704 and $_ < 485_704) {return "USletter, ${measuresInMm}"; }
  0            
583             # ISO A4: 500395
584 0 0 0       if ($_ > 499_395 and $_ < 501_395) {return "A4, ${measuresInMm}"; }
  0            
585             # US legal: 616896
586 0 0 0       if ($_ > 615_896 and $_ < 617_896) {return "USlegal, ${measuresInMm}";}
  0            
587             }
588 0           return "unknown, ${measuresInMm}";
589             }
590              
591              
592             ##########################################################
593             sub getPage {
594             ##########################################################
595 0     0 0   my $pagenumber = $_[0];
596 0           my ( $reference, $formRes, $formCont );
597              
598             # Find root:
599 0           my $elObje = getObject($Groot);
600              
601             # Find pages:
602 0 0         die "[!] Didn't find Pages section in '${GinFile}', aborting.\n"
603             unless $elObje =~ m'/Pages\s+(\d+)\s+\d+\s+R's;
604 0           $elObje = getObject($1);
605              
606 0           $elObje = xformObjForThisPage($elObje, $pagenumber);
607 0           ($formRes, $formCont) = getResources( $elObje );
608              
609 0           $reference = writeRes($elObje, $formRes, $formCont);
610              
611 0           writeToBeCreated();
612              
613 0           return $reference;
614             }
615              
616              
617             ##########################################################
618             sub writeRes {
619             ##########################################################
620 0     0 0   my ($elObje, $formRes, $formCont) = ($_[0], $_[1], $_[2]);
621 0           my $out_line;
622              
623 0           $elObje = getObject($formCont);
624 0           $elObje =~ m'^(\d+ \d+ obj\s*<<)(.+)(>>\s*stream)'s;
625 0           my $strPos = length($1) + length($2) + length($3);
626 0           my $newPart = "<
627             . "/BBox [@{GformBox}] ${2}";
628              
629 0           ++$GobjNr;
630 0           $Gobject[$GobjNr] = $Gpos;
631 0           my $reference = $GobjNr;
632 0           update_references_and_populate_to_be_created($newPart);
633 0           $out_line = "${reference} 0 obj\n${newPart}>>\nstream";
634 0           $out_line .= substr( $elObje, $strPos );
635 0           $Gpos += syswrite $OUT_FILE, $out_line;
636 0           return $reference;
637             }
638              
639              
640             ##########################################################
641             sub xformObjForThisPage {
642             ##########################################################
643 0     0 0   my ($elObje, $pagenumber) = ($_[0], $_[1]);
644 0           my ($vector, @pageObj, @pageObjBackup, $pageAccumulator);
645              
646 0 0         return 0 unless $elObje =~ m'/Kids\s*\[([^\]]+)'s;
647 0           $vector = $1;
648              
649 0           $pageAccumulator = 0;
650              
651 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
652 0           while ( $pageAccumulator < $pagenumber ) {
653 0           @pageObjBackup = @pageObj;
654 0           undef @pageObj;
655 0           for (@pageObjBackup) {
656 0           $elObje = getObject($_);
657 0 0         if ( $elObje =~ m'/Count\s+(\d+)'s ) {
658 0 0         if ( ( $pageAccumulator + $1 ) < $pagenumber ) {
659 0           $pageAccumulator += $1;
660             } else {
661 0 0         $vector = $1 if $elObje =~ m'/Kids\s*\[([^\]]+)'s ;
662 0           push @pageObj, $1 while $vector =~ m'(\d+)\s+\d+\s+R'gs;
663 0           last;
664             }
665             } else {
666 0           ++$pageAccumulator;
667             }
668 0 0         last if $pageAccumulator == $pagenumber;
669             }
670             }
671 0           return $elObje;
672             }
673              
674              
675             ##########################################################
676             sub getResources {
677             ##########################################################
678 0     0 0   my $obj = $_[0];
679 0           my ($resources, $formCont);
680              
681             # Assume all input PDF pages have the same dimensions as first MediaBox found:
682 0 0         if (! @GformBox) {
683 0 0         if ( $obj =~ m'MediaBox\s*\[\s*([\S]+)\s+([\S]+)\s+([\S]+)\s+([\S]+)\s*\]'s ) {
684 0           @GformBox = ($1, $2, $3, $4);
685             }
686             }
687              
688 0 0         if ( $obj =~ m'/Contents\s+(\d+)'s ) {
    0          
689 0           $formCont = $1;
690             } elsif ( $obj =~ m'/Contents\s*\[\s*(\d+)\s+\d+\s+R\s*\]'s ) {
691 0           $formCont = $1;
692             }
693              
694 0           $resources = getResourcesFromObj($obj);
695              
696 0           return ($resources, $formCont);
697             }
698              
699              
700             ##########################################################
701             sub getResourcesFromObj {
702             ##########################################################
703 0     0 0   my $obj = $_[0];
704 0           my $resources;
705              
706 0 0         if ( $obj =~ m'^(.+/Resources)'s ) {
707 0 0         return $1 if $obj =~ m'Resources(\s+\d+\s+\d+\s+R)'s; # Reference (95%)
708             # The resources are a dictionary. The whole is copied (morfologia.pdf):
709 0           my $k;
710 0           ( undef, $obj ) = split /\/Resources/, $obj;
711 0           $obj =~ s/<
712 0           $obj =~ s/>>/>>#/gs;
713 0           for ( split /#/, $obj ) {
714 0 0         if ( m'\S's ) {
715 0           $resources .= $_;
716 0 0         ++$k if m'<<'s;
717 0 0         --$k if m'>>'s;
718 0 0         last if $k == 0;
719             }
720             }
721             }
722 0           return $resources;
723             }
724              
725              
726             ##########################################################
727             sub openInputFile {
728             ##########################################################
729 0     0 0   $GinFile = $_[0];
730 0           my ( $elObje, $inputPageSize, $c );
731              
732 0 0         open( $IN_FILE, q{<}, $GinFile )
733             or die "[!] Couldn't open '${GinFile}'.\n";
734 0           binmode $IN_FILE;
735              
736 0           sysread $IN_FILE, $c, 5;
737 0 0         return 0 if $c ne "%PDF-";
738              
739             # Find root
740 0           $Groot = getRootAndMapGobjects();
741 0 0         return 0 unless $Groot > 0;
742              
743             # Find input page size:
744 0           $inputPageSize = getInputPageDimensions();
745              
746             # Find pages
747 0 0         return 0 unless eval { $elObje = getObject($Groot); 1; };
  0            
  0            
748              
749 0 0         if ( $elObje =~ m'/Pages\s+(\d+)\s+\d+\s+R's ) {
750 0           $elObje = getObject($1);
751 0 0         return ($1, $inputPageSize) if $elObje =~ m'/Count\s+(\d+)'s;
752             }
753 0           return 0;
754             }
755              
756              
757             ##########################################################
758             sub addSizeToGObjects {
759             ##########################################################
760 0     0 0   my $size = (stat($GinFile))[7]; # stat[7] = filesize
761             # Objects are sorted numerically (<=>) and in reverse order ($b $a)
762             # according to their offset in the file: last first
763 0           my @offset = sort { $GObjects{$b} <=> $GObjects{$a} } keys %GObjects;
  0            
764              
765 0           my $pos;
766              
767 0           for (@offset) {
768 0           $pos = $GObjects{$_};
769 0           $size -= $pos;
770 0           $GObjects{$_} = [ $pos, $size ]; # if ! m'^xref';
771 0           $size = $pos;
772             }
773             }
774              
775              
776             ##########################################################
777             sub update_references_and_populate_to_be_created {
778             ##########################################################
779             # $xform translates an old object number to a new one
780             # and populates a table with what must be created
781 0     0 0   state %known;
782             my $xform = sub {
783 0 0   0     return $known{$1} if exists $known{$1};
784 0           push @Gto_be_created, [ $1, ++$GobjNr ];
785 0           return $known{$1} = $GobjNr;
786 0           };
787 0           $_[0] =~ s/\b(\d+)\s+\d+\s+R\b/&$xform . ' 0 R'/eg;
  0            
788 0           return;
789             }
790              
791              
792             ##########################################################
793             sub extractXrefSection {
794             ##########################################################
795 0     0 0   my $readBytes = ""; my ($qty, $idx, $c);
  0            
796              
797 0           sysread $IN_FILE, $c, 1;
798 0           sysread $IN_FILE, $c, 1 while $c =~ m'\s's;
799 0           while ( $c =~ /[\d ]/ ) {
800 0           $readBytes .= $c;
801 0           sysread $IN_FILE, $c, 1;
802             }
803 0 0         ($idx, $qty) = ($1, $2) if $readBytes =~ m'^(\d+)\s+(\d+)';
804 0           return ($qty, $idx);
805             }
806              
807              
808             ##########################################################
809             sub openOutputFile {
810             ##########################################################
811 0 0   0 0   closeOutputFile() if $Gpos;
812              
813 0           my $outputfile = $_[0];
814 0           my $pdf_signature = "%PDF-1.4\n%\â\ã\Ï\Ó\n"; # Keep it far from file beginning!
815              
816 0 0         open( $OUT_FILE, q{>}, $outputfile )
817             or die "[!] Couldn't open file '${outputfile}'.\n";
818 0           binmode $OUT_FILE;
819 0           $Gpos = syswrite $OUT_FILE, $pdf_signature;
820              
821 0           $GobjNr = 2; # Objeto reservado 1 para raíz y 2 para nodo de pág. inicial
822 0           $Gparents[0] = 2;
823              
824 0           setInitGrState();
825 0           return;
826             }
827              
828              
829             ##########################################################
830             sub closeInputFile {
831             ##########################################################
832 0     0 0   close $IN_FILE;
833             }
834              
835             1;
836              
837             __END__