File Coverage

blib/lib/PDF/Builder/Outline.pm
Criterion Covered Total %
statement 170 224 75.8
branch 73 108 67.5
condition 14 34 41.1
subroutine 26 35 74.2
pod 25 26 96.1
total 308 427 72.1


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