File Coverage

blib/lib/PDF/Builder/Outline.pm
Criterion Covered Total %
statement 171 233 73.3
branch 74 112 66.0
condition 14 34 41.1
subroutine 26 37 70.2
pod 22 27 81.4
total 307 443 69.3


line stmt bran cond sub pod time code
1             package PDF::Builder::Outline;
2              
3 2     2   15 use base 'PDF::Builder::Basic::PDF::Dict';
  2         4  
  2         198  
4              
5 2     2   11 use strict;
  2         8  
  2         36  
6 2     2   9 use warnings;
  2         5  
  2         96  
7              
8             our $VERSION = '3.024'; # VERSION
9             our $LAST_UPDATE = '3.024'; # manually update whenever code is changed
10              
11 2     2   12 use Carp qw(croak);
  2         3  
  2         92  
12 2     2   12 use PDF::Builder::Basic::PDF::Utils;
  2         3  
  2         148  
13 2     2   11 use Scalar::Util qw(weaken);
  2         2  
  2         4744  
14              
15             =head1 NAME
16              
17             PDF::Builder::Outline - Manage PDF outlines (a.k.a. I)
18              
19             =head1 SYNOPSIS
20              
21             # Get/create the top-level outline tree
22             my $outlines = $pdf->outline();
23              
24             # Add an entry
25             my $item = $outlines->outline();
26             $item->title('First Page');
27             $item->destination($pdf->open_page(1)); # or dest(...)
28              
29             =head1 METHODS
30              
31             =over
32              
33             =item $outline = PDF::Builder::Outline->new($api, $parent, $prev)
34              
35             Returns a new outline object (called from $outlines->outline()).
36              
37             =cut
38              
39             sub new {
40 17     17 1 29 my ($class, $api, $parent, $prev) = @_;
41 17         37 my $self = $class->SUPER::new();
42              
43 17 100       34 $self->{'Parent'} = $parent if defined $parent;
44 17 50       28 $self->{'Prev'} = $prev if defined $prev;
45 17         22 $self->{' api'} = $api;
46 17         42 weaken $self->{' api'};
47 17 100       42 weaken $self->{'Parent'} if defined $parent;
48             #weaken $self->{'Prev'} if defined $prev; # not in API2
49              
50 17         31 return $self;
51             }
52              
53             =back
54              
55             =head2 Examine the Outline Tree
56              
57             =over
58              
59             =item $boolean = $outline->has_children()
60              
61             Return true if the current outline item has children (child items).
62              
63             =cut
64              
65             sub has_children {
66 67     67 1 69 my $self = shift();
67              
68             # Opened by PDF::Builder
69 67 100       114 return 1 if exists $self->{'First'};
70              
71             # Created by PDF::Builder
72 54 100       79 return @{$self->{' children'}} > 0 if exists $self->{' children'};
  18         42  
73              
74 36         69 return;
75             }
76              
77             =item $integer = $outline->count()
78              
79             Return the number of descendants that are visible when the current outline item
80             is open (expanded).
81              
82             =cut
83              
84             sub count {
85 26     26 1 27 my $self = shift();
86              
87             # Set count to the number of descendant items that will be visible when the
88             # current item is open.
89 26         30 my $count = 0;
90 26 100       41 if ($self->has_children()) {
91 20 100       39 $self->_load_children() unless exists $self->{' children'};
92 20         21 $count += @{$self->{' children'}};
  20         27  
93 20         22 foreach my $child (@{$self->{' children'}}) {
  20         30  
94 39 100       46 next unless $child->has_children();
95 10 100       17 next unless $child->is_open();
96 6         14 $count += $child->count();
97             }
98             }
99              
100 26 100       42 if ($count) {
101 20 100       37 $self->{'Count'} = PDFNum($self->is_open() ? $count : -$count);
102             }
103              
104 26         67 return $count;
105             }
106             #sub count { # older version
107             # my $self = shift();
108             #
109             # my $count = scalar @{$self->{' children'} || []};
110             # $count += $_->count() for @{$self->{' children'}};
111             # $self->{'Count'} = PDFNum($self->{' closed'}? -$count: $count) if $count > 0;
112             # return $count;
113             #}
114              
115             sub _load_children {
116 2     2   2 my $self = shift();
117 2         3 my $item = $self->{'First'};
118 2 50       5 return unless $item;
119 2         5 $item->realise();
120 2         3 bless $item, __PACKAGE__;
121              
122 2         2 push @{$self->{' children'}}, $item;
  2         6  
123 2         4 while ($item->next()) {
124 2         4 $item = $item->next();
125 2         6 $item->realise();
126 2         3 bless $item, __PACKAGE__;
127 2         3 push @{$self->{' children'}}, $item;
  2         6  
128             }
129 2         3 return $self;
130             }
131              
132             =item $child = $outline->first()
133              
134             Return the first child of the current outline level, if one exists.
135              
136             =cut
137              
138             sub first {
139 19     19 1 26 my $self = shift();
140 19 100 66     61 if (defined $self->{' children'} and defined $self->{' children'}->[0]) {
141 14         22 $self->{'First'} = $self->{' children'}->[0];
142             }
143             #weaken $self->{'First'}; # not in API2
144 19         37 return $self->{'First'};
145             }
146              
147             =item $child = $outline->last()
148              
149             Return the last child of the current outline level, if one exists.
150              
151             =cut
152              
153             sub last {
154 11     11 1 16 my $self = shift();
155 11 100 66     37 if (defined $self->{' children'} and defined $self->{' children'}->[-1]) {
156 6         11 $self->{'Last'} = $self->{' children'}->[-1];
157             }
158             #weaken $self->{'Last'}; # not in API2
159 11         18 return $self->{'Last'};
160             }
161              
162             =item $parent = $outline->parent()
163              
164             Return the parent of the current item, if not at the top level of the outline
165             tree.
166              
167             =cut
168              
169             sub parent {
170 11     11 1 12 my $self = shift();
171 11 50       23 $self->{'Parent'} = shift() if defined $_[0];
172             #weaken $self->{'Parent'}; # not in API2
173 11         28 return $self->{'Parent'};
174             }
175              
176             =item $sibling = $outline->prev()
177              
178             Return the previous item of the current level of the outline tree.
179              
180             =cut
181              
182             sub prev {
183 25     25 1 35 my $self = shift();
184 25 100       45 $self->{'Prev'} = shift() if defined $_[0];
185             #weaken $self->{'Prev'}; # not in API2
186 25         53 return $self->{'Prev'};
187             }
188              
189             =item $sibling = $outline->next()
190              
191             Return the next item of the current level of the outline tree.
192              
193             =cut
194              
195             sub next {
196 67     67 1 84 my $self = shift();
197 67 100       106 $self->{'Next'} = shift() if defined $_[0];
198             #weaken $self->{'Next'}; # not in API2
199 67         115 return $self->{'Next'};
200             }
201              
202             =back
203              
204             =head2 Modify the Outline Tree
205              
206             =over
207              
208             =item $child_outline = $parent_outline->outline()
209              
210             Returns a new sub-outline (nested outline) added at the end of the
211             current outline's children.
212              
213             =cut
214              
215             sub outline {
216 10     10 1 35 my $self = shift();
217              
218 10         21 my $child = PDF::Builder::Outline->new($self->{' api'}, $self);
219 10   100     32 $self->{' children'} //= [];
220             # it's not clear whether self->{children} will change by prev() call,
221             # so leave as done in PDF::API2
222 10 100       10 $child->prev($self->{' children'}->[-1]) if @{ $self->{' children'} };
  10         24  
223 10 100       11 $self->{' children'}->[-1]->next($child) if @{ $self->{' children'} };
  10         21  
224 10         11 push @{$self->{' children'}}, $child;
  10         14  
225             $self->{' api'}->{'pdf'}->new_obj($child)
226 10 50       24 unless $child->is_obj($self->{' api'}->{'pdf'});
227              
228 10         33 return $child;
229             }
230              
231             =item $sibling = $outline->insert_after()
232              
233             Add an outline item immediately following the current item.
234              
235             =cut
236              
237             sub insert_after {
238 2     2 1 5 my $self = shift();
239              
240 2         6 my $sibling = PDF::Builder::Outline->new($self->{' api'}, $self->parent());
241 2         5 $sibling->next($self->next());
242 2 100       5 $self->next->prev($sibling) if $self->next();
243 2         6 $self->next($sibling);
244 2         6 $sibling->prev($self);
245 2 50       6 unless ($sibling->is_obj($self->{' api'}->{'pdf'})) {
246 2         8 $self->{' api'}->{'pdf'}->new_obj($sibling);
247             }
248 2         5 $self->parent->_reset_children();
249 2         6 return $sibling;
250             }
251              
252             =item $sibling = $outline->insert_before()
253              
254             Add an outline item immediately preceding the current item.
255              
256             =cut
257              
258             sub insert_before {
259 2     2 1 5 my $self = shift();
260              
261 2         5 my $sibling = PDF::Builder::Outline->new($self->{' api'}, $self->parent());
262 2         6 $sibling->prev($self->prev());
263 2 100       4 $self->prev->next($sibling) if $self->prev();
264 2         4 $self->prev($sibling);
265 2         3 $sibling->next($self);
266 2 50       4 unless ($sibling->is_obj($self->{' api'}->{'pdf'})) {
267 2         6 $self->{' api'}->{'pdf'}->new_obj($sibling);
268             }
269 2         5 $self->parent->_reset_children();
270 2         4 return $sibling;
271             }
272              
273             sub _reset_children {
274 4     4   7 my $self = shift();
275 4         7 my $item = $self->first();
276 4         7 $self->{' children'} = [];
277 4 50       10 return unless $item;
278              
279 4         5 push @{$self->{' children'}}, $item;
  4         8  
280 4         9 while ($item->next()) {
281 16         21 $item = $item->next();
282 16         17 push @{$self->{' children'}}, $item;
  16         21  
283             }
284 4         6 return $self;
285             }
286              
287             =item $outline->delete()
288              
289             Remove the current outline item from the outline tree. If the item has any
290             children, they will effectively be deleted as well, since they will no longer
291             be linked.
292              
293             =cut
294              
295             sub delete {
296 1     1 1 2 my $self = shift();
297              
298 1         4 my $prev = $self->prev();
299 1         2 my $next = $self->next();
300 1 50       3 $prev->next($next) if defined $prev;
301 1 50       2 $next->prev($prev) if defined $next;
302              
303 1         2 my $siblings = $self->parent->{' children'};
304 1         2 @$siblings = grep { $_ ne $self } @$siblings;
  1         4  
305 1 50       3 delete $self->parent->{' children'} unless $self->parent->has_children();
306              
307 1         3 return;
308             }
309              
310             =item $boolean = $outline->is_open() # Get
311              
312             =item $outline = $outline->is_open($boolean) # Set
313              
314             Get/set whether the outline is expanded (open) or collapsed (closed).
315              
316             =cut
317              
318             sub is_open {
319 33     33 1 40 my $self = shift();
320              
321             # Get
322 33 100       60 unless (@_) {
323             # Created by PDF::Builder
324 32 50       72 return $self->{' closed'} ? 0 : 1 if exists $self->{' closed'};
    100          
325              
326             # Opened by PDF::Builder
327 26 100       82 return $self->{'Count'}->val() > 0 if exists $self->{'Count'};
328              
329             # Default
330 7         31 return 1;
331             }
332              
333             # Set
334 1         1 my $is_open = shift();
335 1         5 $self->{' closed'} = (not $is_open);
336              
337 1         2 return $self;
338             }
339              
340             =item $outline->open()
341              
342             Set the status of the outline to open (i.e., expanded).
343              
344             This is an B method to using is_open(true).
345              
346             =cut
347              
348             # deprecated in API2
349             sub open {
350 0     0 1 0 my $self = shift();
351 0         0 delete $self->{' closed'};
352 0         0 return $self;
353             }
354              
355             =item $outline->closed()
356              
357             Set the status of the outline to closed (i.e., collapsed).
358              
359             This is an B method to using is_open(false).
360              
361             =cut
362              
363             # deprecated in API2
364             sub closed {
365 0     0 1 0 my $self = shift();
366 0         0 $self->{' closed'} = 1;
367 0         0 return $self;
368             }
369              
370             =back
371              
372             =head2 Set Outline Attributes
373              
374             =over
375              
376             =item $title = $outline->title() # Get
377              
378             =item $outline = $outline->title($text) # Set
379              
380             Get/set the title of the outline item.
381              
382             =cut
383              
384             sub title {
385 4     4 1 13 my $self = shift();
386              
387             # Get
388 4 100       9 unless (@_) {
389 1 50       4 return unless $self->{'Title'};
390 1         3 return $self->{'Title'}->val();
391             }
392              
393             # Set
394 3         4 my $text = shift();
395 3         8 $self->{'Title'} = PDFString($text, 'o');
396 3         5 return $self;
397             }
398              
399             =item $outline->dest($page_object, %position)
400              
401             =item $outline->dest($page_object)
402              
403             Sets the destination page and optional position of the outline.
404              
405             %position can be any of those listed in L.
406              
407             "xyz" is the B fit setting, with position (left and top) and zoom
408             the same as the calling page's.
409              
410             =item $outline->dest($name, %position)
411              
412             =item $outline->dest($name)
413              
414             Connect the Outline to a "Named Destination" defined elsewhere,
415             and optional positioning as described above.
416              
417             =cut
418              
419             sub dest {
420 2     2 1 8 my ($self, $page, %position) = @_;
421 2         3 delete $self->{'A'};
422              
423 2 50       5 if (ref($page)) {
424 2         6 $self = $self->_fit($page, %position);
425             } else {
426 0         0 $self->{'Dest'} = PDFString($page, 'n');
427             }
428              
429 2         10 return $self;
430             }
431              
432             # process destination, including position setting, with default of xyz undef*3
433            
434             sub _fit {
435 2     2   3 my ($self, $destination, %position) = @_;
436             # copy dashed names over to preferred non-dashed names
437 2 50 33     5 if (defined $position{'-fit'} && !defined $position{'fit'}) { $position{'fit'} = delete($position{'-fit'}); }
  0         0  
438 2 50 33     5 if (defined $position{'-fith'} && !defined $position{'fith'}) { $position{'fith'} = delete($position{'-fith'}); }
  0         0  
439 2 50 33     5 if (defined $position{'-fitb'} && !defined $position{'fitb'}) { $position{'fitb'} = delete($position{'-fitb'}); }
  0         0  
440 2 50 33     5 if (defined $position{'-fitbh'} && !defined $position{'fitbh'}) { $position{'fitbh'} = delete($position{'-fitbh'}); }
  0         0  
441 2 50 33     4 if (defined $position{'-fitv'} && !defined $position{'fitv'}) { $position{'fitv'} = delete($position{'-fitv'}); }
  0         0  
442 2 50 33     13 if (defined $position{'-fitbv'} && !defined $position{'fitbv'}) { $position{'fitbv'} = delete($position{'-fitbv'}); }
  0         0  
443 2 50 33     5 if (defined $position{'-fitr'} && !defined $position{'fitr'}) { $position{'fitr'} = delete($position{'-fitr'}); }
  0         0  
444 2 50 33     5 if (defined $position{'-xyz'} && !defined $position{'xyz'}) { $position{'xyz'} = delete($position{'-xyz'}); }
  0         0  
445              
446 2 50       12 if (defined $position{'fit'}) {
    50          
    50          
    50          
    50          
    50          
    50          
    50          
447 0         0 $self->{'Dest'} = PDFArray($destination, PDFName('Fit'));
448             } elsif (defined $position{'fith'}) {
449 0         0 $self->{'Dest'} = PDFArray($destination, PDFName('FitH'), PDFNum($position{'fith'}));
450             } elsif (defined $position{'fitb'}) {
451 0         0 $self->{'Dest'} = PDFArray($destination, PDFName('FitB'));
452             } elsif (defined $position{'fitbh'}) {
453 0         0 $self->{'Dest'} = PDFArray($destination, PDFName('FitBH'), PDFNum($position{'fitbh'}));
454             } elsif (defined $position{'fitv'}) {
455 0         0 $self->{'Dest'} = PDFArray($destination, PDFName('FitV'), PDFNum($position{'fitv'}));
456             } elsif (defined $position{'fitbv'}) {
457 0         0 $self->{'Dest'} = PDFArray($destination, PDFName('FitBV'), PDFNum($position{'fitbv'}));
458             } elsif (defined $position{'fitr'}) {
459 0 0       0 croak "Insufficient parameters to fitr => []) " unless scalar @{$position{'fitr'}} == 4;
  0         0  
460 0         0 $self->{'Dest'} = PDFArray($destination, PDFName('FitR'), map {PDFNum($_)} @{$position{'fitr'}});
  0         0  
  0         0  
461             } elsif (defined $position{'xyz'}) {
462 0 0       0 croak "Insufficient parameters to xyz => []) " unless scalar @{$position{'xyz'}} == 3;
  0         0  
463 0 0       0 $self->{'Dest'} = PDFArray($destination, PDFName('XYZ'), map {defined $_? PDFNum($_): PDFNull()} @{$position{'xyz'}});
  0         0  
  0         0  
464             } else {
465             # no "fit" option found. use default.
466 2         11 $position{'xyz'} = [undef,undef,undef];
467 2 50       7 $self->{'Dest'} = PDFArray($destination, PDFName('XYZ'), map {defined $_? PDFNum($_): PDFNull()} @{$position{'xyz'}});
  6         13  
  2         5  
468             }
469              
470 2         5 return $self;
471             }
472              
473             =item $outline = $outline->destination($destination, $location, @args)
474              
475             Set the destination page and optional position of the outline. C<$location> and
476             C<@args> are as defined in L.
477              
478             C<$destination> can optionally be the name of a named destination defined
479             elsewhere.
480              
481             This is an B for changes made in PDF::API2; it maintains
482             compatibility with the new PDF::API2 version.
483              
484             =cut
485              
486             sub _destination {
487 0     0   0 require PDF::Builder::NamedDestination;
488 0         0 return PDF::Builder::NamedDestination::_destination(@_);
489             }
490              
491             sub destination {
492 0     0 1 0 my ($self, $destination, $location, @args) = @_;
493              
494             # Remove an existing action dictionary
495 0         0 delete $self->{'A'};
496              
497 0 0       0 if (ref($destination)) {
498             # Page Destination
499 0         0 $self->{'Dest'} = _destination($destination, $location, @args);
500             }
501             else {
502             # Named Destination
503 0         0 $self->{'Dest'} = PDFStr($destination);
504             }
505              
506 0         0 return $self;
507             }
508              
509             =back
510              
511             =head2 Destination targets
512              
513             =over
514              
515             =item $outline->uri($url)
516              
517             Defines the outline as launch-url with url C<$url>, typically a web page.
518              
519             B C
520              
521             Either C or C may be used; C is for compatibility with PDF::API2.
522              
523             =cut
524              
525 0     0 0 0 sub url { return uri(@_); } # alternate name
526              
527             sub uri {
528 0     0 1 0 my ($self, $url) = @_;
529              
530 0         0 delete $self->{'Dest'};
531 0         0 $self->{'A'} = PDFDict();
532 0         0 $self->{'A'}->{'S'} = PDFName('URI');
533 0         0 $self->{'A'}->{'URI'} = PDFString($url, 'u');
534              
535 0         0 return $self;
536             }
537              
538             =item $outline->launch($file)
539              
540             Defines the outline as launch-file with filepath C<$file>. This is typically
541             a local application or file.
542              
543             B C
544              
545             Either C or C may be used; C is for compatibility with PDF::API2.
546              
547             =cut
548              
549 0     0 0 0 sub file { return launch(@_); } # alternate name
550              
551             sub launch {
552 0     0 1 0 my ($self, $file) = @_;
553              
554 0         0 delete $self->{'Dest'};
555 0         0 $self->{'A'} = PDFDict();
556 0         0 $self->{'A'}->{'S'} = PDFName('Launch');
557 0         0 $self->{'A'}->{'F'} = PDFString($file, 'f');
558              
559 0         0 return $self;
560             }
561              
562             =item $outline->pdf($pdffile, $page_number, %position, %args)
563              
564             =item $outline->pdf($pdffile, $page_number)
565              
566             Defines the destination of the outline as a PDF-file with filepath
567             C<$pdffile>, on page C<$pagenum> (default 0), and position C<%position>
568             (same as dest()).
569              
570             B C and C
571              
572             Either C or C (or the older C) may be used; C is
573             for compatibility with PDF::API2. B that PDF::API2 now uses a string name
574             for the location, and an array of dimensions, etc., rather than the old hash
575             element name => dimensions (as still used here in PDF::Builder).
576              
577             =cut
578              
579 0     0 0 0 sub pdf_file { return pdf(@_); } # alternative method
580 0     0 0 0 sub pdfile { return pdf(@_); } # alternative method (older)
581              
582             sub pdf {
583 0     0 1 0 my ($self, $file, $page_number, %position) = @_;
584              
585 0         0 delete $self->{'Dest'};
586 0         0 $self->{'A'} = PDFDict();
587 0         0 $self->{'A'}->{'S'} = PDFName('GoToR');
588 0         0 $self->{'A'}->{'F'} = PDFString($file, 'f');
589 0   0     0 $self->{'A'}->{'D'} = $self->_fit(PDFNum($page_number // 0), %position);
590            
591 0         0 return $self;
592             }
593              
594             # internal routine
595             sub fix_outline {
596 9     9 0 15 my ($self) = @_;
597              
598 9         20 $self->first();
599 9         17 $self->last();
600 9         19 $self->count();
601 9         11 return;
602             }
603              
604             #sub out_obj {
605             # my ($self, @param) = @_;
606             #
607             # $self->fix_outline();
608             # return $self->SUPER::out_obj(@param);
609             #}
610              
611             sub outobjdeep {
612             # my ($self, @param) = @_;
613             #
614             # $self->fix_outline();
615             # foreach my $k (qw/ api apipdf apipage /) {
616             # $self->{" $k"} = undef;
617             # delete($self->{" $k"});
618             # }
619             # my @ret = $self->SUPER::outobjdeep(@param);
620             # foreach my $k (qw/ First Parent Next Last Prev /) {
621             # $self->{$k} = undef;
622             # delete($self->{$k});
623             # }
624             # return @ret;
625 9     9 1 13 my $self = shift();
626 9         42 $self->fix_outline();
627 9         20 return $self->SUPER::outobjdeep(@_);
628             }
629              
630             =back
631              
632             =cut
633              
634             1;