File Coverage

blib/lib/PDF/Make/Builder/TOC.pm
Criterion Covered Total %
statement 69 69 100.0
branch 12 12 100.0
condition 18 22 81.8
subroutine 6 6 100.0
pod 2 2 100.0
total 107 111 96.4


line stmt bran cond sub pod time code
1             package PDF::Make::Builder::TOC;
2 42     42   270 use strict;
  42         67  
  42         1244  
3 42     42   145 use warnings;
  42         57  
  42         1724  
4 42     42   156 use Object::Proto;
  42         54  
  42         2933  
5              
6             BEGIN {
7 42     42   3261 Object::Proto::define('PDF::Make::Builder::TOC',
8             'title:Str:default(Table of Contents)',
9             'title_font_args:HashRef:default({})',
10             'title_padding:Num:default(10)',
11             'font_args:HashRef:default({})',
12             'padding:Num:default(0)',
13             'level_indent:Num:default(2)',
14             'entries:ArrayRef:default([])',
15             'page_index:Int:default(0)',
16             'x:Num', 'y:Num', 'w:Num', 'h:Num',
17             );
18 42         31224 Object::Proto::import_accessors('PDF::Make::Builder::TOC');
19             }
20              
21             sub outline {
22 17     17 1 395 my ($self, $builder, $level, %args) = @_;
23 17         23 my $e = entries $self;
24             push @$e, {
25             text => $args{text},
26             page_num => $args{page_num},
27 17         54 level => $level,
28             };
29 17         34 entries $self, $e;
30             }
31              
32             sub render {
33 6     6 1 340 my ($self, $builder) = @_;
34 6   50     19 my $all_pages = $builder->pages // [];
35 6         45 my $toc_page_idx = page_index($self);
36 6 100 66     34 my $page = ($toc_page_idx >= 0 && $toc_page_idx < @$all_pages)
37             ? $all_pages->[$toc_page_idx]
38             : $builder->page;
39              
40 6 100       15 return unless $page;
41              
42 5         12 my $canvas = $page->canvas;
43 5         11 my $font = $builder->font;
44              
45             # Title
46 5         8 my $tf = title_font_args $self;
47 5   100     28 my $title_size = $tf->{size} // 30;
48 5         22 my $res = $font->ensure_loaded($page->xs_page, 'bold');
49 5   100     22 my ($tr, $tg, $tb) = $font->hex_to_rgb($tf->{colour} // '#000');
50              
51 5         16 my $cx = $page->content_x;
52 5         11 my $cy = $page->cursor_y - $title_size;
53 5         12 my $tw = $page->width;
54 5         8 my $right_x = $cx + $tw;
55              
56 5         65 $canvas->BT
57             ->rg($tr, $tg, $tb)
58             ->Tf($res, $title_size)
59             ->Tm(1, 0, 0, 1, $cx, $cy)
60             ->Tj(title $self)
61             ->ET;
62              
63 5         8 $cy -= title_padding $self;
64              
65             # Entries
66 5         6 my $ef = font_args $self;
67 5   100     15 my $entry_size = $ef->{size} // 11;
68 5   33     19 my $entry_lh = $ef->{line_height} // ($entry_size + 4);
69 5         15 my $entry_res = $font->ensure_loaded($page->xs_page, 'normal');
70 5   100     24 my ($er, $eg, $eb) = $font->hex_to_rgb($ef->{colour} // '#333');
71 5         15 my $indent_w = $font->space_width * (level_indent $self);
72 5         9 my $pad = padding $self;
73              
74 5         5 for my $entry (@{entries $self}) {
  5         11  
75 14         16 $cy -= $entry_lh;
76 14         23 my $indent = $indent_w * ($entry->{level} - 1);
77 14 100       30 my $text = defined $entry->{text} ? $entry->{text} : '';
78 14         17 my $pnum = $entry->{page_num};
79 14 100       20 my $pnum_str = defined($pnum) ? "$pnum" : '?';
80 14         33 my $pnum_w = $font->measure_text($pnum_str) * ($entry_size / $font->size);
81 14         23 my $text_w = $font->measure_text($text) * ($entry_size / $font->size);
82 14         19 my $text_x = $cx + $indent;
83 14         20 my $pnum_x = $right_x - $pnum_w;
84              
85 14         21 my $leader_start = $text_x + $text_w + 4;
86 14         16 my $leader_end = $pnum_x - 2;
87              
88             # Left text
89 14         149 $canvas->BT
90             ->rg($er, $eg, $eb)
91             ->Tf($entry_res, $entry_size)
92             ->Tm(1, 0, 0, 1, $text_x, $cy)
93             ->Tj($text)
94             ->ET;
95              
96             # Dot leaders: draw vector dots so spacing is visual, not font-dependent
97 14 100       36 if ($leader_end > $leader_start + 2) {
98 13         27 my $dot_y = $cy + ($entry_size * 0.22);
99 13         15 my $step = $entry_size * 0.42;
100 13         12 my $dot_len = 0.01;
101 13         15 my $dot_width = $entry_size * 0.11;
102              
103 13         83 $canvas->q
104             ->RG($er, $eg, $eb)
105             ->w($dot_width)
106             ->J(1);
107              
108 13         25 for (my $x = $leader_start; $x <= $leader_end; $x += $step) {
109 1416         5834 $canvas->m($x, $dot_y)
110             ->l($x + $dot_len, $dot_y)
111             ->S;
112             }
113              
114 13         33 $canvas->Q;
115             }
116              
117             # Right page number
118             $canvas->BT
119 14         146 ->rg($er, $eg, $eb)
120             ->Tf($entry_res, $entry_size)
121             ->Tm(1, 0, 0, 1, $pnum_x, $cy)
122             ->Tj($pnum_str)
123             ->ET;
124              
125             # Make TOC row clickable (internal GoTo)
126 14 100 100     99 if (defined $pnum && $pnum =~ /^\d+$/ && $pnum > 0) {
      100        
127 9         12 my $y1 = $cy - 2;
128 9         134 my $y2 = $cy + $entry_size + 2;
129 9         39 $builder->add_link(
130             on_page => $toc_page_idx,
131             page => $pnum - 1,
132             rect => [ $text_x, $y1, $right_x, $y2 ],
133             );
134             }
135              
136 14         31 $cy -= $pad;
137             }
138              
139 5         27 $page->y($cy);
140             }
141              
142             1;
143              
144             __END__