File Coverage

blib/lib/App/PDF/Link.pm
Criterion Covered Total %
statement 36 305 11.8
branch 0 156 0.0
condition 0 24 0.0
subroutine 12 24 50.0
pod 0 9 0.0
total 48 518 9.2


line stmt bran cond sub pod time code
1             #! perl
2              
3             package App::PDF::Link;
4              
5             # pdflink -- insert file links in PDF documents
6              
7             our $VERSION = '0.22';
8              
9             # Author : Johan Vromans
10             # Created On : Thu Sep 15 11:43:40 2016
11             # Last Modified By: Johan Vromans
12             # Last Modified On: Tue Feb 5 10:46:41 2019
13             # Update Count : 386
14             # Status : Unknown, Use with caution!
15              
16             ################ Common stuff ################
17              
18 1     1   71922 use strict;
  1         3  
  1         33  
19 1     1   5 use warnings;
  1         3  
  1         29  
20 1     1   736 use utf8;
  1         16  
  1         5  
21 1     1   34 use Carp;
  1         2  
  1         60  
22              
23             ################ The Process ################
24              
25 1     1   406 use App::PDF::Link::Icons;
  1         3  
  1         78  
26 1     1   700 use PDF::API2 2.029;
  1         242938  
  1         47  
27 1     1   11 use Encode qw (encode_utf8 decode_utf8 );
  1         3  
  1         91  
