File Coverage

blib/lib/PDF/Builder/Outline.pm
Criterion Covered Total %
statement 171 225 76.0
branch 74 110 67.2
condition 14 34 41.1
subroutine 26 35 74.2
pod 21 26 80.7
total 306 430 71.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         3  
  2         255  
4              
5 2     2   16 use strict;
  2         4  
  2         49  
6 2     2   9 use warnings;
  2         4  
  2         108  
7              
8             our $VERSION = '3.025'; # VERSION
9             our $LAST_UPDATE = '3.024'; # manually update whenever code is changed
10              
11 2     2   11 use Carp qw(croak);
  2         9  
  2         157  
12 2     2   15 use PDF::Builder::Basic::PDF::Utils;
  2         7  
  2         167  
13 2     2   12 use Scalar::Util qw(weaken);
  2         6  
  2         5299  
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->dest($pdf->open_page(1), fit-def);
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 32 my ($class, $api, $parent, $prev) = @_;
41 17         43 my $self = $class->SUPER::new();
42              
43 17 100       39 $self->{'Parent'} = $parent if defined $parent;
44 17 50       30 $self->{'Prev'} = $prev if defined $prev;
45 17         24 $self->{' api'} = $api;
46 17         44 weaken $self->{' api'};
47 17 100       49 weaken $self->{'Parent'} if defined $parent;
48             #weaken $self->{'Prev'} if defined $prev; # not in API2
49              
50 17         26 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 89 my $self = shift();
67              
68             # Opened by PDF::Builder
69 67 100       118 return 1 if exists $self->{'First'};
70              
71             # Created by PDF::Builder
72 54 100       82 return @{$self->{' children'}} > 0 if exists $self->{' children'};
  18         43  
73              
74 36         66 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 39 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         32 my $count = 0;
90 26 100       55 if ($self->has_children()) {
91 20 100       44 $self->_load_children() unless exists $self->{' children'};
92 20         24 $count += @{$self->{' children'}};
  20         29  
93 20         25 foreach my $child (@{$self->{' children'}}) {
  20         35  
94 39 100       54 next unless $child->has_children();
95 10 100       25 next unless $child->is_open();
96 6         17 $count += $child->count();
97             }
98             }
99              
100 26 100       48 if ($count) {
101 20 100       40 $self->{'Count'} = PDFNum($self->is_open() ? $count : -$count);
102             }
103              
104 26         74 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   4 my $self = shift();
117 2         3 my $item = $self->{'First'};
118 2 50       4 return unless $item;
119 2         7 $item->realise();
120 2         4 bless $item, __PACKAGE__;
121              
122 2         3 push @{$self->{' children'}}, $item;
  2         5  
