File Coverage

blib/lib/PDF/Make/Builder/Text.pm
Criterion Covered Total %
statement 112 112 100.0
branch 41 50 82.0
condition 24 38 63.1
subroutine 6 6 100.0
pod 1 1 100.0
total 184 207 88.8


line stmt bran cond sub pod time code
1             package PDF::Make::Builder::Text;
2 42     42   246 use strict;
  42         56  
  42         1188  
3 42     42   149 use warnings;
  42         54  
  42         1443  
4 42     42   136 use Object::Proto;
  42         51  
  42         3210  
5              
6             BEGIN {
7 42     42   3706 Object::Proto::define('PDF::Make::Builder::Text',
8             'text:Str:required',
9             'align:Str:default(left)',
10             'indent:Int:default(0)',
11             'padding:Num:default(0)',
12             'spacing:Num:default(0)',
13             'pad:Str',
14             'pad_end:Str',
15             'margin:Num:default(5)',
16             'overflow:Bool:default(0)',
17             'font:HashRef',
18             'x:Num', 'y:Num', 'w:Num', 'h:Num',
19             'end_w:Num:default(0)',
20             'end_y:Num:default(0)',
21             );
22 42         41108 Object::Proto::import_accessors('PDF::Make::Builder::Text');
23             }
24              
25             sub _resolve_font {
26 352     352   442 my ($self, $builder) = @_;
27 352         542 my $base = $builder->font;
28 352         386 my $overrides = font $self;
29 352 100       602 if ($overrides) {
30             my $f = PDF::Make::Builder::Font->new(
31             colour => $overrides->{colour} // $base->colour,
32             size => $overrides->{size} // $base->size,
33             family => $overrides->{family} // $base->family,
34             bold => $overrides->{bold} // $base->bold,
35             italic => $overrides->{italic} // $base->italic,
36 75   66     998 line_height => $overrides->{line_height} // $base->effective_line_height,
      66        
      66        
      66        
      66        
      66        
37             );
38 75         163 return $f;
39             }
40 277         349 return $base;
41             }
42              
43             sub add {
44 352     352 1 532 my ($self, $builder) = @_;
45              
46 352         695 my $page = $builder->page;
47 352         578 my $canvas = $page->canvas;
48 352         616 my $font = $self->_resolve_font($builder);
49 352         877 my $res_name = $font->ensure_loaded($page->xs_page);
50 352         662 my $font_size = $font->size;
51 352         567 my $lh = $font->effective_line_height;
52 352         431 my $line_spacing = spacing $self;
53 352 50 33     963 $line_spacing = 0 if !defined($line_spacing) || $line_spacing < 0;
54 352         854 my ($cr, $cg, $cb) = $font->hex_to_rgb($font->colour);
55              
56 352         425 my $pad = padding $self;
57 352 50 33     910 $pad = 0 if !defined($pad) || $pad < 0;
58              
59 352   66     1065 my $text_w = ($self->w // $page->width) - (2 * $pad);
60 352 50       551 $text_w = 1 if $text_w < 1;
61 352   66     1026 my $cx = ($self->x // $page->content_x) + $pad;
62 352         536 my $cy = $self->y;
63              
64             # Explicit y is already in builder/PDF bottom-left coordinates
65 352 100       499 if (!defined $cy) {
66 343         580 $cy = $page->cursor_y;
67             }
68 352         393 $cy -= $pad;
69              
70             # Apply indent
71 352         357 my $indent_w = 0;
72 352         388 my $ind = indent $self;
73 352 100       510 if ($ind > 0) {
74 3         12 $indent_w = $font->space_width * $ind;
75             }
76              
77             # Word-wrap
78 352         414 my $raw = text $self;
79 352         2957 my @words = split /\s+/, $raw;
80 352 50       538 return $self unless @words;
81              
82 352         347 my @lines;
83 352         368 my $line = '';
84 352         371 my $line_w = $indent_w;
85 352         363 my $first_line = 1;
86              
87 352         522 for my $word (@words) {
88 14574 100       23399 my $candidate = $line eq '' ? $word : ($line . ' ' . $word);
89 14574         21252 my $candidate_w = $font->measure_text($candidate);
90 14574 100       19902 my $test_w = $candidate_w + ($first_line ? $indent_w : 0);
91 14574         15187 my $max_w = $text_w;
92              
93 14574 100 66     24750 if ($test_w > $max_w && $line ne '') {
94 649         1727 push @lines, [$line, $line_w, $first_line];
95 649         741 $first_line = 0;
96 649         816 $line = $word;
97 649         1009 $line_w = $font->measure_text($line);
98             } else {
99 13925         15585 $line = $candidate;
100 13925         17654 $line_w = $test_w;
101             }
102             }
103 352 50       917 push @lines, [$line, $line_w, $first_line] if $line ne '';
104              
105             # Render lines
106 352         438 my $al = align $self;
107 352         371 my $can_overflow = overflow $self;
108              
109 352         673 for my $idx (0 .. $#lines) {
110 1001         1128 my $entry = $lines[$idx];
111 1001         1321 my ($line_text, $lw, $is_first) = @$entry;
112              
113             # Check if we have room
114 1001 100       1469 if ($cy - $lh < $page->bottom_y) {
115             # Try next column first
116 15 100       45 if ($page->has_next_column) {
    100          
117 3         15 $page->next_column;
118 3         10 $cx = $page->content_x + $pad;
119 3         9 $cy = $page->cursor_y - $pad;
120 3         6 $text_w = $page->width - (2 * $pad);
121 3 50       10 $text_w = 1 if $text_w < 1;
122             } elsif ($can_overflow) {
123             # All columns full — overflow to new page
124             # Inherit settings from current page
125 4         18 my $cols = $page->columns;
126 4         15 my $psz = $page->page_size;
127 4         9 my $page_pad = $page->padding;
128 4         13 my $bg = $page->background;
129 4         35 $builder->add_page(
130             page_size => $psz,
131             padding => $page_pad,
132             columns => $cols,
133             background => $bg,
134             );
135 4         11 $page = $builder->page;
136 4         10 $canvas = $page->canvas;
137 4         23 $res_name = $font->ensure_loaded($page->xs_page);
138 4         14 $cx = $page->content_x + $pad;
139 4         11 $cy = $page->cursor_y - $pad;
140 4         13 $text_w = $page->width - (2 * $pad);
141 4 50       11 $text_w = 1 if $text_w < 1;
142             } else {
143 8         13 last;
144             }
145             }
146              
147             # Baseline sits near top of the line slot (font_size below cursor).
148             # The full line_height advances the cursor to the bottom of the slot.
149 993         1014 my $baseline_y = $cy - $font_size;
150              
151             # Calculate x based on alignment
152 993         946 my $tx = $cx;
153 993 100       1120 my $extra_indent = $is_first ? $indent_w : 0;
154 993 100       1425 if ($al eq 'center') {
    100          
155 6         16 $tx = $cx + ($text_w - $lw) / 2;
156             } elsif ($al eq 'right') {
157 4         10 $tx = $cx + $text_w - $lw;
158             } else {
159 983         973 $tx += $extra_indent;
160             }
161              
162             # Pad support (for TOC dot leaders)
163 993         954 my $pad_char = pad $self;
164 993 100 66     1319 if ($pad_char && length($pad_char)) {
165 4   100     15 my $pad_end_text = pad_end($self) // '';
166 4         13 my $pad_w = $font->measure_word($pad_char);
167 4 100       11 my $end_w = length($pad_end_text) ? $font->measure_text($pad_end_text) : 0;
168 4         11 my $gap = $text_w - $lw - $end_w;
169 4 50       8 if ($gap > $pad_w) {
170 4         7 my $num_pads = int($gap / $pad_w);
171 4         12 $line_text .= ' ' . ($pad_char x $num_pads);
172 4 100       10 $line_text .= $pad_end_text if length($pad_end_text);
173             }
174             }
175              
176             $canvas->BT
177 993         9805 ->rg($cr, $cg, $cb)
178             ->Tf($res_name, $font_size)
179             ->Tm(1, 0, 0, 1, $tx, $baseline_y)
180             ->Tj($line_text)
181             ->ET;
182              
183 993         1007 $cy -= $lh;
184 993 100       1668 $cy -= $line_spacing if $idx < $#lines;
185             }
186              
187             # Update page cursor - spacing applies after the entire block
188 352         502 my $final_y = $cy - (margin $self) - $line_spacing - $pad;
189 352         788 $page->y($final_y);
190 352 50       523 if (@lines) {
191 352         804 end_w $self, $lines[-1][1];
192 352         411 end_y $self, $cy; # bottom of last line slot
193             }
194              
195 352         1828 return $self;
196             }
197              
198             1;
199              
200             __END__