28              
29             sub run {
30 0     0 0   my ( $pkg, $env ) = @_;
31              
32 1     1   591 use PDF::API2::Annotation;
  1         2559  
  1         101  
33              
34 0 0         if ( $env->{embed} <= 1 ) {
35             die("No attachment support??")
36             unless PDF::API2::Annotation->can( $env->{embed}
37 0 0         ? "fileattachment"
    0          
38             : "file" );
39             }
40             else {
41 0 0         die("No embedding support??")
42             unless PDF::API2->can("embeddedfile");
43             }
44              
45 1     1   8 use PDF::API2::Page;
  1         3  
  1         1711  
46 0           *PDF::API2::Page::annotation =
47             *PDF::API2::Page::annotation_xx;
48              
49 0 0         if ( @{ $env->{targets} } ) {
  0            
50             linktargets( $env, $_, $env->{targets} )
51 0           foreach @ARGV;
52             }
53             else {
54 0           linkit( $env );
55             }
56             }
57              
58             ################ Subroutines ################
59              
60             sub linktargets {
61 0     0 0   my $env = shift;
62 0           my ( $pdf, $pageno, $pdfname, $targets, @targets );
63 0           my ( $v, $d, $p );
64              
65 0 0         if ( @_ == 2 ) {
    0          
66 0           ( $pdfname, $targets ) = @_;
67              
68 0 0         warn("Loading PDF $pdfname...\n") if $env->{verbose};
69 0 0         $pdf = PDF::API2->open($pdfname)
70             or die("$pdfname: $!\n");
71              
72 0           ( $v, $d, $p ) = File::Spec->splitpath($pdfname);
73 0           my $pp = $p;
74 0           $pp =~ s/\.pdf$//i;
75              
76 0           @targets = @$targets;
77 0           foreach ( @targets ) {
78 0 0         $_ = $pp . $_ if /^\.\w+$/;
79             }
80 0           $pageno = 1;
81             }
82             elsif ( @_ == 3 ) {
83 0           ( $pdf, $pageno, $targets ) = @_;
84 0           @targets = @$targets;
85             }
86             else {
87 0           die("Internal error -- wrong args to linktargets\n");
88             }
89              
90 0           my $page; # the current page
91             my $text; # text content
92 0           my $gfx; # graphics content
93 0           my $x; # current x for icon
94 0           my $y; # current y for icon
95 0           my $did;
96 0           my $embed = $env->{embed}; # 0 = linked
97             # 1 = embedded
98             # 2 = attached
99 0           my $action = ( qw( linked embedded attached ) )[$embed];
100              
101 0           foreach ( @targets ) {
102 0 0         unless ( -r $_ ) {
103 0           warn("\tTarget: ", encode_utf8($_), " missing (skipped)\n");
104 0           next;
105             }
106 0           $did++;
107              
108 0   0       my $t = substr( $_, length(File::Spec->catpath($v, $d||"", "") ) );
109 0           ( my $ext = $t ) =~ s;^.*\.(\w+)$;$1;;
110 0           my $p = get_icon( $env, $pdf, $ext );
111 0 0         unless ( $p ) {
112 0           warn("\tFile: ", encode_utf8($t), " (ignored)\n");
113 0           next;
114             }
115              
116 0           warn("\tFile: ", encode_utf8($t), " ($action)\n");
117              
118 0 0         if ( $action eq "attached" ) {
119             # Separate objects, no coordinate problems.
120 0           $pdf->embeddedfile( $t );
121 0           next;
122             }
123              
124 0           my $dx = $env->{iconsz} + $env->{padding};
125 0           my $dy = $env->{iconsz} + $env->{padding};
126              
127 0 0         unless ( $page ) {
128 0           $page = $pdf->openpage($pageno);
129 0           my @m = $page->get_mediabox;
130 0 0         if ( $env->{xpos} >= 0 ) {
131 0           $x = $m[0] + $env->{xpos};
132             }
133             else {
134 0           $x = $m[2] + $env->{xpos} - $env->{iconsz};
135 0 0         $dx = -$dx unless $env->{vertical};
136             }
137 0 0         if ( $env->{ypos} >= 0 ) {
138 0           $y = $m[3] - $env->{ypos} - $env->{iconsz};
139             }
140             else {
141 0           $y = $m[1] - $env->{ypos};
142 0 0         $dy = -$dy if $env->{vertical};
143             }
144              
145 0           $text = $page->text;
146             ####WARNING: Coordinates may be wrong!
147             # The graphics context uses the user transformations
148             # currently in effect. If these were not neatly restored,
149             # the graphics may be misplaced/scaled.
150             # By using --gfunder, the images are placed behind the page
151             # but this only works for transparent pages.
152 0 0         $gfx = $page->gfx( $embed ? 0 : $env->{gfunder} );
153             }
154              
155 0           my $border = $env->{border};
156 0           my @r = ( $x, $y, $x + $env->{iconsz}, $y + $env->{iconsz} );
157              
158 0 0         if ( $action eq "embedded" ) {
159             # This always uses the right coordinates.
160 0           my $ann = $page->annotation_xx;
161 0           $ann->fileattachment( $t,
162             -text => "$t $action by pdflink $VERSION",
163             -icon => $p,
164             -rect => \@r );
165             }
166             else {
167 0           my $ann = $page->annotation_xx;
168 0           $ann->file( $t, -rect => \@r );
169 0           my $scale = $env->{iconsz} / $p->width;
170 0           $gfx->image( $p, @r[0,1], $scale );
171             }
172              
173 0 0         if ( $env->{border} ) {
174 0           $gfx->rectxy(@r );
175 0           $gfx->stroke;
176             }
177              
178             # Next link.
179 0 0         if ( $env->{vertical} ) {
180 0           $y -= $dy;
181             }
182             else {
183 0           $x += $dx;
184             }
185             }
186 0 0         return unless $pdfname;
187              
188             # Finish PDF document.
189 0 0         if ( $env->{output} ) {
    0          
190 0 0         warn("Writing PDF ", $env->{output}, " ...\n") if $env->{verbose};
191 0           $pdf->saveas($env->{output});
192 0 0         warn("Wrote: ", $env->{output}, "\n") if $env->{verbose};
193             }
194             elsif ( $did ) {
195 0 0         warn("Updating PDF $pdfname...\n") if $env->{verbose};
196 0           $pdf->update;
197 0 0         warn("Wrote: $pdfname\n") if $env->{verbose};
198             }
199             else {
200 0 0         warn("Not modified: $pdfname\n") if $env->{verbose};
201             }
202             }
203              
204             sub linkit {
205 0     0 0   my $env = shift;
206              
207 0           require Text::CSV_XS;
208 0           require File::Spec;
209 0           require File::Glob;
210              
211 0           my ( $pdfname, $csvname ) = @ARGV;
212 0 0         unless ( $csvname ) {
213 0           ( $csvname = $pdfname ) =~ s/\.pdf$/.csv/i;
214             }
215 0   0       $env->{output} ||= "__new__.pdf";
216              
217 0 0         warn("Loading PDF $pdfname...\n") if $env->{verbose};
218 0 0         my $pdf = PDF::API2->open($pdfname)
219             or die("$pdfname: $!\n");
220              
221 0           my ( $v, $d, $p ) = File::Spec->splitpath($pdfname);
222 0           my $pp = $p;
223 0           $pp =~ s/\.pdf$//i;
224              
225             # Read/parse CSV.
226 0 0         warn("Loading CSV $csvname...\n") if $env->{verbose};
227 0           my $csv = Text::CSV_XS->new( { binary => 1,
228             sep_char => ";",
229             empty_is_undef => 1,
230             auto_diag => 1 });
231 0 0         open( my $fh, "<:encoding(utf8)", $csvname )
232             or die("$csvname: $!\n");
233              
234 0           my $i_title;
235             my $i_pages;
236 0           my $i_xpos;
237 0           my $i_ypos;
238 0           my $row = $csv->getline($fh);
239 0           for ( my $i = 0; $i < @$row; $i++ ) {
240 0 0         next unless defined $row->[$i];
241 0 0         $i_title = $i if lc($row->[$i]) eq "title";
242 0 0         $i_pages = $i if lc($row->[$i]) eq "pages";
243 0 0         $i_xpos = $i if lc($row->[$i]) eq "xpos";
244 0 0         $i_ypos = $i if lc($row->[$i]) eq "ypos";
245             }
246 0 0         die("Invalid info in $csvname. missing TITLE\n")
247             unless defined $i_title;
248 0 0         die("Invalid info in $csvname. missing PAGES\n")
249             unless defined $i_pages;
250              
251 0 0         warn("Processing CSV entries...\n") if $env->{verbose};
252 0           while ( $row = $csv->getline($fh)) {
253 0           my $title = $row->[$i_title];
254 0           my $pageno = $row->[$i_pages];
255 0 0         $pageno = $1 if $pageno =~ /^(\d+)/;
256 0 0         warn("Page: $pageno, ", encode_utf8($title), "\n") if $env->{verbose};
257              
258 0           my $t = $title;
259 0           $t =~ s;[:/];@;g; # eliminate dangerous characters
260 0 0         $t =~ s;["<>?\\|*];@;g if $^O =~ /win/i; # eliminate dangerous characters
261              
262 0           my @files = File::Glob::bsd_glob( File::Spec->catpath($v, $d, "$t.*" ) );
263 0           linktargets( $env, $pdf, $pageno, \@files );
264              
265             }
266 0           close $fh;
267              
268             # Finish PDF document.
269 0 0         warn("Writing PDF ", $env->{output}, " ...\n") if $env->{verbose};
270 0           $pdf->saveas($env->{output});
271 0 0         warn("Wrote: ", $env->{output}, "\n") if $env->{verbose};
272             }
273              
274             ################ Options and Configuration ################
275              
276 1     1   863 use Getopt::Long 2.13;
  1         10545  
  1         26  
277 1     1   165 use File::Spec;
  1         2  
  1         23  
278 1     1   6 use Carp;
  1         2  
  1         2237  
279              
280             # Package name.
281             my $my_package;
282             # Program name and version.
283             my ($my_name, $my_version);
284              
285             sub app_setup {
286 0     0 0   my ( $pkg, $appname, $appversion, %args ) = @_;
287 0           my $help = 0; # handled locally
288 0           my $ident = 0; # handled locally
289 0           my $man = 0; # handled locally
290              
291             # Package name.
292 0           $my_package = $args{package};
293             # Program name and version.
294 0 0         if ( defined $appname ) {
295 0           ($my_name, $my_version) = ($appname, $appversion);
296             }
297             else {
298 0           ($my_name, $my_version) = qw( MyProg 0.01 );
299             }
300              
301 0           my $options =
302             {
303             output => undef, # output pdf
304             embed => undef, # link, embed or attach
305             all => 0, # link all files
306             xpos => 60, # position of icons
307             ypos => 60, # position of icons
308             padding => 0, # padding between icons
309             iconsz => 50, # desired icon size
310             icons => {}, # additional icons
311             vertical => undef, # stacking of icons
312             border => 0, # draw borders around icon
313             gfunder => 0, # draw images behind the page
314             targets => [], # explicit link targets
315             verbose => 0, # verbose processing
316             ### ADD OPTIONS HERE ###
317              
318             # Development options (not shown with -help).
319             debug => 0, # debugging
320             trace => 0, # trace (show process)
321              
322             # Service.
323             _package => $my_package,
324             _name => $my_name,
325             _version => $my_version,
326             _stdin => \*STDIN,
327             _stdout => \*STDOUT,
328             _stderr => \*STDERR,
329             _argv => [ @ARGV ],
330             };
331              
332             my $pod2usage = sub {
333             # Load Pod::Usage only if needed.
334 0     0     require Pod::Usage;
335 0           my $f = __FILE__;
336 0 0         if ( $App::Packager::PACKAGED ) {
337 0           $f = App::Packager::GetResource("pod/pdflink.pod");
338 0           unshift( @_, -noperldoc => 1 );
339             }
340 0           unshift( @_, -input => $f );
341 0           &Pod::Usage::pod2usage;
342 0           };
343              
344             # Collect command line options in a hash, for they will be needed
345             # later.
346 0           my $clo = { embed => 0 };
347              
348             # Sorry, layout is a bit ugly...
349 0 0         if ( !GetOptions
350             ($clo,
351              
352             ### ADD OPTIONS HERE ###
353             'output|pdf=s',
354             'embed',
355 0     0     'attach' => sub { $clo->{embed} = 2 },
356             'all',
357             'xpos=i',
358             'ypos=i',
359             'iconsz|iconsize|icon=i',
360             'icons=s%',
361             'padding=i',
362             'vertical',
363             'border',
364             'gfunder',
365             'targets|t=s@',
366              
367             # Standard options.
368             'ident' => \$ident,
369             'help|?' => \$help,
370             'manual' => \$man,
371             'verbose|v',
372             'trace',
373             'debug',
374 0     0     'version' => sub { app_ident(\*STDOUT); exit },
  0            
375             ) )
376             {
377 0           $pod2usage->(2);
378             }
379             # GNU convention: message to STDOUT upon request.
380 0 0 0       app_ident(\*STDOUT) if $ident or $help;
381 0 0 0       if ( $man or $help ) {
382 0 0         $pod2usage->(1) if $help;
383 0 0         $pod2usage->(VERBOSE => 2) if $man;
384             }
385              
386             # Plug in command-line options.
387 0           @{$options}{keys %$clo} = values %$clo;
  0            
388              
389 0 0         if ( @{ $options->{targets} } ) {
  0            
390 0           @{ $options->{targets} } = split( /[;,]/, join(":", @{ $options->{targets} }) );
  0            
  0            
391 0 0         $pod2usage->(1) unless @ARGV;
392 0 0 0       $pod2usage->(1) if $options->{pdf} && @ARGV > 1;
393             }
394             else {
395 0 0 0       $pod2usage->(1) if @ARGV < 1 || @ARGV > 2;
396             }
397              
398 0           $options;
399             }
400              
401             sub app_ident {
402 0     0 0   my ($fh) = @_;
403 0 0         print {$fh} ("This is ",
  0            
404             $my_package
405             ? "$my_package [$my_name $my_version]"
406             : "$my_name version $my_version",
407             "\n");
408             }
409              
410             =head1 NAME
411              
412             pdflink - insert document links in PDF
413              
414             =head1 SYNOPSIS
415              
416             pdflink [options] pdf-file [csv-file]
417              
418             pdflink [options] --targets=file1;file2 pdf-file [pdf-file ...]
419              
420             Inserts document links in PDF
421              
422             Options:
423             --output=XXX name of the new PDF (default __new__.pdf)
424             --embed embed the data files instead of linking
425             --attach attach the data files instead of linking
426             --xpos=NN X-position for links
427             --ypos=NN Y-position for links relative to top
428             --iconsize=NN size of the icons, default 50
429             --icons=type=XXX add icon image XXX for this type
430             --padding=NN padding between icons, default 0
431             --vertical stacks icons vertically
432             --border draws a border around the links
433             --gfunder draws the images behind the page
434             --targets=XXX specifies the target(s) to link to
435             --ident shows identification
436             --help shows a brief help message and exits
437             --man shows full documentation and exits
438             --verbose provides more verbose information
439              
440             =head1 DESCRIPTION
441              
442             When invoked without a B<--targets> option, this program will process
443             the PDF document using the associated CSV as table of contents.
444              
445             For every item in the PDF that has one or more additional files (files
446             with the same name as the title, but differing extensions), clickable
447             icons are added to the first page of the item. When clicked in a
448             suitable PDF viewing tool, the corrresponding file will be activated.
449              
450             For example, if the CSV contains
451              
452             title;pages;
453             Blue Moon;24;
454              
455             And the following files are present in the current directory
456              
457             Blue Moon.html
458             Blue Moon.mscz
459              
460             Then two clickable icons will be added to page 24 of the document,
461             leading to these two files.
462              
463             Upon completion, the updated PDF is written out under the specified name.
464              
465             When invoked with the B<--targets> option, all specified PDF files get
466             links inserted to the targets on the first page. If there is only one
467             PDF file you can use the B<--pdf> option to designate the name of the
468             new PDF document, otherwise all PDF files are updated (rewritten.
469              
470             =head1 OPTIONS
471              
472             Note that all sizes and dimensions are in I (72 points per inch).
473              
474             =over 8
475              
476             =item B<--pdf=>I
477              
478             Specifies the updated PDF to be written.
479              
480             =item B<--embed>
481              
482             Normally links are inserted into the PDF document that point to files
483             on disk. To use the links from the PDF document, the target files must
484             exist on disk.
485              
486             With B<--embed>, the target files are embedded (as file attachments)
487             to the PDF document. The resultant PDF document will be usable on its
488             own, no other files needed.
489              
490             =item B<--attach>
491              
492             This is similar to B<--embed>, but the files are attached to the PDF
493             document and no icons are placed on the pages.
494              
495             =item B<--all>
496              
497             Normally, only files with known types (extensions) are taken into
498             account. Currently, these are C for iRealPro, C for
499             MuseScore and C and similar for Band in a Box.
500              
501             With B<--all>, all files that have matching names will be processed.
502             However, files with unknown extensions will get a neutral document
503             icon.
504              
505             =item B<--xpos=>I
506              
507             Horizontal position of the icons.
508              
509             If the value is positive, icon placement starts relative to the left
510             side of the page.
511              
512             If the value is negative, icon placement starts relative to the right
513             side of the page.
514              
515             Default is 0 (zero); icon placement begins against the left side of
516             the page.
517              
518             Icons are always placed from the outside of the page towards the
519             inner side.
520              
521             An I value may also be specified in the CSV file, in a column
522             with title C. If present, this value is added to position
523             resulting from the command line / default values.
524              
525             =item B<--ypos=>I
526              
527             If the value is positive, icon placement starts relative to the top
528             of the page.
529              
530             If the value is negative, icon placement starts relative to the bottom
531             of the page.
532              
533             Default is 0 (zero); icon placement begins against the top of the
534             page.
535              
536             Icons are always placed from the outside of the page towards the
537             inner side.
538              
539             An I offset value may also be specified in the CSV file, in a
540             column with title C. If present, this value is added to position
541             resulting from the command line / default values. This is especially
542             useful if there are songs in the PDF that do not start at the top of
543             the page, e.g., when there are multiple songs on a single page.
544              
545             =item B<--iconsize=>I
546              
547             Desired size of the link icons. Default is 50.
548              
549             =item B<--padding=>I
550              
551             Space between icons. Default is to place the icons adjacent to each
552             other.
553              
554             =item B<--vertical>
555              
556             Stacks the icons vertically.
557              
558             =item B<--border>
559              
560             Requests a border to be drawn around the links.
561              
562             Borders are always drawn for links without icons.
563              
564             =item B<--gfunder>
565              
566             Drawing the icon images uses the page transformations in effect at the
567             end of the page. If these were not neatly restored, the graphics may
568             be misplaced/scaled/flipped.
569              
570             By using B<--gfunder>, the images are placed behind the page
571             but this only works for transparent pages.
572              
573             This option is only relevant when adding links to external files. With
574             B<--embed> the problem does not occur.
575              
576             =item B<--targets=>I [ B<;> I ... ]
577              
578             Explicitly specifies the target files to link to. In this case no CSV
579             is processed and the input PDF(s) are updated (rewritten) unless
580             B<--pdf> is used to designate the output PDF name.
581              
582             =item B<--help>
583              
584             Prints a brief help message and exits.
585              
586             =item B<--man>
587              
588             Prints the manual page and exits.
589              
590             =item B<--ident>
591              
592             Prints program identification.
593              
594             =item B<--verbose>
595              
596             Provides more verbose information.
597              
598             =item I
599              
600             The directory to process. Defaults to the current directory.
601              
602             =back
603              
604             =head1 ICONS
605              
606             B has a number of icons built-in for common file types.
607             Associations between a filename extension and an icon can be made with
608             the B<--icons> command line option.
609              
610             For example,
611              
612             --icons=pdf=builtin:PDF
613              
614             This will associate the built-in icon PDF with filename extension C.
615              
616             Alternatively, an image file may be specified to add user defined icons.
617              
618             --icons=pdf=builtin:myicons/pdficon.png
619              
620             The following icons are built-in. By default, only MuseScore and
621             iRealPro icons are associated and all other filename extensions will
622             be skipped. When pdflink is run with command line option B<--all>, all
623             built-in icons will be associated and all matching files will get
624             linked.
625              
626             =over
627              
628             =item PDF
629              
630             Associated to filename extension C (generic PDF document).
631              
632             =item PNG
633              
634             Associated to filename extension C (PNG image).
635              
636             =item JPG
637              
638             Associated to filename extensions C and C (JPG image).
639              
640             =item MuseScore
641              
642             Associated to filename extension C (MuseScore document).
643              
644             =item iRealPro
645              
646             Associated to filename extension C (iRealPro link in HTML document).
647              
648             While technically this is wrong, this is the way iRealPro data is
649             handled on Android and iPad.
650              
651             =item BandInABox
652              
653             Associated to filename extensions C, C and so on (Band-In-A-Box document).
654              
655             =item Document
656              
657             Fallback icon for unknown filename extensions.
658              
659             =item Border
660              
661             Alternative fallback icon for unknown filename extensions.
662              
663             =back
664              
665             =head1 LIMITATIONS
666              
667             Some PDF files cannot be processed. If this happens, try converting
668             the PDF to PDF-1.4 or PDF/A.
669              
670             Files with extension B are assumed to be iRealPro files and will
671             get the iRealPro icon.
672              
673             Unknown extensions will get an empty square box instead of an icon.
674              
675             Since colon C<:> and slash C are not allowed in file names, they
676             are replaced with C<@> characters.
677              
678             If the icons come out at the wrong place or upside down, try
679             B<--gfunder>.
680              
681             =head1 AUTHOR
682              
683             Johan Vromans Ejvromans@squirrel.nlE
684              
685             =head1 COPYRIGHT
686              
687             Copyright 2016 Johan Vromans. All rights reserved.
688              
689             This module is free software. You can redistribute it and/or
690             modify it under the terms of the Artistic License 2.0.
691              
692             This program is distributed in the hope that it will be useful,
693             but without any warranty; without even the implied warranty of
694             merchantability or fitness for a particular purpose.
695              
696             =cut
697              
698             ################ Patches ################
699              
700             package PDF::API2::Page;
701              
702             sub annotation_xx {
703 0     0 0   my ($self, $type, $key, $obj) = @_;
704              
705 0   0       $self->{'Annots'}||=PDFArray();
706 0 0         $self->{'Annots'}->realise if(ref($self->{'Annots'})=~/Objind/);
707 0 0         if($self->{'Annots'}->is_obj($self->{' apipdf'}))
708             {
709             # $self->{'Annots'}->update();
710             }
711             else
712             {
713 0           $self->update();
714             }
715              
716 0           my $ant=PDF::API2::Annotation->new;
717 0           $self->{'Annots'}->add_elements($ant);
718 0           $self->{' apipdf'}->new_obj($ant);
719 0           $ant->{' apipdf'}=$self->{' apipdf'};
720 0           $ant->{' apipage'}=$self;
721              
722 0 0         if($self->{'Annots'}->is_obj($self->{' apipdf'}))
723             {
724 0           $self->{' apipdf'}->out_obj($self->{'Annots'});
725             }
726              
727 0           return($ant);
728             }
729              
730             package PDF::API2::Annotation;
731              
732             #=item $ant->fileattachment $file, %opts
733             #
734             #Defines the annotation as a file attachment with file $file and
735             #options %opts (-rect, -border, -content (type), -icon (name), -text (comment)).
736             #
737             #=cut
738              
739             sub fileattachment {
740 0     0 0   my ( $self, $file, %opts ) = @_;
741              
742 0           my $icon;
743 0 0 0       $icon = $opts{-icon} || 'PushPin' if exists $opts{-icon};
744 0 0         my @r = @{ $opts{-rect} } if defined $opts{-rect};
  0            
745 0 0         my @b = @{ $opts{-border} } if defined $opts{-border};
  0            
746              
747 0           $self->{Subtype} = PDFName('FileAttachment');
748 0 0         $self->{T} = PDFStr($opts{"-text"}) if exists($opts{"-text"});
749              
750 0 0         if ( is_utf8($file)) {
751             # URI must be 7-bit ascii
752 0           utf8::downgrade($file);
753             }
754              
755             # 9 0 obj <<
756             # /Type /Annot
757             # /Subtype /FileAttachment
758             # /Name /PushPin
759             # /C [ 1 1 0 ]
760             # /Contents (test.txt)
761             # /FS <<
762             # /Type /F
763             # /EF << /F 10 0 R >>
764             # /F (test.txt)
765             # >>
766             # /Rect [ 100 100 200 200 ]
767             # /Border [ 0 0 1 ]
768             # >> endobj
769             #
770             # 10 0 obj <<
771             # /Type /EmbeddedFile
772             # /Length ...
773             # >> stream
774             # ...
775             # endstream endobj
776              
777 0           $self->{Contents} = PDFStr($file);
778             # Name will be ignored if there is an AP.
779 0 0 0       $self->{Name} = PDFName($icon) if $icon && !ref($icon);
780             # $self->{F} = PDFNum(0b0);
781 0           $self->{C} = PDFArray( map { PDFNum($_) } 1, 1, 0 );
  0            
782              
783             # The File Specification.
784 0           $self->{FS} = PDFDict();
785 0           $self->{FS}->{F} = PDFStr($file);
786 0           $self->{FS}->{Type} = PDFName('F');
787 0           $self->{FS}->{EF} = PDFDict($file);
788 0           $self->{FS}->{EF}->{F} = PDFDict($file);
789 0           $self->{' apipdf'}->new_obj($self->{FS}->{EF}->{F});
790 0           $self->{FS}->{EF}->{F}->{Type} = PDFName('EmbeddedFile');
791 0           $self->{FS}->{EF}->{F}->{' streamfile'} = $file;
792              
793             # Set the annotation rectangle and border.
794 0 0         $self->rect(@r) if @r;
795 0 0         $self->border(@b) if @b;
796              
797             # Set the appearance.
798 0 0         $self->appearance($icon, %opts) if $icon;
799              
800 0           return($self);
801             }
802              
803             sub appearance {
804 0     0 0   my ( $self, $icon, %opts ) = @_;
805              
806 0 0         return unless $self->{Subtype}->val eq 'FileAttachment';
807              
808 0 0         my @r = @{ $opts{-rect}} if defined $opts{-rect};
  0            
809 0 0         die "insufficient -rect parameters to annotation->appearance( ) "
810             unless(scalar @r == 4);
811              
812             # Handle custom icon type 'None'.
813 0 0         if ( $icon eq 'None' ) {
    0          
814             # It is not clear what viewers will do, so provide an
815             # appearance dict with no graphics content.
816              
817             # 9 0 obj <<
818             # ...
819             # /AP << /D 11 0 R /N 11 0 R /R 11 0 R >>
820             # ...
821             # >>
822             # 11 0 obj <<
823             # /BBox [ 0 0 100 100 ]
824             # /FormType 1
825             # /Length 6
826             # /Matrix [ 1 0 0 1 0 0 ]
827             # /Resources <<
828             # /ProcSet [ /PDF ]
829             # >>
830             # >> stream
831             # 0 0 m
832             # endstream endobj
833              
834 0           $self->{AP} = PDFDict();
835 0           my $d = PDFDict();
836 0           $self->{' apipdf'}->new_obj($d);
837 0           $d->{FormType} = PDFNum(1);
838 0           $d->{Matrix} = PDFArray( map { PDFNum($_) } 1, 0, 0, 1, 0, 0 );
  0            
839 0           $d->{Resources} = PDFDict();
840 0           $d->{Resources}->{ProcSet} = PDFArray( map { PDFName($_) } qw(PDF));
  0            
841 0           $d->{BBox} = PDFArray( map { PDFNum($_) } 0, 0, $r[2]-$r[0], $r[3]-$r[1] );
  0            
842 0           $d->{' stream'} = "0 0 m";
843 0           $self->{AP}->{N} = $d; # normal appearance
844             # Should default to N, but be sure.
845 0           $self->{AP}->{R} = $d; # Rollover
846 0           $self->{AP}->{D} = $d; # Down
847             }
848              
849             # Handle custom icon.
850             elsif ( ref $icon ) {
851             # Provide an appearance dict with the image.
852              
853             # 9 0 obj <<
854             # ...
855             # /AP << /D 11 0 R /N 11 0 R /R 11 0 R >>
856             # ...
857             # >>
858             # 11 0 obj <<
859             # /BBox [ 0 0 1 1 ]
860             # /FormType 1
861             # /Length 13
862             # /Matrix [ 1 0 0 1 0 0 ]
863             # /Resources <<
864             # /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
865             # /XObject << /PxCBA 7 0 R >>
866             # >>
867             # >> stream
868             # q /PxCBA Do Q
869             # endstream endobj
870              
871 0           $self->{AP} = PDFDict();
872 0           my $d = PDFDict();
873 0           $self->{' apipdf'}->new_obj($d);
874 0           $d->{FormType} = PDFNum(1);
875 0           $d->{Matrix} = PDFArray( map { PDFNum($_) } 1, 0, 0, 1, 0, 0 );
  0            
876 0           $d->{Resources} = PDFDict();
877 0           $d->{Resources}->{ProcSet} = PDFArray( map { PDFName($_) } qw(PDF Text ImageB ImageC ImageI));
  0            
878 0           $d->{Resources}->{XObject} = PDFDict();
879 0           my $im = $icon->{Name}->val;
880 0           $d->{Resources}->{XObject}->{$im} = $icon;
881             # Note that the image is scaled to one unit in user space.
882 0           $d->{BBox} = PDFArray( map { PDFNum($_) } 0, 0, 1, 1 );
  0            
883 0           $d->{' stream'} = "q /$im Do Q";
884 0           $self->{AP}->{N} = $d; # normal appearance
885              
886 0           if ( 0 ) {
887             # Testing... Provide an alternative for R and D.
888             # Works only with Adobe Reader.
889             $d = PDFDict();
890             $self->{' apipdf'}->new_obj($d);
891             $d->{Type} = PDFName('XObject');
892             $d->{Subtype} = PDFName('Form');
893             $d->{FormType} = PDFNum(1);
894             $d->{Matrix} = PDFArray( map { PDFNum($_) } 1, 0, 0, 1, 0, 0 );
895             $d->{Resources} = PDFDict();
896             $d->{Resources}->{ProcSet} = PDFArray( map { PDFName($_) } qw(PDF));
897             $d->{BBox} = PDFArray( map { PDFNum($_) } 0, 0, $r[2]-$r[0], $r[3]-$r[1] );
898             $d->{' stream'} =
899             join( " ",
900             # black outline
901             0, 0, 'm',
902             0, $r[2]-$r[0], 'l',
903             $r[2]-$r[0], $r[3]-$r[1], 'l',
904             $r[2]-$r[0], 0, 'l',
905             's',
906             );
907             }
908              
909             # Should default to N, but be sure.
910 0           $self->{AP}->{R} = $d; # Rollover
911 0           $self->{AP}->{D} = $d; # Down
912             }
913              
914 0           return $self;
915             }
916              
917             package PDF::API2;
918              
919             #=item $pdf->embeddedfile $file, %opts
920             #
921             #Adds a file attachment to the document.
922             #
923             #=cut
924              
925             sub embeddedfile {
926 0     0 0   my ( $self, $file, %opts ) = @_;
927              
928 0 0         if ( is_utf8($file)) {
929             # URI must be 7-bit ascii
930 0           utf8::downgrade($file);
931             }
932              
933             # The File Specification.
934 0           my $fs = PDFDict();
935 0           $fs->{Type} = PDFName('Filespec');
936              
937 0           $fs->{F} = PDFStr($file);
938 0           $fs->{UF} = PDFStr( Encode::encode( 'UTF16-BE', "\x{FEFF}$file" ) );
939 0           $fs->{Desc} = PDFStr("$file attached by pdflink $VERSION");
940              
941 0           $fs->{EF} = PDFDict($file);
942 0           $fs->{EF}->{F} = PDFDict($file);
943 0           $self->{pdf}->new_obj($fs->{EF}->{F});
944 0           $fs->{EF}->{F}->{Type} = PDFName('EmbeddedFile');
945 0           $fs->{EF}->{F}->{' streamfile'} = $file;
946 0           $self->{pdf}->new_obj($fs);
947 0           my $nd = $self->named_destination( 'EmbeddedFiles', $file, $fs );
948 0           $self->{'pdf'}->out_obj($self->{catalog});
949              
950 0           return($self);
951             }
952              
953             1;