File Coverage

blib/lib/PDF/Builder/NamedDestination.pm
Criterion Covered Total %
statement 80 135 59.2
branch 30 54 55.5
condition 4 9 44.4
subroutine 12 21 57.1
pod 10 13 76.9
total 136 232 58.6


line stmt bran cond sub pod time code
1             package PDF::Builder::NamedDestination;
2              
3 39     39   261 use base 'PDF::Builder::Basic::PDF::Dict';
  39         84  
  39         4899  
4              
5 39     39   268 use strict;
  39         95  
  39         933  
6 39     39   198 use warnings;
  39         72  
  39         1740  
7              
8 39     39   265 use Carp;
  39         78  
  39         3371  
9 39     39   333 use Encode qw(:all);
  39         119  
  39         14971  
10              
11             our $VERSION = '3.028'; # VERSION
12             our $LAST_UPDATE = '3.028'; # manually update whenever code is changed
13              
14 39     39   294 use PDF::Builder::Util;
  39         92  
  39         6346  
15 39     39   370 use PDF::Builder::Basic::PDF::Utils;
  39         125  
  39         84211  
16              
17             =head1 NAME
18              
19             PDF::Builder::NamedDestination - Add named destinations (views) to a PDF
20              
21             Inherits from L<PDF::Builder::Basic::PDF::Dict>
22              
23             =head2 Usage
24              
25             A Named Destination is defined in a PDF which is intended to be the target of
26             a specified Name (string), from within the same PDF, from another PDF, or
27             from a PDF Reader command line (e.g, MyBigPDF.pdf#nameddest=foo). The advantage
28             over going to a specific I<page> is that a Named Destination always points to
29             the same content in the document, even if that location moves around (i.e., to
30             a different page number).
31              
32             Some Operating Systems support command line invocation (e.g.,
33             E<gt> MyPDF.pdf#name=bar), and some browsers' PDF readers also support it.
34             C<#nameddest=foo> is the most basic form, and if Named Destinations are
35             supported, this form should work. Some readers support the shortcut
36             C<#name=foo>, and some even support the bare name: C<#foo> (similar to HTML).
37             Named Destination syntax on the command line can vary widely by Reader.
38              
39             A Named Destination must be unique within a given PDF, and is defined at the
40             top level in the C<$pdf> object. There are often limitations on the length of
41             a Named Destination string (name), including the length of whatever means is
42             used to invoke it (e.g., C<#nameddest=foo> takes up 11 characters before you
43             even get to the name 'foo' itself, so if the limit is 32, you're left with
44             perhaps 21 characters for the name itself). Be aware of this, and keep the
45             names as short as reasonably possible. Spaces within a name are not permitted,
46             and allowable punctuation is limited. Consult the PDF documentation for
47             specifics, but A-Z, a-z, 0-9, and '_' (underscore) are generally permitted.
48             Usually names are case-sensitive ('foo' is a different destination than 'Foo').
49              
50             # create a Named Destination 'foo' in this PDF file on page $page
51             my $dest = PDF::Builder::NamedDestination->new($pdf);
52             # its action will be to go to this page object $page, and a window within it
53             # (various 'fits' can be defined
54             $dest->goto($page, 'xyz', (undef, undef, undef));
55             $pdf->named_destination('Dests', 'foo', $dest);
56              
57             See L<PDF::Builder/named_destination> and L<PDF::Builder::Docs/Page Fit Options>
58             for information on named destinations.
59              
60             =head1 METHODS
61              
62             =head2 new
63              
64             $dest = PDF::Builder::NamedDestination->new($pdf)
65              
66             $dest = PDF::Builder::NamedDestination->new($pdf, @args)
67              
68             =over
69              
70             Creates a new named destination object. Any optional additional arguments
71             will be passed on to destination processing for "goto".
72              
73             $dest = PDF::Builder::NamedDestination->new($pdf, $page, 'xyz', 0,700, 1.5);
74              
75             This will create a Named Destination which goes to ("goto") page object
76             C<$page>, with fit XYZ at position 0,700 and zoom factor 1.5.
77              
78             It is possible to then call `goto()` to override the `new()` defined I<fit>:
79              
80             $dest->goto($page, 'fitb'); # overrides XYZ fit
81              
82             If you did B<not> give I<fit> options in the `new()` call (just `$pdf`), it
83             will be necessary to call `goto()` with I<fit> settings, anyway:
84              
85             $dest = PDF::Builder::NamedDestination->new($pdf);
86             $dest->goto($page, 'fit');
87              
88             Finally, however you created the Named Destination, its action, and its page
89             fit, you need to tell the system
90             to insert an entry into the Named Destination directory:
91              
92             $pdf->named_destination('Dests', "foo", $dest);
93              
94             This is where you actually I<name> the destination. Consult
95             L<PDF::Builder/named_destination> and L<PDF::Builder::Docs/Page Fit Options>
96             for more information.
97              
98             =back
99              
100             =cut
101              
102             sub new {
103 1     1 1 14 my $class = shift;
104 1         2 my $pdf = shift;
105              
106 1 50       10 $pdf = $pdf->{'pdf'} if $pdf->isa('PDF::Builder');
107 1         8 my $self = $class->SUPER::new($pdf);
108 1         4 $pdf->new_obj($self);
109              
110 1 50       28 if (@_) { # leftover arguments? page_obj + fit + data
111 0         0 my $page = shift;
112 0         0 my %opts = $self->list2hash(@_); # may be empty
113 0         0 $self->{'S'} = PDFName('GoTo'); # default
114 0         0 $self->{'D'} = $self->dest($page, %opts);
115             }
116              
117 1         6 return $self;
118             }
119              
120             # Note: new_api() removed in favor of new():
121             # new_api($api, ...) replace with new($api->{'pdf'}, ...)
122             # Appears to be added back in, PDF::API2 2.042
123             sub new_api {
124 0     0 0 0 my ($class, $api2) = @_;
125 0         0 warnings::warnif('deprecated',
126             'Call to deprecated method new_api, replace with new');
127              
128 0         0 my $destination = $class->new($api2);
129 0         0 return $destination;
130             }
131              
132             # returns an anonymous array with page object and page fit info
133             sub dest {
134 3     3 0 13 my ($self, $page, @args) = @_;
135              
136             # $page is either 1. a page object (from goto)
137             # 2. a formatted page number (from pdf)
138             # 3. a named destination string (from pdf)
139 3         29 my %opts = $self->list2hash(@args); # may be empty!
140              
141 3         9 my ($location, @arglist, $ptr);
142              
143 3         42 my %arg_counts = (
144             # key = location given by user
145             # [0] = required number of arguments, [1] = name for PDF
146             'xyz' => [3, 'XYZ' ], # s/b array ref
147             'fit' => [0, 'Fit' ], # 1 (scalar) ignored
148             'fith' => [1, 'FitH' ], # s/b scalar
149             'fitv' => [1, 'FitV' ], # s/b scalar
150             'fitr' => [4, 'FitR' ], # s/b array ref
151             'fitb' => [0, 'FitB' ], # 1 (scalar) ignored
152             'fitbh' => [1, 'FitBH'], # s/b scalar
153             'fitbv' => [1, 'FitBV'], # s/b scalar
154             );
155              
156             # do any of the options contain a fit location, and if so, the right
157             # number of data values (put into @arglist)?
158 3         13 foreach (keys %arg_counts) {
159 24 100       87 if (defined $opts{$_}) {
160             # this fit $_ is given in the options
161 1         2 $location = $_;
162 1 50       4 if (ref($opts{$_}) eq 'ARRAY') {
163             # it's an anonymous array with presumably 3 or 4 elements
164 0         0 @arglist = @{$opts{$_}};
  0         0  
165             } else {
166             # it's a scalar value
167 1         11 @arglist = ($opts{$_});
168             }
169             }
170             }
171 3 100       24 if (!defined $location) {
172             # no fit location given. default to xyz undef undef undef
173 2         6 $location = 'xyz';
174 2         8 @arglist = (undef, undef, undef);
175             }
176 3 100 66     20 if ($location eq 'fit' || $location eq 'fitb') {
177             # these two locations take no location data, and hash would contain
178             # a dummy value
179 1         3 @arglist = ();
180             }
181             # check number of arguments given for location
182 3 50       24 if (@arglist > $arg_counts{$location}->[0]) {
    50          
183 0         0 carp "Too many items given for '$location' location. Excess discarded.";
184 0         0 splice(@arglist, $arg_counts{$location}->[0]);
185             } elsif (@arglist < $arg_counts{$location}->[0]) {
186 0         0 croak "Too few items given for '$location' location.";
187             }
188              
189 3 100       23 if (ref($page) ne '') {
    50          
190             # it's an object
191             } elsif ($page =~ /^\d+$/) {
192             # it's a number
193 1         6 $page = PDFNum($page);
194             } else {
195             # is a string, and therefore a Named Destination (?) shouldn't see here
196 0         0 croak "string (Named Destination) passed to dest()";
197             }
198 3         13 return _array($page, $arg_counts{$location}->[1], @arglist);
199             }
200              
201             # internal utilities
202              
203             sub _array {
204 3     3   9 my $page = shift();
205 3         8 my $location = shift();
206             # remaining @_ is list of any args needed
207             return PDFArray($page, PDFName($location),
208 3 50       12 map { defined($_) ? PDFNum($_) : PDFNull() } @_);
  6         18  
209             }
210              
211             =head2 Target Destinations
212              
213             Note that the usual practice for a Named Destination, invoked when the PDF is
214             opened with a Named Destination specified, is to I<goto> a point in the
215             document. It is I<possible>, though unusual, to go to a point in another
216             document (C<pdf()>), launch a local application (C<launch()>), or launch a web
217             browser (C<uri()>).
218              
219             The only "options" supported for C<goto> and C<pdf> are if you wish to give
220             the location and its arguments (data) in the form of a hash element (anonymous
221             array if more than one value). Unlike Annotation's "action" methods (C<goto>,
222             C<pdf>, C<uri>, and C<launch>), there is no defining a "click area" (button)
223             for the user interaction; thus, no B<rect>, B<border>, or B<color> entries
224             are recognized in NamedDestination. Any found will be ignored.
225              
226             See L<PDF::Builder::Docs/Page Fit Options> for a listing of the available
227             locations and their syntax.
228              
229             "xyz" is the B<default> fit setting, with position (left and top) and zoom
230             all the same as the calling page's.
231              
232             =head3 goto, link
233              
234             $dest->goto($page, $location, @args) # preferred
235              
236             $dest->goto($page, %opts) # opts including location and data
237              
238             =over
239              
240             A go-to (link) action changes the view to a specified destination (page object,
241             location code, and various pieces of data for it). This is a jump I<within>
242             the current PDF document (B<internal>), and is the usual way of doing things.
243              
244             B<Alternate name:> C<link>
245              
246             Originally this method was C<link>, but PDF::API2 changed the name
247             to C<goto>, to match the internal PDF command C<GoTo>. "link" is retained
248             for compatibility.
249              
250             B<Notes:> C<goto> is a reserved Perl keyword (go to a label), so take care when
251             using this in code that the Perl interpreter doesn't see this as a Perl 'goto'.
252             If you receive an error message about a "missing label" or something equally
253             puzzling, this may have happened. C<link> is a built-in Perl function (Unix
254             C<ln> style command), so take care when using this code that the Perl
255             interpreter doesn't see this as a Perl 'link' call (e.g., error message about
256             "not enough arguments for link").
257              
258             =back
259              
260             =cut
261              
262             # link is also a Perl built-in function call
263 0     0 1 0 sub link { return goto(@_); } ## no critic
264              
265             # goto is also a Perl keyword
266             sub goto { ## no critic
267 1     1 1 8 my $self = shift();
268 1         2 my $page = shift();
269             # remainder of input is any location hash
270              
271 1         4 $self->{'S'} = PDFName('GoTo');
272 1         6 $self->{'D'} = $self->dest($page, @_);
273 1         4 return;
274             }
275              
276             =head3 pdf, pdf_file, pdfile
277              
278             $dest->pdf($pdffile, $page_number, $location, @args) # preferred
279              
280             $dest->pdf($pdffile, $page_number, %opts) # location is a hash element
281              
282             =over
283              
284             Defines the destination as an B<external> PDF-file with filepath C<$pdffile>,
285             on page C<$page_number> (numeric value), and
286             either options %opts (location/fit => any data for it as a scalar or anonymous
287             array) or one of two formats: an array of location/fit string and any data for
288             it, or a location/fit string and an array with any data needed for it.
289              
290             To go to a Named Destination and then immediately jump to a point in another
291             PDF document is unusual, but possible.
292              
293             B<Alternate names:> C<pdf_file> and C<pdfile>
294              
295             Originally this method was C<pdfile>, and had been earlier renamed to
296             C<pdf_file>, but PDF::API2 changed the name to C<pdf>. "pdfile" and
297             "pdf_file" are retained for compatibility.
298              
299             =back
300              
301             =cut
302              
303 0     0 1 0 sub pdf_file { return pdf(@_); } ## no critic
304 0     0 1 0 sub pdfile { return pdf(@_); } ## no critic
305              
306             sub pdf{
307 0     0 1 0 my ($self, $file, $pnum, @args) = @_;
308              
309 0         0 $self->{'S'} = PDFName('GoToR');
310 0         0 $self->{'F'} = PDFString($file, 'u');
311              
312             # $pnum should be a page number 1..
313 0 0       0 if ($pnum =~ /^\d+$/) {
314             # it's a page number
315 0         0 $self->{'D'} = $self->dest(PDFNum($pnum-1), @args);
316             } else {
317 0         0 croak "pdf action for a Named Destination is using a Named Destination!";
318             #if ($pnum =~ /^[#\/](.*)$/) {
319             # $pnum = $1;
320             #}
321             #$self->{'D'} = $self->dest(PDFString($pnum, 'u'), @args);
322             }
323              
324 0         0 return $self;
325             }
326              
327             =head3 uri, url
328              
329             $dest->uri($url)
330              
331             =over
332              
333             Defines the destination as launch-url (typically a web page) with uri C<$url>.
334             There are no options available.
335              
336             To go to a Named Destination and then immediately launch a web browser
337             is unusual, but possible.
338              
339             B<Alternate name:> C<url>
340              
341             Originally this method was C<url>, but PDF::API2 changed the name
342             to C<uri> to match the PDF command. "url" is retained for compatibility.
343              
344             =back
345              
346             =cut
347              
348 0     0 1 0 sub url { return uri(@_); } ## no critic
349              
350             sub uri {
351 0     0 1 0 my ($self, $uri, %opts) = @_;
352             # currently no options supported
353              
354 0         0 $self->{'S'} = PDFName('URI');
355 0         0 $self->{'URI'} = PDFString($uri, 'u');
356              
357 0         0 return $self;
358             }
359              
360             =head3 launch, file
361              
362             $dest->launch($file)
363              
364             =over
365              
366             Defines the destination as launch-file with filepath C<$file> and
367             page-fit options %opts. The target application is run. Note that this is
368             B<not> a PDF I<or> a browser file -- it is a usually a local application,
369             such as a text editor or photo viewer.
370             There are no options available.
371              
372             To go to a Named Destination and then immediately launch a local application
373             is unusual, but possible.
374              
375             B<Alternate name:> C<file>
376              
377             Originally this method was C<file>, but PDF::API2 changed the name
378             to C<launch> to match the PDF command. "file" is retained for compatibility.
379              
380             =back
381              
382             =cut
383              
384 0     0 1 0 sub file { return launch(@_); } ## no critic
385              
386             sub launch {
387 0     0 1 0 my ($self, $file, %opts) = @_;
388             # currently no options supported
389              
390 0         0 $self->{'S'} = PDFName('Launch');
391 0         0 $self->{'F'} = PDFString($file, 'u');
392              
393 0         0 return $self;
394             }
395              
396             # return an array as a hash, with key leading -'s removed
397             # assumes possible hash elements already as scalars or arrayrefs
398             # leading element(s) may be a list, turn it into one name=>[list]
399             sub list2hash {
400 5     5 0 14 my ($self, @args) = @_;
401              
402             # nothing passed in?
403 5 100       16 if (!@args) { return @args; }
  4         14  
404              
405 1         20 my %arg_counts = (
406             # key = location given by user
407             # value = required number of arguments
408             'xyz' => 3,
409             'fit' => 0,
410             'fith' => 1,
411             'fitv' => 1,
412             'fitr' => 4,
413             'fitb' => 0,
414             'fitbh' => 1,
415             'fitbv' => 1,
416             );
417 1         4 my $location;
418              
419             # try to match first element as location name
420 1 50       6 if ($args[0] =~ /^-(.*)$/) {
421 0         0 $args[0] = $1;
422             }
423 1         4 my $num_args = scalar(@args);
424 1         2 my $match = -1;
425 1         5 my @keylist = keys %arg_counts;
426 1         5 for (my $i=0; $i<@keylist; $i++) {
427 7 100       22 if ($args[0] eq $keylist[$i]) {
428 1         3 $match = $arg_counts{$keylist[$i]};
429             # note that if hash element and not list, minimum is 1 data value
430 1         3 last;
431             }
432             }
433 1 50       3 if ($match > -1) {
434             # first element is a location value, but is it a hash element or a list?
435 1         3 $location = $args[0];
436              
437 1 50 33     21 if (ref($args[1]) eq 'ARRAY' || $arg_counts{$location} == 1) {
    50          
    50          
    50          
438             # location and args supplied as hash element OR
439             # single value hash element or list, so entire array should be hash
440              
441             } elsif ($arg_counts{$location} == 3) {
442             # 3 elements should follow location, so as long as none
443             # are arrayrefs, we should be good. already checked that is not
444             # one arrayref for element
445 0         0 my @vals;
446 0         0 for (my $i=0; $i<3; $i++) {
447 0         0 $vals[$i] = $args[$i+1];
448 0 0       0 if (ref($vals[$i]) eq '') { next; }
  0         0  
449 0         0 croak "list of elements contains non-scalars in list2hash()!";
450             }
451 0         0 splice(@args, 0, 4);
452 0         0 unshift @args, ($location, \@vals);
453              
454             } elsif ($arg_counts{$location} == 4) {
455             # 4 elements should follow location, so as long as none
456             # are arrayrefs, we should be good. already checked that is not
457             # one arrayref for element
458 0         0 my @vals;
459 0         0 for (my $i=0; $i<4; $i++) {
460 0         0 $vals[$i] = $args[$i+1];
461 0 0       0 if (ref($vals[$i]) eq '') { next; }
  0         0  
462 0         0 croak "list of elements contains non-scalars in list2hash()!";
463             }
464 0         0 splice(@args, 0, 5);
465 0         0 unshift @args, ($location, \@vals);
466              
467             } elsif ($arg_counts{$location} == 0) {
468             # 0 data items, but if is hash element, will have a dummy value
469 1 50       4 if ($num_args%2) {
470             # assume was list, with no args values following location
471 1         3 splice(@args, 0, 1); # drop location
472 1         4 unshift @args, ($location, 1); # hash element with dummy 1
473             } else {
474             # assume was hash element, with dummy value after location
475             # e.g., 'fit'=>1
476             }
477             }
478             }
479              
480             # any pair already string, scalar or arrayref can simply be copied over
481 1 50       4 if (scalar(@args)%2) {
482 0         0 croak "list2hash() sees hash with odd number of elements!";
483             }
484 1         5 for (my $i=0; $i<scalar(@args)-1; $i+=2) {
485 1 50       12 if (ref($args[$i]) ne '') {
486 0         0 croak "list2hash() sees hash element with non scalar key.";
487             }
488 1 50 33     6 if (ref($args[$i+1]) ne '' && ref($args[$i+1]) ne 'ARRAY') {
489 0         0 croak "argument structure list2hash() can't handle!";
490             }
491             # elements look OK for hash, remove leading -'s
492 1 50       6 if ($args[$i] =~ /^-(.*)$/) {
493 0         0 $args[$i] = $1;
494             }
495             }
496              
497 1         8 return @args; # see as %opts by caller
498             }
499              
500             1;