123 2         8 while ($item->next()) {
124 2         4 $item = $item->next();
125 2         7 $item->realise();
126 2         3 bless $item, __PACKAGE__;
127 2         4 push @{$self->{' children'}}, $item;
  2         7  
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 28 my $self = shift();
140 19 100 66     71 if (defined $self->{' children'} and defined $self->{' children'}->[0]) {
141 14         27 $self->{'First'} = $self->{' children'}->[0];
142             }
143             #weaken $self->{'First'}; # not in API2
144 19         36 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 18 my $self = shift();
155 11 100 66     38 if (defined $self->{' children'} and defined $self->{' children'}->[-1]) {
156 6         10 $self->{'Last'} = $self->{' children'}->[-1];
157             }
158             #weaken $self->{'Last'}; # not in API2
159 11         19 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 19 my $self = shift();
171 11 50       21 $self->{'Parent'} = shift() if defined $_[0];
172             #weaken $self->{'Parent'}; # not in API2
173 11         49 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 32 my $self = shift();
184 25 100       51 $self->{'Prev'} = shift() if defined $_[0];
185             #weaken $self->{'Prev'}; # not in API2
186 25         57 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       99 $self->{'Next'} = shift() if defined $_[0];
198             #weaken $self->{'Next'}; # not in API2
199 67         107 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 39 my $self = shift();
217              
218 10         23 my $child = PDF::Builder::Outline->new($self->{' api'}, $self);
219 10   100     37 $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       11 $child->prev($self->{' children'}->[-1]) if @{ $self->{' children'} };
  10         30  
223 10 100       14 $self->{' children'}->[-1]->next($child) if @{ $self->{' children'} };
  10         23  
224 10         12 push @{$self->{' children'}}, $child;
  10         19  
225             $self->{' api'}->{'pdf'}->new_obj($child)
226 10 50       26 unless $child->is_obj($self->{' api'}->{'pdf'});
227              
228 10         39 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 7 my $self = shift();
239              
240 2         8 my $sibling = PDF::Builder::Outline->new($self->{' api'}, $self->parent());
241 2         7 $sibling->next($self->next());
242 2 100       6 $self->next->prev($sibling) if $self->next();
243 2         7 $self->next($sibling);
244 2         6 $sibling->prev($self);
245 2 50       7 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         6 my $sibling = PDF::Builder::Outline->new($self->{' api'}, $self->parent());
262 2         6 $sibling->prev($self->prev());
263 2 100       5 $self->prev->next($sibling) if $self->prev();
264 2         6 $self->prev($sibling);
265 2         6 $sibling->next($self);
266 2 50       7 unless ($sibling->is_obj($self->{' api'}->{'pdf'})) {
267 2         8 $self->{' api'}->{'pdf'}->new_obj($sibling);
268             }
269 2         5 $self->parent->_reset_children();
270 2         5 return $sibling;
271             }
272              
273             sub _reset_children {
274 4     4   6 my $self = shift();
275 4         10 my $item = $self->first();
276 4         10 $self->{' children'} = [];
277 4 50       9 return unless $item;
278              
279 4         7 push @{$self->{' children'}}, $item;
  4         7  
280 4         9 while ($item->next()) {
281 16         24 $item = $item->next();
282 16         18 push @{$self->{' children'}}, $item;
  16         27  
283             }
284 4         7 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 3 my $self = shift();
297              
298 1         6 my $prev = $self->prev();
299 1         3 my $next = $self->next();
300 1 50       3 $prev->next($next) if defined $prev;
301 1 50       3 $next->prev($prev) if defined $next;
302              
303 1         5 my $siblings = $self->parent->{' children'};
304 1         3 @$siblings = grep { $_ ne $self } @$siblings;
  1         4  
305 1 50       4 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 39 my $self = shift();
320              
321             # Get
322 33 100       59 unless (@_) {
323             # Created by PDF::Builder
324 32 50       69 return $self->{' closed'} ? 0 : 1 if exists $self->{' closed'};
    100          
325              
326             # Opened by PDF::Builder
327 26 100       67 return $self->{'Count'}->val() > 0 if exists $self->{'Count'};
328              
329             # Default
330 7         26 return 1;
331             }
332              
333             # Set
334 1         3 my $is_open = shift();
335 1         3 $self->{' closed'} = (not $is_open);
336              
337 1         3 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 18 my $self = shift();
386              
387             # Get
388 4 100       9 unless (@_) {
389 1 50       5 return unless $self->{'Title'};
390 1         5 return $self->{'Title'}->val();
391             }
392              
393             # Set
394 3         5 my $text = shift();
395 3         9 $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 11 my ($self, $page, %position) = @_;
421 2         4 delete $self->{'A'};
422              
423 2 50       5 if (ref($page)) {
424 2         9 $self = $self->_fit($page, %position);
425             } else {
426 0         0 $self->{'Dest'} = PDFString($page, 'n');
427             }
428              
429 2         5 return $self;
430             }
431              
432             # process destination, including position setting, with default of xyz undef*3
433            
434             sub _fit {
435 2     2   6 my ($self, $destination, %position) = @_;
436             # copy dashed names over to preferred non-dashed names
437 2 50 33     8 if (defined $position{'-fit'} && !defined $position{'fit'}) { $position{'fit'} = delete($position{'-fit'}); }
  0         0  
438 2 50 33     6 if (defined $position{'-fith'} && !defined $position{'fith'}) { $position{'fith'} = delete($position{'-fith'}); }
  0         0  
439 2 50 33     6 if (defined $position{'-fitb'} && !defined $position{'fitb'}) { $position{'fitb'} = delete($position{'-fitb'}); }
  0         0  
440 2 50 33     7 if (defined $position{'-fitbh'} && !defined $position{'fitbh'}) { $position{'fitbh'} = delete($position{'-fitbh'}); }
  0         0  
441 2 50 33     6 if (defined $position{'-fitv'} && !defined $position{'fitv'}) { $position{'fitv'} = delete($position{'-fitv'}); }
  0         0  
442 2 50 33     6 if (defined $position{'-fitbv'} && !defined $position{'fitbv'}) { $position{'fitbv'} = delete($position{'-fitbv'}); }
  0         0  
443 2 50 33     13 if (defined $position{'-fitr'} && !defined $position{'fitr'}) { $position{'fitr'} = delete($position{'-fitr'}); }
  0         0  
444 2 50 33     13 if (defined $position{'-xyz'} && !defined $position{'xyz'}) { $position{'xyz'} = delete($position{'-xyz'}); }
  0         0  
445              
446 2 50       15 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         6 $position{'xyz'} = [undef,undef,undef];
467 2 50       7 $self->{'Dest'} = PDFArray($destination, PDFName('XYZ'), map {defined $_? PDFNum($_): PDFNull()} @{$position{'xyz'}});
  6         14  
  2         4  
468             }
469              
470 2         6 return $self;
471             }
472              
473             =back
474              
475             =head2 Destination targets
476              
477             =over
478              
479             =item $outline->uri($url)
480              
481             Defines the outline as launch-url with url C<$url>, typically a web page.
482              
483             B C
484              
485             Either C or C may be used; C is for compatibility with PDF::API2.
486              
487             =cut
488              
489 0     0 0 0 sub url { return uri(@_); } # alternate name
490              
491             sub uri {
492 0     0 1 0 my ($self, $url, %opts) = @_;
493             # no current opts
494              
495 0         0 delete $self->{'Dest'};
496 0         0 $self->{'A'} = PDFDict();
497 0         0 $self->{'A'}->{'S'} = PDFName('URI');
498 0         0 $self->{'A'}->{'URI'} = PDFString($url, 'u');
499              
500 0         0 return $self;
501             }
502              
503             =item $outline->launch($file)
504              
505             Defines the outline as launch-file with filepath C<$file>. This is typically
506             a local application or file.
507              
508             B C
509              
510             Either C or C may be used; C is for compatibility with PDF::API2.
511              
512             =cut
513              
514 0     0 0 0 sub file { return launch(@_); } # alternate name
515              
516             sub launch {
517 0     0 1 0 my ($self, $file, %opts) = @_;
518             # no current opts
519              
520 0         0 delete $self->{'Dest'};
521 0         0 $self->{'A'} = PDFDict();
522 0         0 $self->{'A'}->{'S'} = PDFName('Launch');
523 0         0 $self->{'A'}->{'F'} = PDFString($file, 'f');
524              
525 0         0 return $self;
526             }
527              
528             =item $outline->pdf($pdffile, $page_number, %position, %args)
529              
530             =item $outline->pdf($pdffile, $page_number)
531              
532             Defines the destination of the outline as a PDF-file with filepath
533             C<$pdffile>, on page C<$pagenum> (default 0), and position C<%position>
534             (same as dest()).
535              
536             B C and C
537              
538             Either C or C (or the older C) may be used; C is
539             for compatibility with PDF::API2.
540              
541             =cut
542              
543 0     0 0 0 sub pdf_file { return pdf(@_); } # alternative method
544 0     0 0 0 sub pdfile { return pdf(@_); } # alternative method (older)
545              
546             sub pdf {
547 0     0 1 0 my ($self, $file, $page_number, %position) = @_;
548              
549 0         0 delete $self->{'Dest'};
550 0         0 $self->{'A'} = PDFDict();
551 0         0 $self->{'A'}->{'S'} = PDFName('GoToR');
552 0         0 $self->{'A'}->{'F'} = PDFString($file, 'f');
553 0   0     0 $self->{'A'}->{'D'} = $self->_fit(PDFNum($page_number // 0), %position);
554            
555 0         0 return $self;
556             }
557              
558             # internal routine
559             sub fix_outline {
560 9     9 0 13 my ($self) = @_;
561              
562 9         22 $self->first();
563 9         53 $self->last();
564 9         24 $self->count();
565 9         12 return;
566             }
567              
568             #sub out_obj {
569             # my ($self, @param) = @_;
570             #
571             # $self->fix_outline();
572             # return $self->SUPER::out_obj(@param);
573             #}
574              
575             sub outobjdeep {
576             # my ($self, @param) = @_;
577             #
578             # $self->fix_outline();
579             # foreach my $k (qw/ api apipdf apipage /) {
580             # $self->{" $k"} = undef;
581             # delete($self->{" $k"});
582             # }
583             # my @ret = $self->SUPER::outobjdeep(@param);
584             # foreach my $k (qw/ First Parent Next Last Prev /) {
585             # $self->{$k} = undef;
586             # delete($self->{$k});
587             # }
588             # return @ret;
589 9     9 1 11 my $self = shift();
590 9         22 $self->fix_outline();
591 9         21 return $self->SUPER::outobjdeep(@_);
592             }
593              
594             =back
595              
596             =cut
597              
598             1;