1package PDF::Builder::Outline;
2
3use base 'PDF::Builder::Basic::PDF::Dict';
4
5use strict;
6use warnings;
7
8our $VERSION = '3.023'; # VERSION
9our $LAST_UPDATE = '3.020'; # manually update whenever code is changed
10
11use Carp qw(croak);
12use PDF::Builder::Basic::PDF::Utils;
13use Scalar::Util qw(weaken);
14
15=head1 NAME
16
17PDF::Builder::Outline - Manage PDF outlines (a.k.a. I<bookmarks>)
18
19=head1 METHODS
20
21=over
22
23=item $outline = PDF::Builder::Outline->new($api, $parent, $prev)
24
25Returns a new outline object (called from $outlines->outline()).
26
27=cut
28
29sub new {
30    my ($class, $api, $parent, $prev) = @_;
31    my $self = $class->SUPER::new();
32
33    $self->{'Parent'} = $parent if defined $parent;
34    $self->{'Prev'}   = $prev   if defined $prev;
35    $self->{' api'}   = $api;
36    weaken $self->{' api'};
37    weaken $self->{'Parent'} if defined $parent;
38    weaken $self->{'Prev'} if defined $prev;
39
40    return $self;
41}
42
43# unused?
44sub parent {
45    my $self = shift();
46    $self->{'Parent'} = shift() if defined $_[0];
47    weaken $self->{'Parent'};
48    return $self->{'Parent'};
49}
50
51# internal routine
52sub prev {
53    my $self = shift();
54    $self->{'Prev'} = shift() if defined $_[0];
55    weaken $self->{'Prev'};
56    return $self->{'Prev'};
57}
58
59# internal routine
60sub next {
61    my $self = shift();
62    $self->{'Next'} = shift() if defined $_[0];
63    weaken $self->{'Next'};
64    return $self->{'Next'};
65}
66
67# internal routine
68sub first {
69    my $self = shift();
70
71    $self->{'First'} = $self->{' children'}->[0]
72        if defined $self->{' children'} and defined $self->{' children'}->[0];
73    weaken $self->{'First'};
74    return $self->{'First'};
75}
76
77# internal routine
78sub last {
79    my $self = shift();
80
81    $self->{'Last'} = $self->{' children'}->[-1]
82        if defined $self->{' children'} and defined $self->{' children'}->[-1];
83    weaken $self->{'Last'};
84    return $self->{'Last'};
85}
86
87# internal routine
88sub count {
89    my $self = shift();
90
91    my $count = scalar @{$self->{' children'} || []};
92    $count += $_->count() for @{$self->{' children'}};
93    $self->{'Count'} = PDFNum($self->{' closed'}? -$count: $count) if $count > 0;
94    return $count;
95}
96
97# internal routine
98sub fix_outline {
99    my ($self) = @_;
100
101    $self->first();
102    $self->last();
103    $self->count();
104    return;
105}
106
107=item $outline->title($text)
108
109Set the title of the outline.
110
111=cut
112
113sub title {
114    my ($self, $text) = @_;
115    $self->{'Title'} = PDFString($text, 'o');
116    return $self;
117}
118
119=item $outline->closed()
120
121Set the status of the outline to closed (i.e., collapsed).
122
123=cut
124
125sub closed {
126    my $self = shift();
127    $self->{' closed'} = 1;
128    return $self;
129}
130
131=item $outline->open()
132
133Set the status of the outline to open (i.e., expanded).
134
135=cut
136
137sub open {
138    my $self = shift();
139    delete $self->{' closed'};
140    return $self;
141}
142
143=item $child_outline = $parent_outline->outline()
144
145Returns a new sub-outline (nested outline).
146
147=cut
148
149sub outline {
150    my $self = shift();
151
152    my $child = PDF::Builder::Outline->new($self->{' api'}, $self);
153    if (defined $self->{' children'}) {
154        $child->prev($self->{' children'}->[-1]);
155        $self->{' children'}->[-1]->next($child);
156    }
157    push @{$self->{' children'}}, $child;
158    $self->{' api'}->{'pdf'}->new_obj($child)
159        unless $child->is_obj($self->{' api'}->{'pdf'});
160
161    return $child;
162}
163
164=item $outline->dest($page_object, %position)
165
166=item $outline->dest($page_object)
167
168Sets the destination page and optional position of the outline.
169
170%position can be any of the following:
171
172=over
173
174=item -fit => 1
175
176Display the page designated by C<$page>, with its contents magnified just enough
177to fit the entire page within the window both horizontally and vertically. If
178the required horizontal and vertical magnification factors are different, use
179the smaller of the two, centering the page within the window in the other
180dimension.
181
182=item -fith => $top
183
184Display the page designated by C<$page>, with the vertical coordinate C<$top>
185positioned at the top edge of the window and the contents of the page magnified
186just enough to fit the entire width of the page within the window.
187
188=item -fitv => $left
189
190Display the page designated by C<$page>, with the horizontal coordinate C<$left>
191positioned at the left edge of the window and the contents of the page magnified
192just enough to fit the entire height of the page within the window.
193
194=item -fitr => [$left, $bottom, $right, $top]
195
196Display the page designated by C<$page>, with its contents magnified just enough
197to fit the rectangle specified by the coordinates C<$left>, C<$bottom>,
198C<$right>, and C<$top> entirely within the window both horizontally and
199vertically. If the required horizontal and vertical magnification factors are
200different, use the smaller of the two, centering the rectangle within the window
201in the other dimension.
202
203=item -fitb => 1
204
205Display the page designated by C<$page>, with its contents magnified just
206enough to fit its bounding box entirely within the window both horizontally and
207vertically. If the required horizontal and vertical magnification factors are
208different, use the smaller of the two, centering the bounding box within the
209window in the other dimension.
210
211=item -fitbh => $top
212
213Display the page designated by C<$page>, with the vertical coordinate C<$top>
214positioned at the top edge of the window and the contents of the page magnified
215just enough to fit the entire width of its bounding box within the window.
216
217=item -fitbv => $left
218
219Display the page designated by C<$page>, with the horizontal coordinate C<$left>
220positioned at the left edge of the window and the contents of the page
221magnified just enough to fit the entire height of its bounding box within the
222window.
223
224=item -xyz => [$left, $top, $zoom]
225
226Display the page designated by C<$page>, with the coordinates C<[$left, $top]>
227positioned at the top-left corner of the window and the contents of the page
228magnified by the factor C<$zoom>. A zero (0) value for any of the parameters
229C<$left>, C<$top>, or C<$zoom> specifies that the current value of that
230parameter is to be retained unchanged.
231
232This is the B<default> fit setting, with position (left and top) and zoom
233the same as the calling page's.
234
235=back
236
237=item $outline->dest($name, %position)
238
239=item $outline->dest($name)
240
241Connect the Outline to a "Named Destination" defined elsewhere,
242and optional positioning as described above.
243
244=cut
245
246sub dest {
247    my ($self, $page, %position) = @_;
248    delete $self->{'A'};
249
250    if (ref($page)) {
251        $self = $self->_fit($page, %position);
252    } else {
253        $self->{'Dest'} = PDFString($page, 'n');
254    }
255
256    return $self;
257}
258
259=item $outline->url($url)
260
261Defines the outline as launch-url with url C<$url>.
262
263=cut
264
265sub url {
266    my ($self, $url) = @_;
267
268    delete $self->{'Dest'};
269    $self->{'A'}          = PDFDict();
270    $self->{'A'}->{'S'}   = PDFName('URI');
271    $self->{'A'}->{'URI'} = PDFString($url, 'u');
272
273    return $self;
274}
275
276=item $outline->file($file)
277
278Defines the outline as launch-file with filepath C<$file>.
279
280=cut
281
282sub file {
283    my ($self, $file) = @_;
284
285    delete $self->{'Dest'};
286    $self->{'A'}        = PDFDict();
287    $self->{'A'}->{'S'} = PDFName('Launch');
288    $self->{'A'}->{'F'} = PDFString($file, 'f');
289
290    return $self;
291}
292
293=item $outline->pdf_file($pdffile, $page_number, %position)
294
295=item $outline->pdf_file($pdffile, $page_number)
296
297Defines the destination of the outline as a PDF-file with filepath
298C<$pdffile>, on page C<$pagenum> (default 0), and position C<%position>
299(same as dest()).
300
301=cut
302
303sub pdf_file {
304    my ($self, $file, $page_number, %position) = @_;
305
306    delete $self->{'Dest'};
307    $self->{'A'}        = PDFDict();
308    $self->{'A'}->{'S'} = PDFName('GoToR');
309    $self->{'A'}->{'F'} = PDFString($file, 'f');
310    $self->{'A'}->{'D'} = $self->_fit(PDFNum($page_number // 0), %position);
311
312    return $self;
313}
314
315=back
316
317=cut
318
319# process destination, including position setting, with default of -xyz undef*3
320sub _fit {
321    my ($self, $destination, %position) = @_;
322
323    if      (defined $position{'-fit'}) {
324        $self->{'Dest'} = PDFArray($destination, PDFName('Fit'));
325    } elsif (defined $position{'-fith'}) {
326        $self->{'Dest'} = PDFArray($destination, PDFName('FitH'), PDFNum($position{'-fith'}));
327    } elsif (defined $position{'-fitb'}) {
328        $self->{'Dest'} = PDFArray($destination, PDFName('FitB'));
329    } elsif (defined $position{'-fitbh'}) {
330        $self->{'Dest'} = PDFArray($destination, PDFName('FitBH'), PDFNum($position{'-fitbh'}));
331    } elsif (defined $position{'-fitv'}) {
332        $self->{'Dest'} = PDFArray($destination, PDFName('FitV'), PDFNum($position{'-fitv'}));
333    } elsif (defined $position{'-fitbv'}) {
334        $self->{'Dest'} = PDFArray($destination, PDFName('FitBV'), PDFNum($position{'-fitbv'}));
335    } elsif (defined $position{'-fitr'}) {
336        croak "Insufficient parameters to -fitr => []) " unless scalar @{$position{'-fitr'}} == 4;
337        $self->{'Dest'} = PDFArray($destination, PDFName('FitR'), map {PDFNum($_)} @{$position{'-fitr'}});
338    } elsif (defined $position{'-xyz'}) {
339        croak "Insufficient parameters to -xyz => []) " unless scalar @{$position{'-xyz'}} == 3;
340        $self->{'Dest'} = PDFArray($destination, PDFName('XYZ'), map {defined $_? PDFNum($_): PDFNull()} @{$position{'-xyz'}});
341    } else {
342        # no "fit" option found. use default.
343        $position{'-xyz'} = [undef,undef,undef];
344        $self->{'Dest'} = PDFArray($destination, PDFName('XYZ'), map {defined $_? PDFNum($_): PDFNull()} @{$position{'-xyz'}});
345    }
346
347    return $self;
348}
349
350#sub out_obj {
351#    my ($self, @param) = @_;
352#
353#    $self->fix_outline();
354#    return $self->SUPER::out_obj(@param);
355#}
356
357sub outobjdeep {
358#   my ($self, @param) = @_;
359#
360#   $self->fix_outline();
361#   foreach my $k (qw/ api apipdf apipage /) {
362#       $self->{" $k"} = undef;
363#       delete($self->{" $k"});
364#   }
365#   my @ret = $self->SUPER::outobjdeep(@param);
366#   foreach my $k (qw/ First Parent Next Last Prev /) {
367#       $self->{$k} = undef;
368#       delete($self->{$k});
369#   }
370#   return @ret;
371    my $self = shift();
372    $self->fix_outline();
373    return $self->SUPER::outobjdeep(@_);
374}
375
3761;
377