blib/lib/PDF/Builder/Content/Text.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 206 | 1418 | 14.5 |
branch | 87 | 670 | 12.9 |
condition | 44 | 360 | 12.2 |
subroutine | 18 | 43 | 41.8 |
pod | 14 | 15 | 93.3 |
total | 369 | 2506 | 14.7 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package PDF::Builder::Content::Text; | ||||||
2 | |||||||
3 | 38 | 38 | 290 | use base 'PDF::Builder::Content'; | |||
38 | 95 | ||||||
38 | 3941 | ||||||
4 | |||||||
5 | 38 | 38 | 243 | use strict; | |||
38 | 101 | ||||||
38 | 752 | ||||||
6 | 38 | 38 | 210 | use warnings; | |||
38 | 107 | ||||||
38 | 846 | ||||||
7 | 38 | 38 | 200 | use Carp; | |||
38 | 85 | ||||||
38 | 2088 | ||||||
8 | 38 | 38 | 248 | use List::Util qw(min max); | |||
38 | 172 | ||||||
38 | 576566 | ||||||
9 | #use Data::Dumper; # for debugging | ||||||
10 | # $Data::Dumper::Sortkeys = 1; # hash keys in sorted order | ||||||
11 | |||||||
12 | our $VERSION = '3.025'; # VERSION | ||||||
13 | our $LAST_UPDATE = '3.025'; # manually update whenever code is changed | ||||||
14 | |||||||
15 | =head1 NAME | ||||||
16 | |||||||
17 | PDF::Builder::Content::Text - additional specialized text-related formatting methods. Inherits from L |
||||||
18 | |||||||
19 | B |
||||||
20 | I |
||||||
21 | type object (e.g., $page->gfx()->method()), you may have to change to a I |
||||||
22 | type object (e.g., $page->text()->method()). | ||||||
23 | |||||||
24 | =head1 METHODS | ||||||
25 | |||||||
26 | =cut | ||||||
27 | |||||||
28 | sub new { | ||||||
29 | 20 | 20 | 1 | 67 | my ($class) = @_; | ||
30 | 20 | 145 | my $self = $class->SUPER::new(@_); | ||||
31 | 20 | 124 | $self->textstart(); | ||||
32 | 20 | 62 | return $self; | ||||
33 | } | ||||||
34 | |||||||
35 | =head2 Single Lines from a String | ||||||
36 | |||||||
37 | =over | ||||||
38 | |||||||
39 | =item $width = $content->text_left($text, %opts) | ||||||
40 | |||||||
41 | Alias for C |
||||||
42 | C |
||||||
43 | |||||||
44 | Adds text to the page (left justified), at the current position. | ||||||
45 | Note that there is no maximum width, and nothing to keep you from overflowing | ||||||
46 | the physical page on the right! | ||||||
47 | The width used (in points) is B |
||||||
48 | |||||||
49 | =back | ||||||
50 | |||||||
51 | =cut | ||||||
52 | |||||||
53 | sub text_left { | ||||||
54 | 0 | 0 | 1 | 0 | my ($self, $text, @opts) = @_; | ||
55 | |||||||
56 | 0 | 0 | return $self->text($text, @opts); | ||||
57 | } | ||||||
58 | |||||||
59 | =over | ||||||
60 | |||||||
61 | =item $width = $content->text_center($text, %opts) | ||||||
62 | |||||||
63 | As C |
||||||
64 | |||||||
65 | Adds text to the page (centered). | ||||||
66 | The width used (in points) is B |
||||||
67 | |||||||
68 | =back | ||||||
69 | |||||||
70 | =cut | ||||||
71 | |||||||
72 | sub text_center { | ||||||
73 | 6 | 6 | 1 | 43 | my ($self, $text, @opts) = @_; | ||
74 | |||||||
75 | 6 | 40 | my $width = $self->advancewidth($text, @opts); | ||||
76 | 6 | 44 | return $self->text($text, 'indent' => -($width/2), @opts); | ||||
77 | } | ||||||
78 | |||||||
79 | =over | ||||||
80 | |||||||
81 | =item $width = $content->text_right($text, %opts) | ||||||
82 | |||||||
83 | As C |
||||||
84 | |||||||
85 | Adds text to the page (right justified). | ||||||
86 | Note that there is no maximum width, and nothing to keep you from overflowing | ||||||
87 | the physical page on the left! | ||||||
88 | The width used (in points) is B |
||||||
89 | |||||||
90 | =back | ||||||
91 | |||||||
92 | =cut | ||||||
93 | |||||||
94 | sub text_right { | ||||||
95 | 3 | 3 | 1 | 15 | my ($self, $text, @opts) = @_; | ||
96 | |||||||
97 | 3 | 14 | my $width = $self->advancewidth($text, @opts); | ||||
98 | 3 | 21 | return $self->text($text, 'indent' => -$width, @opts); | ||||
99 | } | ||||||
100 | |||||||
101 | =over | ||||||
102 | |||||||
103 | =item $width = $content->text_justified($text, $width, %opts) | ||||||
104 | |||||||
105 | As C |
||||||
106 | last resort) C |
||||||
107 | (available) C<$width>. Note that if the desired width is I |
||||||
108 | natural width taken by the text, it will be I |
||||||
109 | same three routines. | ||||||
110 | |||||||
111 | The unchanged C<$width> is B |
||||||
112 | change it (e.g., overflow). | ||||||
113 | |||||||
114 | B |
||||||
115 | |||||||
116 | =over | ||||||
117 | |||||||
118 | =item 'nocs' => value | ||||||
119 | |||||||
120 | If this option value is 1 (default 0), do B |
||||||
121 | spacing. This is useful for connected characters, such as fonts for Arabic, | ||||||
122 | Devanagari, Latin cursive handwriting, etc. You don't want to add additional | ||||||
123 | space between characters during justification, which would disconnect them. | ||||||
124 | |||||||
125 | I |
||||||
126 | nocs is 1. This is to make up for the lack of added/subtracted intercharacter | ||||||
127 | spacing. | ||||||
128 | |||||||
129 | =item 'wordsp' => value | ||||||
130 | |||||||
131 | The percentage of one space character (default 100) that is the maximum amount | ||||||
132 | to add to (each) interword spacing to expand the line. | ||||||
133 | If C |
||||||
134 | |||||||
135 | =item 'charsp' => value | ||||||
136 | |||||||
137 | If adding interword space didn't do enough, the percentage of one em (default | ||||||
138 | 100) that is the maximum amount to add to (each) intercharacter spacing to | ||||||
139 | further expand the line. | ||||||
140 | If C |
||||||
141 | |||||||
142 | =item 'wordspa' => value | ||||||
143 | |||||||
144 | If adding intercharacter space didn't do enough, the percentage of one space | ||||||
145 | character (default 100) that is the maximum I |
||||||
146 | (each) interword spacing to further expand the line. | ||||||
147 | If C |
||||||
148 | |||||||
149 | =item 'charspa' => value | ||||||
150 | |||||||
151 | If adding more interword space didn't do enough, the percentage of one em | ||||||
152 | (default 100) that is the maximum I |
||||||
153 | intercharacter spacing to further expand the line. | ||||||
154 | If C |
||||||
155 | |||||||
156 | =item 'condw' => value | ||||||
157 | |||||||
158 | The percentage of one space character (default 25) that is the maximum amount | ||||||
159 | to subtract from (each) interword spacing to condense the line. | ||||||
160 | If C |
||||||
161 | |||||||
162 | =item 'condc' => value | ||||||
163 | |||||||
164 | If removing interword space didn't do enough, the percentage of one em | ||||||
165 | (default 10) that is the maximum amount to subtract from (each) intercharacter | ||||||
166 | spacing to further condense the line. | ||||||
167 | If C |
||||||
168 | |||||||
169 | =back | ||||||
170 | |||||||
171 | If expansion (or reduction) wordspace and charspace changes didn't do enough | ||||||
172 | to make the line fit the desired width, use C |
||||||
173 | condensing the line to fit. | ||||||
174 | |||||||
175 | =back | ||||||
176 | |||||||
177 | =cut | ||||||
178 | |||||||
179 | sub text_justified { | ||||||
180 | 4 | 4 | 1 | 20 | my ($self, $text, $width, %opts) = @_; | ||
181 | # copy dashed option names to the preferred undashed names | ||||||
182 | 4 | 50 | 33 | 19 | if (defined $opts{'-wordsp'} && !defined $opts{'wordsp'}) { $opts{'wordsp'} = delete($opts{'-wordsp'}); } | ||
0 | 0 | ||||||
183 | 4 | 50 | 33 | 20 | if (defined $opts{'-charsp'} && !defined $opts{'charsp'}) { $opts{'charsp'} = delete($opts{'-charsp'}); } | ||
0 | 0 | ||||||
184 | 4 | 50 | 33 | 15 | if (defined $opts{'-wordspa'} && !defined $opts{'wordspa'}) { $opts{'wordspa'} = delete($opts{'-wordspa'}); } | ||
0 | 0 | ||||||
185 | 4 | 50 | 33 | 15 | if (defined $opts{'-charspa'} && !defined $opts{'charspa'}) { $opts{'charspa'} = delete($opts{'-charspa'}); } | ||
0 | 0 | ||||||
186 | 4 | 50 | 33 | 17 | if (defined $opts{'-condw'} && !defined $opts{'condw'}) { $opts{'condw'} = delete($opts{'-condw'}); } | ||
0 | 0 | ||||||
187 | 4 | 50 | 33 | 15 | if (defined $opts{'-condc'} && !defined $opts{'condc'}) { $opts{'condc'} = delete($opts{'-condc'}); } | ||
0 | 0 | ||||||
188 | 4 | 50 | 33 | 15 | if (defined $opts{'-nocs'} && !defined $opts{'nocs'}) { $opts{'nocs'} = delete($opts{'-nocs'}); } | ||
0 | 0 | ||||||
189 | |||||||
190 | # optional parameters to control how expansion or condensation are done | ||||||
191 | # 1. expand interword space up to 100% of 1 space | ||||||
192 | 4 | 50 | 14 | my $wordsp = defined($opts{'wordsp'})? $opts{'wordsp'}: 100; | |||
193 | # 2. expand intercharacter space up to 100% of 1em | ||||||
194 | 4 | 50 | 11 | my $charsp = defined($opts{'charsp'})? $opts{'charsp'}: 100; | |||
195 | # 3. expand interword space up to another 100% of 1 space | ||||||
196 | 4 | 50 | 15 | my $wordspa = defined($opts{'wordspa'})? $opts{'wordspa'}: 100; | |||
197 | # 4. expand intercharacter space up to another 100% of 1em | ||||||
198 | 4 | 50 | 16 | my $charspa = defined($opts{'charspa'})? $opts{'charspa'}: 100; | |||
199 | # 5. condense interword space up to 25% of 1 space | ||||||
200 | 4 | 50 | 11 | my $condw = defined($opts{'condw'})? $opts{'condw'}: 25; | |||
201 | # 6. condense intercharacter space up to 10% of 1em | ||||||
202 | 4 | 50 | 175 | my $condc = defined($opts{'condc'})? $opts{'condc'}: 10; | |||
203 | # 7. if still short or long, hscale() | ||||||
204 | |||||||
205 | 4 | 50 | 163 | my $nocs = defined($opts{'nocs'})? $opts{'nocs'}: 0; | |||
206 | 4 | 50 | 17 | if ($nocs) { | |||
207 | 0 | 0 | $charsp = $charspa = $condc = 0; | ||||
208 | 0 | 0 | $wordsp *= 2; | ||||
209 | 0 | 0 | $wordspa *= 2; | ||||
210 | 0 | 0 | $condw *= 2; | ||||
211 | } | ||||||
212 | |||||||
213 | # with original wordspace, charspace, and hscale settings | ||||||
214 | # note that we do NOT change any existing charspace here | ||||||
215 | 4 | 21 | my $length = $self->advancewidth($text, %opts); | ||||
216 | 4 | 13 | my $overage = $length - $width; # > 0, raw text is too wide, < 0, narrow | ||||
217 | |||||||
218 | 4 | 10 | my ($i, @chars, $val, $limit); | ||||
219 | 4 | 20 | my $hs = $self->hscale(); # save old settings and reset to 0 | ||||
220 | 4 | 16 | my $ws = $self->wordspace(); | ||||
221 | 4 | 15 | my $cs = $self->charspace(); | ||||
222 | 4 | 19 | $self->hscale(100); $self->wordspace(0); $self->charspace(0); | ||||
4 | 20 | ||||||
4 | 19 | ||||||
223 | |||||||
224 | # not near perfect fit? not within .1 pt of fitting | ||||||
225 | 4 | 50 | 17 | if (abs($overage) > 0.1) { | |||
226 | |||||||
227 | # how many interword spaces can we change with wordspace? | ||||||
228 | 4 | 22 | my $num_spaces = 0; | ||||
229 | # how many intercharacter spaces can be added to or removed? | ||||||
230 | 4 | 8 | my $num_chars = -1; | ||||
231 | 4 | 27 | @chars = split //, $text; | ||||
232 | 4 | 19 | for ($i=0; $i | ||||
233 | 78 | 100 | 133 | if ($chars[$i] eq ' ') { $num_spaces++; } # TBD other whitespace? | |||
16 | 23 | ||||||
234 | 78 | 140 | $num_chars++; # count spaces as characters, too | ||||
235 | } | ||||||
236 | 4 | 21 | my $em = $self->advancewidth('M'); | ||||
237 | 4 | 17 | my $sp = $self->advancewidth(' '); | ||||
238 | |||||||
239 | 4 | 50 | 20 | if ($overage > 0) { | |||
240 | # too wide: need to condense it | ||||||
241 | # 1. subtract from interword space, up to -$condw/100 $sp | ||||||
242 | 0 | 0 | 0 | 0 | if ($overage > 0 && $num_spaces > 0 && $condw > 0) { | ||
0 | |||||||
243 | 0 | 0 | $val = $overage/$num_spaces; | ||||
244 | 0 | 0 | $limit = $condw/100*$sp; | ||||
245 | 0 | 0 | 0 | if ($val > $limit) { $val = $limit; } | |||
0 | 0 | ||||||
246 | 0 | 0 | $self->wordspace(-$val); | ||||
247 | 0 | 0 | $overage -= $val*$num_spaces; | ||||
248 | } | ||||||
249 | # 2. subtract from intercharacter space, up to -$condc/100 $em | ||||||
250 | 0 | 0 | 0 | 0 | if ($overage > 0 && $num_chars > 0 && $condc > 0) { | ||
0 | |||||||
251 | 0 | 0 | $val = $overage/$num_chars; | ||||
252 | 0 | 0 | $limit = $condc/100*$em; | ||||
253 | 0 | 0 | 0 | if ($val > $limit) { $val = $limit; } | |||
0 | 0 | ||||||
254 | 0 | 0 | $self->charspace(-$val); | ||||
255 | 0 | 0 | $overage -= $val*$num_chars; | ||||
256 | } | ||||||
257 | # 3. nothing more to do than scale down with hscale() | ||||||
258 | } else { | ||||||
259 | # too narrow: need to expand it (usual case) | ||||||
260 | 4 | 12 | $overage = -$overage; # working with positive value is easier | ||||
261 | # 1. add to interword space, up to $wordsp/100 $sp | ||||||
262 | 4 | 50 | 33 | 30 | if ($overage > 0 && $num_spaces > 0 && $wordsp > 0) { | ||
33 | |||||||
263 | 4 | 12 | $val = $overage/$num_spaces; | ||||
264 | 4 | 10 | $limit = $wordsp/100*$sp; | ||||
265 | 4 | 100 | 12 | if ($val > $limit) { $val = $limit; } | |||
1 | 2 | ||||||
266 | 4 | 15 | $self->wordspace($val); | ||||
267 | 4 | 11 | $overage -= $val*$num_spaces; | ||||
268 | } | ||||||
269 | # 2. add to intercharacter space, up to $charsp/100 $em | ||||||
270 | 4 | 50 | 66 | 34 | if ($overage > 0 && $num_chars > 0 && $charsp > 0) { | ||
66 | |||||||
271 | 1 | 3 | $val = $overage/$num_chars; | ||||
272 | 1 | 3 | $limit = $charsp/100*$em; | ||||
273 | 1 | 50 | 4 | if ($val > $limit) { $val = $limit; } | |||
0 | 0 | ||||||
274 | 1 | 4 | $self->charspace($val); | ||||
275 | 1 | 9 | $overage -= $val*$num_chars; | ||||
276 | } | ||||||
277 | # 3. add to interword space, up to $wordspa/100 $sp additional | ||||||
278 | 4 | 0 | 33 | 17 | if ($overage > 0 && $num_spaces > 0 && $wordspa > 0) { | ||
33 | |||||||
279 | 0 | 0 | $val = $overage/$num_spaces; | ||||
280 | 0 | 0 | $limit = $wordspa/100*$sp; | ||||
281 | 0 | 0 | 0 | if ($val > $limit) { $val = $limit; } | |||
0 | 0 | ||||||
282 | 0 | 0 | $self->wordspace($val+$self->wordspace()); | ||||
283 | 0 | 0 | $overage -= $val*$num_spaces; | ||||
284 | } | ||||||
285 | # 4. add to intercharacter space, up to $charspa/100 $em additional | ||||||
286 | 4 | 0 | 33 | 19 | if ($overage > 0 && $num_chars > 0 && $charspa > 0) { | ||
33 | |||||||
287 | 0 | 0 | $val = $overage/$num_chars; | ||||
288 | 0 | 0 | $limit = $charspa/100*$em; | ||||
289 | 0 | 0 | 0 | if ($val > $limit) { $val = $limit; } | |||
0 | 0 | ||||||
290 | 0 | 0 | $self->charspace($val+$self->charspace()); | ||||
291 | 0 | 0 | $overage -= $val*$num_chars; | ||||
292 | } | ||||||
293 | # 5. nothing more to do than scale up with hscale() | ||||||
294 | } | ||||||
295 | |||||||
296 | # last ditch effort to fill the line: use hscale() | ||||||
297 | # temporarily resets hscale to expand width of line to match $width | ||||||
298 | # wordspace and charspace are already (temporarily) at max/min | ||||||
299 | 4 | 50 | 14 | if ($overage > 0.1) { | |||
300 | 0 | 0 | $self->hscale(100*($width/$self->advancewidth($text, %opts))); | ||||
301 | } | ||||||
302 | |||||||
303 | } # original $overage was not near 0 | ||||||
304 | # do the output, with wordspace, charspace, and possiby hscale changed | ||||||
305 | 4 | 26 | $self->text($text, %opts); | ||||
306 | |||||||
307 | # restore settings | ||||||
308 | 4 | 22 | $self->hscale($hs); $self->wordspace($ws); $self->charspace($cs); | ||||
4 | 22 | ||||||
4 | 17 | ||||||
309 | |||||||
310 | 4 | 26 | return $width; | ||||
311 | } | ||||||
312 | |||||||
313 | =head2 Multiple Lines from a String | ||||||
314 | |||||||
315 | The string is split at regular blanks (spaces), x20, to find the longest | ||||||
316 | substring that will fit the C<$width>. | ||||||
317 | If a single word is longer than C<$width>, it will overflow. | ||||||
318 | To stay strictly within the desired bounds, set the option | ||||||
319 | C |
||||||
320 | |||||||
321 | =head3 Hyphenation | ||||||
322 | |||||||
323 | If hyphenation is enabled, those methods which split up a string into multiple | ||||||
324 | lines (the "text fill", paragraph, and section methods) will attempt to split | ||||||
325 | up the word that overflows the line, in order to pack the text even more | ||||||
326 | tightly ("greedy" line splitting). There are a number of controls over where a | ||||||
327 | word may be split, but note that there is nothing language-specific (i.e., | ||||||
328 | following a given language's rules for where a word may be split). This is left | ||||||
329 | to other packages. | ||||||
330 | |||||||
331 | There are hard coded minimums of 2 letters before the split, and 2 letters after | ||||||
332 | the split. See C |
||||||
333 | line splitting makes any attempt to prevent widows and orphans, prevent | ||||||
334 | splitting of the last word in a column or page, or otherwise engage in | ||||||
335 | I |
||||||
336 | |||||||
337 | =over | ||||||
338 | |||||||
339 | =item 'hyphenate' => value | ||||||
340 | |||||||
341 | 0: no hyphenation (B |
||||||
342 | splitting at a soft hyphen (\xAD). Unicode hyphen (U+2010) and non-splitting | ||||||
343 | hyphen (U+2011) are ignored as split points. | ||||||
344 | |||||||
345 | =item 'spHH' => value | ||||||
346 | |||||||
347 | 0: do I |
||||||
348 | |||||||
349 | =item 'spOP' => value | ||||||
350 | |||||||
351 | 0: do I |
||||||
352 | |||||||
353 | =item 'spDR' => value | ||||||
354 | |||||||
355 | 0: do I |
||||||
356 | |||||||
357 | =item 'spLR' => value | ||||||
358 | |||||||
359 | 0: do I |
||||||
360 | |||||||
361 | =item 'spCC' => value | ||||||
362 | |||||||
363 | 0: do I |
||||||
364 | uppercase letter, 1: I |
||||||
365 | |||||||
366 | =item 'spRB' => value | ||||||
367 | |||||||
368 | 0: do I |
||||||
369 | 1: I |
||||||
370 | move! | ||||||
371 | |||||||
372 | =item 'spFS' => value | ||||||
373 | |||||||
374 | 0: do I |
||||||
375 | 1: I |
||||||
376 | super desperation move, and the split will probably make no linguistic sense! | ||||||
377 | |||||||
378 | =item 'min_prefix' => value | ||||||
379 | |||||||
380 | Minimum number of letters I |
||||||
381 | The B |
||||||
382 | |||||||
383 | =item 'min_suffix' => value | ||||||
384 | |||||||
385 | Minimum number of letters I |
||||||
386 | The B |
||||||
387 | |||||||
388 | =back | ||||||
389 | |||||||
390 | =head3 Methods | ||||||
391 | |||||||
392 | =cut | ||||||
393 | |||||||
394 | # splits input text (on spaces) into words, glues them back together until | ||||||
395 | # have filled desired (available) width. return the new line and remaining | ||||||
396 | # text. runs of spaces should be preserved. if the first word of a line does | ||||||
397 | # not fit within the alloted space, and cannot be split short enough, just | ||||||
398 | # accept the overflow. | ||||||
399 | sub _text_fill_line { | ||||||
400 | 20 | 20 | 72 | my ($self, $text, $width, $over, %opts) = @_; | |||
401 | # copy dashed option names to the preferred undashed names | ||||||
402 | 20 | 50 | 33 | 62 | if (defined $opts{'-hyphenate'} && !defined $opts{'hyphenate'}) { $opts{'hyphenate'} = delete($opts{'-hyphenate'}); } | ||
0 | 0 | ||||||
403 | 20 | 50 | 33 | 56 | if (defined $opts{'-lang'} && !defined $opts{'lang'}) { $opts{'lang'} = delete($opts{'-lang'}); } | ||
0 | 0 | ||||||
404 | 20 | 50 | 33 | 54 | if (defined $opts{'-nosplit'} && !defined $opts{'nosplit'}) { $opts{'nosplit'} = delete($opts{'-nosplit'}); } | ||
0 | 0 | ||||||
405 | |||||||
406 | # options of interest | ||||||
407 | 20 | 50 | 55 | my $hyphenate = defined($opts{'hyphenate'})? $opts{'hyphenate'}: 0; # default off | |||
408 | #my $lang = defined($opts{'lang'})? $opts{'lang'}: 'en'; # English rules by default | ||||||
409 | 20 | 41 | my $lang = 'basic'; | ||||
410 | #my $nosplit = defined($opts{'nosplit'})? $opts{'nosplit'}: ''; # indexes NOT to split at, given | ||||||
411 | # as string of integers | ||||||
412 | # my @noSplit = split /[,\s]+/, $nosplit; # normally empty array | ||||||
413 | # 1. indexes start at 0 (split after character N not permitted) | ||||||
414 | # 2. SHYs (soft hyphens) should be skipped | ||||||
415 | # 3. need to map entire string's indexes to each word under | ||||||
416 | # consideration for splitting (hyphenation) | ||||||
417 | |||||||
418 | # TBD should we consider any non-ASCII spaces? | ||||||
419 | # don't split on non-breaking space (required blank). | ||||||
420 | 20 | 111 | my @txt = split(/\x20/, $text); | ||||
421 | 20 | 51 | my @line = (); | ||||
422 | 20 | 34 | local $"; # intent is that reset of separator ($") is local to block | ||||
423 | 20 | 42 | $"=' '; ## no critic | ||||
424 | 20 | 32 | my $lastWord = ''; # the one that didn't quite fit | ||||
425 | 20 | 36 | my $overflowed = 0; | ||||
426 | |||||||
427 | 20 | 54 | while (@txt) { | ||||
428 | # build up @line from @txt array until overfills line. | ||||||
429 | # need to remove SHYs (soft hyphens) at this point. | ||||||
430 | 119 | 189 | $lastWord = shift @txt; # preserve any SHYs in the word | ||||
431 | 119 | 244 | push @line, (_removeSHY($lastWord)); | ||||
432 | # one space between each element of line, like join(' ', @line) | ||||||
433 | 119 | 424 | $overflowed = $self->advancewidth("@line", %opts) > $width; | ||||
434 | 119 | 100 | 354 | last if $overflowed; | |||
435 | } | ||||||
436 | # if overflowed, and overflow not allowed, remove the last word added, | ||||||
437 | # unless single word in line and we're not going to attempt word splitting. | ||||||
438 | 20 | 100 | 66 | 82 | if ($overflowed && !$over) { | ||
439 | 13 | 50 | 33 | 101 | if ($hyphenate && @line == 1 || @line > 1) { | ||
33 | |||||||
440 | 13 | 21 | pop @line; # discard last (or only) word | ||||
441 | 13 | 36 | unshift @txt,$lastWord; # restore with SHYs intact | ||||
442 | } | ||||||
443 | # if not hyphenating (splitting words), just leave oversized | ||||||
444 | # single-word line. if hyphenating, could have empty @line. | ||||||
445 | } | ||||||
446 | |||||||
447 | 20 | 60 | my $Txt = "@txt"; # remaining text to put on next line | ||||
448 | 20 | 45 | my $Line = "@line"; # line that fits, but not yet with any split word | ||||
449 | # may be empty if first word in line overflows | ||||||
450 | |||||||
451 | # if we try to hyphenate, try splitting up that last word that | ||||||
452 | # broke the camel's back. otherwise, will return $Line and $Txt as is. | ||||||
453 | 20 | 50 | 33 | 56 | if ($hyphenate && $overflowed) { | ||
454 | 0 | 0 | my $space; | ||||
455 | # @line is current whole word list of line, does NOT overflow because | ||||||
456 | # $lastWord was removed. it may be empty if the first word tried was | ||||||
457 | # too long. @txt is whole word list of the remaining words to be output | ||||||
458 | # (includes $lastWord as its first word). | ||||||
459 | # | ||||||
460 | # we want to try splitting $lastWord into short enough left fragment | ||||||
461 | # (with right fragment remainder as first word of next line). if we | ||||||
462 | # fail to do so, just leave whole word as first word of next line, IF | ||||||
463 | # @line was not empty. if @line was empty, accept the overflow and | ||||||
464 | # output $lastWord as @line and remove it from @txt. | ||||||
465 | 0 | 0 | 0 | if (@line) { | |||
466 | # line not empty. $space is width for word fragment, not | ||||||
467 | # including blank after previous last word of @line. | ||||||
468 | 0 | 0 | $space = $width - $self->advancewidth("@line ", %opts); | ||||
469 | } else { | ||||||
470 | # line empty (first word too long, and we can try hyphenating). | ||||||
471 | # $space is entire $width available for left fragment. | ||||||
472 | 0 | 0 | $space = $width; | ||||
473 | } | ||||||
474 | |||||||
475 | 0 | 0 | 0 | if ($space > 0) { | |||
476 | 0 | 0 | my ($wordLeft, $wordRight); | ||||
477 | # @line is word(s) (if any) currently fitting within $width. | ||||||
478 | # @txt is remaining words unused in this line. $lastWord is first | ||||||
479 | # word of @txt. $space is width remaining to fill in line. | ||||||
480 | 0 | 0 | $wordLeft = ''; $wordRight = $lastWord; # fallbacks | ||||
0 | 0 | ||||||
481 | |||||||
482 | # if there is an error in Hyphenate_$lang, the message may be | ||||||
483 | # that the splitWord() function can't be found. debug errors by | ||||||
484 | # hard coding the require and splitWord() calls. | ||||||
485 | |||||||
486 | ## test that Hyphenate_$lang exists. if not, use Hyphenate_en | ||||||
487 | ## TBD: if Hyphenate_$lang is not found, should we fall back to | ||||||
488 | ## English (en) rules, or turn off hyphenation, or do limited | ||||||
489 | ## hyphenation (nothing language-specific)? | ||||||
490 | # only Hyphenate_basic. leave language support to other packages | ||||||
491 | 0 | 0 | require PDF::Builder::Content::Hyphenate_basic; | ||||
492 | #eval "require PDF::Builder::Content::Hyphenate_$lang"; | ||||||
493 | #if ($@) { | ||||||
494 | #print "something went wrong with require eval: $@\n"; | ||||||
495 | #$lang = 'en'; # perlmonks 27443 fall back to English | ||||||
496 | #require PDF::Builder::Content::Hyphenate_en; | ||||||
497 | #} | ||||||
498 | 0 | 0 | ($wordLeft,$wordRight) = PDF::Builder::Content::Hyphenate_basic::splitWord($self, $lastWord, $space, %opts); | ||||
499 | #eval '($wordLeft,$wordRight) = PDF::Builder::Content::Hyphenate_'.$lang.'::splitWord($self, "$lastWord", $space, %opts)'; | ||||||
500 | 0 | 0 | 0 | if ($@) { print "something went wrong with eval: $@\n"; } | |||
0 | 0 | ||||||
501 | |||||||
502 | # $wordLeft is left fragment of $lastWord that fits in $space. | ||||||
503 | # it might be empty '' if couldn't get a small enough piece. it | ||||||
504 | # includes a hyphen, but no leading space, and can be added to | ||||||
505 | # @line. | ||||||
506 | # $wordRight is the remainder of $lastWord (right fragment) that | ||||||
507 | # didn't fit. it might be the entire $lastWord. it shouldn't be | ||||||
508 | # empty, since the whole point of the exercise is that $lastWord | ||||||
509 | # didn't fit in the remaining space. it will replace the first | ||||||
510 | # element of @txt (there should be at least one). | ||||||
511 | |||||||
512 | # see if have a small enough left fragment of $lastWord to append | ||||||
513 | # to @line. neither left nor right Word should have full $lastWord, | ||||||
514 | # and both cannot be empty. it is highly unlikely that $wordLeft | ||||||
515 | # will be the full $lastWord, but quite possible that it is empty | ||||||
516 | # and $wordRight is $lastWord. | ||||||
517 | |||||||
518 | 0 | 0 | 0 | if (!@line) { | |||
519 | # special case of empty line. if $wordLeft is empty and | ||||||
520 | # $wordRight is presumably the entire $lastWord, use $wordRight | ||||||
521 | # for the line and remove it ($lastWord) from @txt. | ||||||
522 | 0 | 0 | 0 | if ($wordLeft eq '') { | |||
523 | 0 | 0 | @line = ($wordRight); # probably overflows $width. | ||||
524 | 0 | 0 | shift @txt; # remove $lastWord from @txt. | ||||
525 | } else { | ||||||
526 | # $wordLeft fragment fits $width. | ||||||
527 | 0 | 0 | @line = ($wordLeft); # should fit $width. | ||||
528 | 0 | 0 | shift @txt; # replace first element of @txt ($lastWord) | ||||
529 | 0 | 0 | unshift @txt, $wordRight; | ||||
530 | } | ||||||
531 | } else { | ||||||
532 | # usual case of some words already in @line. if $wordLeft is | ||||||
533 | # empty and $wordRight is entire $lastWord, we're done here. | ||||||
534 | # if $wordLeft has something, append it to line and replace | ||||||
535 | # first element of @txt with $wordRight (unless empty, which | ||||||
536 | # shouldn't happen). | ||||||
537 | 0 | 0 | 0 | if ($wordLeft eq '') { | |||
538 | # was unable to split $lastWord into short enough fragment. | ||||||
539 | # leave @line (already has words) and @txt alone. | ||||||
540 | } else { | ||||||
541 | 0 | 0 | push @line, ($wordLeft); # should fit $space. | ||||
542 | 0 | 0 | shift @txt; # replace first element of @txt (was $lastWord) | ||||
543 | 0 | 0 | 0 | unshift @txt, $wordRight if $wordRight ne ''; | |||
544 | } | ||||||
545 | } | ||||||
546 | |||||||
547 | # rebuild $Line and $Txt, in case they were altered. | ||||||
548 | 0 | 0 | $Txt = "@txt"; | ||||
549 | 0 | 0 | $Line = "@line"; | ||||
550 | } # there was $space available to try to fit a word fragment | ||||||
551 | } # we had an overflow to clean up, and hyphenation (word splitting) OK | ||||||
552 | 20 | 100 | return ($Line, $Txt); | ||||
553 | } | ||||||
554 | |||||||
555 | # remove soft hyphens (SHYs) from a word. assume is always #173 (good for | ||||||
556 | # Latin-1, CP-1252, UTF-8; might not work for some encodings) TBD | ||||||
557 | sub _removeSHY { | ||||||
558 | 119 | 119 | 212 | my ($word) = @_; | |||
559 | |||||||
560 | 119 | 284 | my @chars = split //, $word; | ||||
561 | 119 | 189 | my $out = ''; | ||||
562 | 119 | 198 | foreach (@chars) { | ||||
563 | 357 | 50 | 601 | next if ord($_) == 173; | |||
564 | 357 | 582 | $out .= $_; | ||||
565 | } | ||||||
566 | 119 | 283 | return $out; | ||||
567 | } | ||||||
568 | |||||||
569 | =over | ||||||
570 | |||||||
571 | =item ($width, $leftover) = $content->text_fill_left($string, $width, %opts) | ||||||
572 | |||||||
573 | Fill a line of 'width' with as much text as will fit, | ||||||
574 | and outputs it left justified. | ||||||
575 | The width actually used, and the leftover text (that didn't fit), | ||||||
576 | are B |
||||||
577 | |||||||
578 | =item ($width, $leftover) = $content->text_fill($string, $width, %opts) | ||||||
579 | |||||||
580 | Alias for text_fill_left(). | ||||||
581 | |||||||
582 | =back | ||||||
583 | |||||||
584 | =cut | ||||||
585 | |||||||
586 | sub text_fill_left { | ||||||
587 | 10 | 10 | 1 | 33 | my ($self, $text, $width, %opts) = @_; | ||
588 | # copy dashed option names to preferred undashed names | ||||||
589 | 10 | 50 | 33 | 51 | if (defined $opts{'-spillover'} && !defined $opts{'spillover'}) { $opts{'spillover'} = delete($opts{'-spillover'}); } | ||
10 | 24 | ||||||
590 | |||||||
591 | 10 | 33 | 40 | my $over = (not(defined($opts{'spillover'}) and $opts{'spillover'} == 0)); | |||
592 | 10 | 35 | my ($line, $ret) = $self->_text_fill_line($text, $width, $over, %opts); | ||||
593 | 10 | 42 | $width = $self->text($line, %opts); | ||||
594 | 10 | 35 | return ($width, $ret); | ||||
595 | } | ||||||
596 | |||||||
597 | sub text_fill { | ||||||
598 | 0 | 0 | 1 | 0 | my $self = shift; | ||
599 | 0 | 0 | return $self->text_fill_left(@_); | ||||
600 | } | ||||||
601 | |||||||
602 | =over | ||||||
603 | |||||||
604 | =item ($width, $leftover) = $content->text_fill_center($string, $width, %opts) | ||||||
605 | |||||||
606 | Fill a line of 'width' with as much text as will fit, | ||||||
607 | and outputs it centered. | ||||||
608 | The width actually used, and the leftover text (that didn't fit), | ||||||
609 | are B |
||||||
610 | |||||||
611 | =back | ||||||
612 | |||||||
613 | =cut | ||||||
614 | |||||||
615 | sub text_fill_center { | ||||||
616 | 2 | 2 | 1 | 9 | my ($self, $text, $width, %opts) = @_; | ||
617 | # copy dashed option names to preferred undashed names | ||||||
618 | 2 | 50 | 33 | 14 | if (defined $opts{'-spillover'} && !defined $opts{'spillover'}) { $opts{'spillover'} = delete($opts{'-spillover'}); } | ||
2 | 5 | ||||||
619 | |||||||
620 | 2 | 33 | 11 | my $over = (not(defined($opts{'spillover'}) and $opts{'spillover'} == 0)); | |||
621 | 2 | 11 | my ($line, $ret) = $self->_text_fill_line($text, $width, $over, %opts); | ||||
622 | 2 | 11 | $width = $self->text_center($line, %opts); | ||||
623 | 2 | 10 | return ($width, $ret); | ||||
624 | } | ||||||
625 | |||||||
626 | =over | ||||||
627 | |||||||
628 | =item ($width, $leftover) = $content->text_fill_right($string, $width, %opts) | ||||||
629 | |||||||
630 | Fill a line of 'width' with as much text as will fit, | ||||||
631 | and outputs it right justified. | ||||||
632 | The width actually used, and the leftover text (that didn't fit), | ||||||
633 | are B |
||||||
634 | |||||||
635 | =back | ||||||
636 | |||||||
637 | =cut | ||||||
638 | |||||||
639 | sub text_fill_right { | ||||||
640 | 2 | 2 | 1 | 10 | my ($self, $text, $width, %opts) = @_; | ||
641 | # copy dashed option names to preferred undashed names | ||||||
642 | 2 | 50 | 33 | 14 | if (defined $opts{'-spillover'} && !defined $opts{'spillover'}) { $opts{'spillover'} = delete($opts{'-spillover'}); } | ||
2 | 8 | ||||||
643 | |||||||
644 | 2 | 33 | 11 | my $over = (not(defined($opts{'spillover'}) and $opts{'spillover'} == 0)); | |||
645 | 2 | 11 | my ($line, $ret) = $self->_text_fill_line($text, $width, $over, %opts); | ||||
646 | 2 | 10 | $width = $self->text_right($line, %opts); | ||||
647 | 2 | 8 | return ($width, $ret); | ||||
648 | } | ||||||
649 | |||||||
650 | =over | ||||||
651 | |||||||
652 | =item ($width, $leftover) = $content->text_fill_justified($string, $width, %opts) | ||||||
653 | |||||||
654 | Fill a line of 'width' with as much text as will fit, | ||||||
655 | and outputs it fully justified (stretched or condensed). | ||||||
656 | The width actually used, and the leftover text (that didn't fit), | ||||||
657 | are B |
||||||
658 | |||||||
659 | Note that the entire line is fit to the available | ||||||
660 | width via a call to C |
||||||
661 | See C |
||||||
662 | The last line is unjustified (normal size) and left aligned by default, | ||||||
663 | although the option | ||||||
664 | |||||||
665 | B |
||||||
666 | |||||||
667 | =over | ||||||
668 | |||||||
669 | =item 'last_align' => place | ||||||
670 | |||||||
671 | where place is 'left' (default), 'center', or 'right' (may be shortened to | ||||||
672 | first letter) allows you to specify the alignment of the last line output. | ||||||
673 | |||||||
674 | =back | ||||||
675 | |||||||
676 | =back | ||||||
677 | |||||||
678 | =cut | ||||||
679 | |||||||
680 | sub text_fill_justified { | ||||||
681 | 6 | 6 | 1 | 23 | my ($self, $text, $width, %opts) = @_; | ||
682 | # copy dashed option names to preferred undashed names | ||||||
683 | 6 | 100 | 66 | 34 | if (defined $opts{'-last_align'} && !defined $opts{'last_align'}) { $opts{'last_align'} = delete($opts{'-last_align'}); } | ||
4 | 12 | ||||||
684 | 6 | 50 | 33 | 33 | if (defined $opts{'-spillover'} && !defined $opts{'spillover'}) { $opts{'spillover'} = delete($opts{'-spillover'}); } | ||
6 | 16 | ||||||
685 | |||||||
686 | 6 | 12 | my $align = 'l'; # default left align last line | ||||
687 | 6 | 100 | 15 | if (defined($opts{'last_align'})) { | |||
688 | 4 | 50 | 31 | if ($opts{'last_align'} =~ m/^l/i) { $align = 'l'; } | |||
0 | 100 | 0 | |||||
50 | |||||||
689 | 2 | 5 | elsif ($opts{'last_align'} =~ m/^c/i) { $align = 'c'; } | ||||
690 | 2 | 5 | elsif ($opts{'last_align'} =~ m/^r/i) { $align = 'r'; } | ||||
691 | 0 | 0 | else { warn "Unknown last_align for justified fill, 'left' used\n"; } | ||||
692 | } | ||||||
693 | |||||||
694 | 6 | 33 | 28 | my $over = (not(defined($opts{'spillover'}) and $opts{'spillover'} == 0)); | |||
695 | 6 | 31 | my ($line, $ret) = $self->_text_fill_line($text, $width, $over, %opts); | ||||
696 | # if last line, use $align (don't justify) | ||||||
697 | 6 | 100 | 23 | if ($ret eq '') { | |||
698 | 3 | 13 | my $lw = $self->advancewidth($line, %opts); | ||||
699 | 3 | 100 | 23 | if ($align eq 'l') { | |||
100 | |||||||
700 | 1 | 9 | $width = $self->text($line, %opts); | ||||
701 | } elsif ($align eq 'c') { | ||||||
702 | 1 | 6 | $width = $self->text($line, 'indent' => ($width-$lw)/2, %opts); | ||||
703 | } else { # 'r' | ||||||
704 | 1 | 6 | $width = $self->text($line, 'indent' => ($width-$lw), %opts); | ||||
705 | } | ||||||
706 | } else { | ||||||
707 | 3 | 18 | $width = $self->text_justified($line, $width, %opts); | ||||
708 | } | ||||||
709 | 6 | 26 | return ($width, $ret); | ||||
710 | } | ||||||
711 | |||||||
712 | =head2 Larger Text Segments | ||||||
713 | |||||||
714 | =over | ||||||
715 | |||||||
716 | =item ($overflow_text, $unused_height) = $txt->paragraph($text, $width,$height, $continue, %opts) | ||||||
717 | |||||||
718 | =item $overflow_text = $txt->paragraph($text, $width,$height, $continue, %opts) | ||||||
719 | |||||||
720 | Print a single string into a rectangular area on the page, of given width and | ||||||
721 | maximum height. The baseline of the first (top) line is at the current text | ||||||
722 | position. | ||||||
723 | |||||||
724 | Apply the text within the rectangle and B |
||||||
725 | not fit all of it within the rectangle). If called in an array context, the | ||||||
726 | unused height is also B |
||||||
727 | rectangle). | ||||||
728 | |||||||
729 | If C<$continue> is 1, the first line does B |
||||||
730 | indenting or outdenting, because we're printing the continuation of the | ||||||
731 | paragraph that was interrupted earlier. If it's 0, the first line may be | ||||||
732 | indented or outdented. | ||||||
733 | |||||||
734 | B |
||||||
735 | |||||||
736 | =over | ||||||
737 | |||||||
738 | =item 'pndnt' => $indent | ||||||
739 | |||||||
740 | Give the amount of indent (positive) or outdent (negative, for "hanging") | ||||||
741 | for paragraph first lines). This setting is ignored for centered text. | ||||||
742 | |||||||
743 | =item 'align' => $choice | ||||||
744 | |||||||
745 | C<$choice> is 'justified', 'right', 'center', 'left'; the default is 'left'. | ||||||
746 | See C |
||||||
747 | condensed if C<$choice> is 'justified'. C<$choice> may be shortened to the | ||||||
748 | first letter. | ||||||
749 | |||||||
750 | =item 'last_align' => place | ||||||
751 | |||||||
752 | where place is 'left' (default), 'center', or 'right' (may be shortened to | ||||||
753 | first letter) allows you to specify the alignment of the last line output, | ||||||
754 | but applies only when C |
||||||
755 | |||||||
756 | =item 'underline' => $distance | ||||||
757 | |||||||
758 | =item 'underline' => [ $distance, $thickness, ... ] | ||||||
759 | |||||||
760 | If a scalar, distance below baseline, | ||||||
761 | else array reference with pairs of distance and line thickness. | ||||||
762 | |||||||
763 | =item 'spillover' => $over | ||||||
764 | |||||||
765 | Controls if words in a line which exceed the given width should be | ||||||
766 | "spilled over" the bounds, or if a new line should be used for this word. | ||||||
767 | |||||||
768 | C<$over> is 1 or 0, with the default 1 (spills over the width). | ||||||
769 | |||||||
770 | =back | ||||||
771 | |||||||
772 | B |
||||||
773 | |||||||
774 | $txt->font($font,$fontsize); | ||||||
775 | $txt->leading($leading); | ||||||
776 | $txt->translate($x,$y); | ||||||
777 | $overflow = $txt->paragraph( 'long paragraph here ...', | ||||||
778 | $width, | ||||||
779 | $y+$leading-$bottom_margin ); | ||||||
780 | |||||||
781 | B |
||||||
782 | (B |
||||||
783 | plain text (all the same font, size, etc.) can be typeset with C |
||||||
784 | Also, there is currently very limited line splitting (hyphenation) to better | ||||||
785 | fit to a given width, and nothing is done for "widows and orphans". | ||||||
786 | |||||||
787 | =back | ||||||
788 | |||||||
789 | =cut | ||||||
790 | |||||||
791 | # TBD for LTR languages, does indenting on left make sense for right justified? | ||||||
792 | # TBD for bidi/RTL languages, should indenting be on right? | ||||||
793 | |||||||
794 | sub paragraph { | ||||||
795 | 12 | 12 | 1 | 112 | my ($self, $text, $width,$height, $continue, %opts) = @_; | ||
796 | # copy dashed option names to preferred undashed names | ||||||
797 | 12 | 100 | 66 | 59 | if (defined $opts{'-align'} && !defined $opts{'align'}) { $opts{'align'} = delete($opts{'-align'}); } | ||
5 | 20 | ||||||
798 | 12 | 50 | 33 | 42 | if (defined $opts{'-pndnt'} && !defined $opts{'pndnt'}) { $opts{'pndnt'} = delete($opts{'-pndnt'}); } | ||
0 | 0 | ||||||
799 | |||||||
800 | 12 | 29 | my @line = (); | ||||
801 | 12 | 23 | my $nwidth = 0; | ||||
802 | 12 | 34 | my $leading = $self->leading(); | ||||
803 | 12 | 27 | my $align = 'l'; # default left | ||||
804 | 12 | 100 | 38 | if (defined($opts{'align'})) { | |||
805 | 5 | 50 | 53 | if ($opts{'align'} =~ /^l/i) { $align = 'l'; } | |||
0 | 100 | 0 | |||||
100 | |||||||
50 | |||||||
806 | 1 | 4 | elsif ($opts{'align'} =~ /^c/i) { $align = 'c'; } | ||||
807 | 1 | 4 | elsif ($opts{'align'} =~ /^r/i) { $align = 'r'; } | ||||
808 | 3 | 10 | elsif ($opts{'align'} =~ /^j/i) { $align = 'j'; } | ||||
809 | 0 | 0 | else { warn "Unknown align value for paragraph(), 'left' used\n"; } | ||||
810 | } # default stays at 'l' | ||||||
811 | 12 | 50 | 38 | my $indent = defined($opts{'pndnt'})? $opts{'pndnt'}: 0; | |||
812 | 12 | 100 | 34 | if ($align eq 'c') { $indent = 0; } # indent/outdent makes no sense centered | |||
1 | 2 | ||||||
813 | 12 | 29 | my $first_line = !$continue; | ||||
814 | 12 | 18 | my $lw; | ||||
815 | 12 | 50 | my $em = $self->advancewidth('M'); | ||||
816 | |||||||
817 | 12 | 40 | while (length($text) > 0) { # more text to go... | ||||
818 | # indent == 0 (flush) all lines normal width | ||||||
819 | # indent (>0) first line moved in on left, subsequent normal width | ||||||
820 | # outdent (<0) first line is normal width, subsequent moved in on left | ||||||
821 | 20 | 36 | $lw = $width; | ||||
822 | 20 | 50 | 33 | 63 | if ($indent > 0 && $first_line) { $lw -= $indent*$em; } | ||
0 | 0 | ||||||
823 | 20 | 50 | 33 | 102 | if ($indent < 0 && !$first_line) { $lw += $indent*$em; } | ||
0 | 0 | ||||||
824 | # now, need to indent (move line start) right for 'l' and 'j' | ||||||
825 | 20 | 0 | 0 | 70 | if ($lw < $width && ($align eq 'l' || $align eq 'j')) { | ||
33 | |||||||
826 | 0 | 0 | $self->cr($leading); # go UP one line | ||||
827 | 0 | 0 | $self->nl(88*abs($indent)); # come down to right line and move right | ||||
828 | } | ||||||
829 | |||||||
830 | 20 | 100 | 76 | if ($align eq 'j') { | |||
100 | |||||||
100 | |||||||
831 | 6 | 34 | ($nwidth,$text) = $self->text_fill_justified($text, $lw, %opts); | ||||
832 | } elsif ($align eq 'r') { | ||||||
833 | 2 | 11 | ($nwidth,$text) = $self->text_fill_right($text, $lw, %opts); | ||||
834 | } elsif ($align eq 'c') { | ||||||
835 | 2 | 11 | ($nwidth,$text) = $self->text_fill_center($text, $lw, %opts); | ||||
836 | } else { # 'l' | ||||||
837 | 10 | 38 | ($nwidth,$text) = $self->text_fill_left($text, $lw, %opts); | ||||
838 | } | ||||||
839 | |||||||
840 | 20 | 85 | $self->nl(); | ||||
841 | 20 | 39 | $first_line = 0; | ||||
842 | |||||||
843 | # bail out and just return remaining $text if run out of vertical space | ||||||
844 | 20 | 100 | 77 | last if ($height -= $leading) < 0; | |||
845 | } | ||||||
846 | |||||||
847 | 12 | 100 | 58 | if (wantarray) { | |||
848 | # paragraph() called in the context of returning an array | ||||||
849 | 6 | 21 | return ($text, $height); | ||||
850 | } | ||||||
851 | 6 | 29 | return $text; | ||||
852 | } | ||||||
853 | |||||||
854 | =over | ||||||
855 | |||||||
856 | =item ($overflow_text, $continue, $unused_height) = $txt->section($text, $width,$height, $continue, %opts) | ||||||
857 | |||||||
858 | =item $overflow_text = $txt->section($text, $width,$height, $continue, %opts) | ||||||
859 | |||||||
860 | The C<$text> contains a string with one or more paragraphs C<$width> wide, | ||||||
861 | starting at the current text position, with a newline \n between each | ||||||
862 | paragraph. Each paragraph is output (see C |
||||||
863 | limit is met (a partial paragraph may be at the bottom). Whatever wasn't | ||||||
864 | output, will be B |
||||||
865 | If called in an array context, the | ||||||
866 | unused height and the paragraph "continue" flag are also B |
||||||
867 | |||||||
868 | C<$continue> is 0 for the first call of section(), and then use the value | ||||||
869 | returned from the previous call (1 if a paragraph was cut in the middle) to | ||||||
870 | prevent unwanted indenting or outdenting of the first line being printed. | ||||||
871 | |||||||
872 | For compatibility with recent changes to PDF::API2, B |
||||||
873 | as an I |
||||||
874 | |||||||
875 | B |
||||||
876 | |||||||
877 | =over | ||||||
878 | |||||||
879 | =item 'pvgap' => $vertical | ||||||
880 | |||||||
881 | Additional vertical space (unit: pt) between paragraphs (default 0). Note that this space | ||||||
882 | will also be added after the last paragraph printed. | ||||||
883 | |||||||
884 | =back | ||||||
885 | |||||||
886 | See C |
||||||
887 | |||||||
888 | =back | ||||||
889 | |||||||
890 | =cut | ||||||
891 | |||||||
892 | # alias for compatibility | ||||||
893 | sub paragraphs { | ||||||
894 | 1 | 1 | 0 | 16 | return section(@_); | ||
895 | } | ||||||
896 | |||||||
897 | sub section { | ||||||
898 | 2 | 2 | 1 | 16 | my ($self, $text, $width,$height, $continue, %opts) = @_; | ||
899 | # copy dashed option names to preferred undashed names | ||||||
900 | 2 | 50 | 33 | 10 | if (defined $opts{'-pvgap'} && !defined $opts{'pvgap'}) { $opts{'pvgap'} = delete($opts{'-pvgap'}); } | ||
0 | 0 | ||||||
901 | |||||||
902 | 2 | 5 | my $overflow = ''; # text to return if height fills up | ||||
903 | 2 | 50 | 7 | my $pvgap = defined($opts{'pvgap'})? $opts{'pvgap'}: 0; | |||
904 | # $continue =0 if fresh paragraph, or =1 if continuing one cut in middle | ||||||
905 | |||||||
906 | 2 | 13 | foreach my $para (split(/\n/, $text)) { | ||||
907 | # regardless of whether we've run out of space vertically, we will | ||||||
908 | # loop through all the paragraphs requested | ||||||
909 | |||||||
910 | # already seen inability to output more text? | ||||||
911 | # just put unused text back together into the string | ||||||
912 | # $continue should stay 1 | ||||||
913 | 6 | 50 | 18 | if (length($overflow) > 0) { | |||
914 | 0 | 0 | $overflow .= "\n" . $para; | ||||
915 | 0 | 0 | next; | ||||
916 | } | ||||||
917 | 6 | 23 | ($para, $height) = $self->paragraph($para, $width,$height, $continue, %opts); | ||||
918 | 6 | 13 | $continue = 0; | ||||
919 | 6 | 100 | 17 | if (length($para) > 0) { | |||
920 | # we cut a paragraph in half. set flag that continuation doesn't | ||||||
921 | # get indented/outdented | ||||||
922 | 2 | 6 | $overflow .= $para; | ||||
923 | 2 | 4 | $continue = 1; | ||||
924 | } | ||||||
925 | |||||||
926 | # inter-paragraph vertical space? | ||||||
927 | # note that the last paragraph will also get the extra space after it | ||||||
928 | 6 | 50 | 66 | 25 | if (length($para) == 0 && $pvgap != 0) { | ||
929 | 0 | 0 | $self->cr(-$pvgap); | ||||
930 | 0 | 0 | $height -= $pvgap; | ||||
931 | } | ||||||
932 | } | ||||||
933 | |||||||
934 | 2 | 50 | 8 | if (wantarray) { | |||
935 | # section() called in the context of returning an array | ||||||
936 | 0 | 0 | return ($overflow, $continue, $height); | ||||
937 | } | ||||||
938 | 2 | 9 | return $overflow; | ||||
939 | } | ||||||
940 | |||||||
941 | =over | ||||||
942 | |||||||
943 | =item $width = $txt->textlabel($x,$y, $font, $size, $text, %opts) | ||||||
944 | |||||||
945 | Place a line of text at an arbitrary C<[$x,$y]> on the page, with various text | ||||||
946 | settings (treatments) specified in the call. | ||||||
947 | |||||||
948 | =over | ||||||
949 | |||||||
950 | =item $font | ||||||
951 | |||||||
952 | A previously created font. | ||||||
953 | |||||||
954 | =item $size | ||||||
955 | |||||||
956 | The font size (points). | ||||||
957 | |||||||
958 | =item $text | ||||||
959 | |||||||
960 | The text to be printed (a single line). | ||||||
961 | |||||||
962 | =back | ||||||
963 | |||||||
964 | B |
||||||
965 | |||||||
966 | =over | ||||||
967 | |||||||
968 | =item 'rotate' => $deg | ||||||
969 | |||||||
970 | Rotate C<$deg> degrees counterclockwise from due East. | ||||||
971 | |||||||
972 | =item 'color' => $cspec | ||||||
973 | |||||||
974 | A color name or permitted spec, such as C<#CCE840>, for the character I |
||||||
975 | |||||||
976 | =item 'strokecolor' => $cspec | ||||||
977 | |||||||
978 | A color name or permitted spec, such as C<#CCE840>, for the character I |
||||||
979 | |||||||
980 | =item 'charspace' => $cdist | ||||||
981 | |||||||
982 | Additional distance between characters. | ||||||
983 | |||||||
984 | =item 'wordspace' => $wdist | ||||||
985 | |||||||
986 | Additional distance between words. | ||||||
987 | |||||||
988 | =item 'hscale' => $hfactor | ||||||
989 | |||||||
990 | Horizontal scaling mode (percentage of normal, default is 100). | ||||||
991 | |||||||
992 | =item 'render' => $mode | ||||||
993 | |||||||
994 | Character rendering mode (outline only, fill only, etc.). See C |
||||||
995 | |||||||
996 | =item 'left' => 1 | ||||||
997 | |||||||
998 | Left align on the given point. This is the default. | ||||||
999 | |||||||
1000 | =item 'center' => 1 | ||||||
1001 | |||||||
1002 | Center the text on the given point. | ||||||
1003 | |||||||
1004 | =item 'right' => 1 | ||||||
1005 | |||||||
1006 | Right align on the given point. | ||||||
1007 | |||||||
1008 | =item 'align' => $placement | ||||||
1009 | |||||||
1010 | Alternate to left, center, and right. C<$placement> is 'left' (default), | ||||||
1011 | 'center', or 'right'. | ||||||
1012 | |||||||
1013 | =back | ||||||
1014 | |||||||
1015 | Other options available to C |
||||||
1016 | |||||||
1017 | The width used (in points) is B |
||||||
1018 | |||||||
1019 | =back | ||||||
1020 | |||||||
1021 | B |
||||||
1022 | text operations. It is a standalone operation, and does I |
||||||
1023 | write" position (or any other setting) for another C |
||||||
1024 | following write will likely be at C<(0,0)>, and not at the expected location. | ||||||
1025 | |||||||
1026 | C |
||||||
1027 | lines of text, such as a label on some | ||||||
1028 | graphics, and not as part of putting down multiple pieces of text. It I |
||||||
1029 | possible to figure out the position of a following write (either C |
||||||
1030 | or C |
||||||
1031 | (assuming left-justified positioning). | ||||||
1032 | |||||||
1033 | =cut | ||||||
1034 | |||||||
1035 | sub textlabel { | ||||||
1036 | 0 | 0 | 1 | my ($self, $x,$y, $font, $size, $text, %opts) = @_; | |||
1037 | # copy dashed option names to preferred undashed names | ||||||
1038 | 0 | 0 | 0 | if (defined $opts{'-rotate'} && !defined $opts{'rotate'}) { $opts{'rotate'} = delete($opts{'-rotate'}); } | |||
0 | |||||||
1039 | 0 | 0 | 0 | if (defined $opts{'-color'} && !defined $opts{'color'}) { $opts{'color'} = delete($opts{'-color'}); } | |||
0 | |||||||
1040 | 0 | 0 | 0 | if (defined $opts{'-strokecolor'} && !defined $opts{'strokecolor'}) { $opts{'strokecolor'} = delete($opts{'-strokecolor'}); } | |||
0 | |||||||
1041 | 0 | 0 | 0 | if (defined $opts{'-charspace'} && !defined $opts{'charspace'}) { $opts{'charspace'} = delete($opts{'-charspace'}); } | |||
0 | |||||||
1042 | 0 | 0 | 0 | if (defined $opts{'-hscale'} && !defined $opts{'hscale'}) { $opts{'hscale'} = delete($opts{'-hscale'}); } | |||
0 | |||||||
1043 | 0 | 0 | 0 | if (defined $opts{'-wordspace'} && !defined $opts{'wordspace'}) { $opts{'wordspace'} = delete($opts{'-wordspace'}); } | |||
0 | |||||||
1044 | 0 | 0 | 0 | if (defined $opts{'-render'} && !defined $opts{'render'}) { $opts{'render'} = delete($opts{'-render'}); } | |||
0 | |||||||
1045 | 0 | 0 | 0 | if (defined $opts{'-right'} && !defined $opts{'right'}) { $opts{'right'} = delete($opts{'-right'}); } | |||
0 | |||||||
1046 | 0 | 0 | 0 | if (defined $opts{'-center'} && !defined $opts{'center'}) { $opts{'center'} = delete($opts{'-center'}); } | |||
0 | |||||||
1047 | 0 | 0 | 0 | if (defined $opts{'-left'} && !defined $opts{'left'}) { $opts{'left'} = delete($opts{'-left'}); } | |||
0 | |||||||
1048 | 0 | 0 | 0 | if (defined $opts{'-align'} && !defined $opts{'align'}) { $opts{'align'} = delete($opts{'-align'}); } | |||
0 | |||||||
1049 | 0 | my $wht; | |||||
1050 | |||||||
1051 | 0 | my %trans_opts = ( 'translate' => [$x,$y] ); | |||||
1052 | 0 | my %text_state = (); | |||||
1053 | 0 | 0 | $trans_opts{'rotate'} = $opts{'rotate'} if defined($opts{'rotate'}); | ||||
1054 | |||||||
1055 | 0 | my $wastext = $self->_in_text_object(); | |||||
1056 | 0 | 0 | if ($wastext) { | ||||
1057 | 0 | %text_state = $self->textstate(); | |||||
1058 | 0 | $self->textend(); | |||||
1059 | } | ||||||
1060 | 0 | $self->save(); | |||||
1061 | 0 | $self->textstart(); | |||||
1062 | |||||||
1063 | 0 | $self->transform(%trans_opts); | |||||
1064 | |||||||
1065 | 0 | 0 | $self->fillcolor(ref($opts{'color'}) ? @{$opts{'color'}} : $opts{'color'}) if defined($opts{'color'}); | ||||
0 | 0 | ||||||
1066 | 0 | 0 | $self->strokecolor(ref($opts{'strokecolor'}) ? @{$opts{'strokecolor'}} : $opts{'strokecolor'}) if defined($opts{'strokecolor'}); | ||||
0 | 0 | ||||||
1067 | |||||||
1068 | 0 | $self->font($font, $size); | |||||
1069 | |||||||
1070 | 0 | 0 | $self->charspace($opts{'charspace'}) if defined($opts{'charspace'}); | ||||
1071 | 0 | 0 | $self->hscale($opts{'hscale'}) if defined($opts{'hscale'}); | ||||
1072 | 0 | 0 | $self->wordspace($opts{'wordspace'}) if defined($opts{'wordspace'}); | ||||
1073 | 0 | 0 | $self->render($opts{'render'}) if defined($opts{'render'}); | ||||
1074 | |||||||
1075 | 0 | 0 | 0 | if (defined($opts{'right'}) && $opts{'right'} || | |||
0 | 0 | ||||||
0 | 0 | ||||||
0 | |||||||
0 | |||||||
0 | |||||||
0 | |||||||
0 | |||||||
0 | |||||||
1076 | defined($opts{'align'}) && $opts{'align'} =~ /^r/i) { | ||||||
1077 | 0 | $wht = $self->text_right($text, %opts); | |||||
1078 | } elsif (defined($opts{'center'}) && $opts{'center'} || | ||||||
1079 | defined($opts{'align'}) && $opts{'align'} =~ /^c/i) { | ||||||
1080 | 0 | $wht = $self->text_center($text, %opts); | |||||
1081 | } elsif (defined($opts{'left'}) && $opts{'left'} || | ||||||
1082 | defined($opts{'align'}) && $opts{'align'} =~ /^l/i) { | ||||||
1083 | 0 | $wht = $self->text($text, %opts); # explicitly left aligned | |||||
1084 | } else { | ||||||
1085 | 0 | $wht = $self->text($text, %opts); # left aligned by default | |||||
1086 | } | ||||||
1087 | |||||||
1088 | 0 | $self->textend(); | |||||
1089 | 0 | $self->restore(); | |||||
1090 | |||||||
1091 | 0 | 0 | if ($wastext) { | ||||
1092 | 0 | $self->textstart(); | |||||
1093 | 0 | $self->textstate(%text_state); | |||||
1094 | } | ||||||
1095 | 0 | return $wht; | |||||
1096 | } | ||||||
1097 | |||||||
1098 | =head2 Complex Column Output with Markup | ||||||
1099 | |||||||
1100 | =over | ||||||
1101 | |||||||
1102 | =item ($rc, $next_y, $unused) = $text->column($page, $text, $grfx, $markup, $txt, %opts) | ||||||
1103 | |||||||
1104 | This method fills out a column of text on a page, returning any unused portion | ||||||
1105 | that could not be fit, and where it left off on the page. | ||||||
1106 | |||||||
1107 | Tag names, CSS entries, markup type, etc. are case-sensitive (usually | ||||||
1108 | lower-case letters only). For example, you cannot give a paragraph in |
||||||
1109 | HTML or a B selector in CSS styling. |
||||||
1110 | |||||||
1111 | B<$page> is the page context. Currently, its only use is for page annotations | ||||||
1112 | for links ('md1' []() and 'html' E |
||||||
1113 | you may pass anything such as C |
||||||
1114 | |||||||
1115 | B<$text> is the text context, so that various font and text-output operations | ||||||
1116 | may be performed. It is often, but not necessarily always, the same as the | ||||||
1117 | object containing the "column" method. | ||||||
1118 | |||||||
1119 | B<$grfx> is the graphics (gfx) context. It may be a dummy (e.g., undef) if | ||||||
1120 | I |
||||||
1121 | ('outline' option) and horizontal rule ( in HTML markup) use it. |
||||||
1122 | Currently, I |
||||||
1123 | 'html' C |
||||||
1124 | may in the future require a valid graphics context. Images (when implemented) | ||||||
1125 | will require a graphics context. | ||||||
1126 | |||||||
1127 | B<$markup> is information on what sort of I |
||||||
1128 | and lay out the column's text: | ||||||
1129 | |||||||
1130 | =over | ||||||
1131 | |||||||
1132 | =item 'pre' | ||||||
1133 | |||||||
1134 | The input material has already been processed and is already in the desired | ||||||
1135 | form. C<$txt> is an array reference to the list of hashes. This I |
||||||
1136 | when you are calling C |
||||||
1137 | time to output material left over from the first call. It may also be used when | ||||||
1138 | the caller application has already processed the text into the appropriate | ||||||
1139 | format, and other markup isn't being used. | ||||||
1140 | |||||||
1141 | =item 'none' | ||||||
1142 | |||||||
1143 | If I |
||||||
1144 | a new text array element specifies a new paragraph, and that's it. C<$txt> may | ||||||
1145 | be a single string, or an array (list) of strings. | ||||||
1146 | |||||||
1147 | The input B |
||||||
1148 | containing one or more paragraphs. A single string may also be given. An empty | ||||||
1149 | line between paragraphs may be used to separate the paragraphs. Paragraphs may | ||||||
1150 | not span array elements. | ||||||
1151 | |||||||
1152 | =item 'md1' | ||||||
1153 | |||||||
1154 | This specifies a certain flavor of Markdown compatible with Text::Markdown: | ||||||
1155 | |||||||
1156 | * or _ italics, ** bold, *** bold+italic; | ||||||
1157 | bulleted list *, numbered list 1. 2. etc.; | ||||||
1158 | #, ## etc. headings and subheadings; | ||||||
1159 | ---, ===, ___ horizontal rule; | ||||||
1160 | [label](URL) external links (to HTML page or within this document, see 'a') | ||||||
1161 | ` (backticks) enclose a "code" section | ||||||
1162 | |||||||
1163 | HTML (see below) may be mixed in as desired (although not within "code" blocks | ||||||
1164 | marked by backticks, where <, >, and & get turned into HTML entities, disabling | ||||||
1165 | the intended tags). | ||||||
1166 | Markdown will be converted into HTML, which will then be interpreted into PDF. | ||||||
1167 | I | ||||||
1168 | yet supported by HTML processing (see 'html' section below). Let us know if | ||||||
1169 | you need such a feature!> | ||||||
1170 | |||||||
1171 | The input B |
||||||
1172 | containing one or more paragraphs and other markup. A single string may also be | ||||||
1173 | given. Per Markdown formatting, an empty line between paragraphs may be used to | ||||||
1174 | separate the paragraphs. Separate array elements will first be glued together | ||||||
1175 | into a single string before processing, permitting paragraphs to span array | ||||||
1176 | elements if desired. | ||||||
1177 | |||||||
1178 | There are other flavors of Markdown, so other mdI |
||||||
1179 | in the future, such as POD from Perl code. | ||||||
1180 | |||||||
1181 | =item 'html' | ||||||
1182 | |||||||
1183 | This specifies that a subset of HTML markup is used, along with some attributes | ||||||
1184 | and CSS. Currently, HTML tags | ||||||
1185 | |||||||
1186 | 'i'/'em' (italic), 'b'/'strong' (bold), | ||||||
1187 | 'p' (paragraph), | ||||||
1188 | 'font' (font face->font-family, color, size->font-size), | ||||||
1189 | 'span' (needs style= attribute with CSS to do anything useful), | ||||||
1190 | 'ul', 'ol', 'li' (bulleted, numbered lists), | ||||||
1191 | 'img' (TBD, image, empty. hspace->margin-left/right, | ||||||
1192 | vspace->margin-top/bottom, width, height), | ||||||
1193 | 'a' (anchor/link, web page URL or this document target #p[-x-y[-z]]), | ||||||
1194 | 'pre', 'code' (TBD, preformatted and code blocks), | ||||||
1195 | 'h1' through 'h6' (headings) | ||||||
1196 | 'hr' (horizontal rule) | ||||||
1197 | 'br' (TBD, line break, empty) | ||||||
1198 | 'sup', 'sub' (TBD superscript and subscript) | ||||||
1199 | 's', 'strike', 'del' (line-through) | ||||||
1200 | 'u', 'ins' (underline) | ||||||
1201 | 'ovl' (TBD -- non-HTML, overline) | ||||||
1202 | 'k' (TBD -- non-HTML, kerning left/right shift) | ||||||
1203 | |||||||
1204 | are supported (fully or in part I |
||||||
1205 | color, font-size, font-family, etc. | ||||||
1206 | E |
||||||
1207 | within the E |
||||||
1208 | of the body and added (in order) on to the end of any style tag(s) defined in | ||||||
1209 | a head section. Multiple style tags will be condensed into a single collection | ||||||
1210 | (later definitions of equal precedence overriding earlier). These stylings will | ||||||
1211 | have global effect, as though they were defined in the head. As with normal CSS, | ||||||
1212 | the hierarchy of a given property (in decreasing precedence) is | ||||||
1213 | |||||||
1214 | appearance in a style= tag attribute | ||||||
1215 | appearance in a tag attribute (possibly a different name than the property) | ||||||
1216 | appearance in a #IDname selector in a , and an existing $style | ||||||
3516 | # hashref, update $style and return it | ||||||
3517 | sub _process_style_tag { | ||||||
3518 | 0 | 0 | my ($style, $text) = @_; | ||||
3519 | |||||||
3520 | # expect sets of selector { property: value; ... } | ||||||
3521 | # break up into selector => { property => value, ... } | ||||||
3522 | # replace or add to existing $style | ||||||
3523 | # note that a selector may be a tagName, a .className, or an #idName | ||||||
3524 | |||||||
3525 | 0 | $text =~ s/\n/ /sg; # replace end-of-lines with spaces | |||||
3526 | 0 | while ($text ne '') { | |||||
3527 | 0 | my $selector; | |||||
3528 | |||||||
3529 | 0 | 0 | if ($text =~ s/^\s+//) { # remove leading whitespace | ||||
3530 | 0 | 0 | if ($text eq '') { last; } | ||||
0 | |||||||
3531 | } | ||||||
3532 | 0 | 0 | if ($text =~ s/([^\s]+)//) { # extract selector | ||||
3533 | 0 | $selector = $1; | |||||
3534 | } | ||||||
3535 | 0 | 0 | if ($text =~ s/^\s*{//) { # remove whitespace up through { | ||||
3536 | 0 | 0 | if ($text eq '') { last; } | ||||
0 | |||||||
3537 | } | ||||||
3538 | # one or more property-name: value; sets (; might be missing on last) | ||||||
3539 | # go into %prop_val. we don't expect to see any } within a property | ||||||
3540 | # value string. | ||||||
3541 | 0 | 0 | if ($text =~ s/([^}]+)//) { | ||||
3542 | 0 | $style->{$selector} = _process_style_string({}, $1); | |||||
3543 | } | ||||||
3544 | 0 | 0 | if ($text =~ s/^}\s*//) { # remove closing } and whitespace | ||||
3545 | 0 | 0 | if ($text eq '') { last; } | ||||
0 | |||||||
3546 | } | ||||||
3547 | |||||||
3548 | } | ||||||
3549 | |||||||
3550 | 0 | return $style; | |||||
3551 | } # end of _process_style_tag() | ||||||
3552 | |||||||
3553 | # decompose a style string into property-value pairs. used for both |