File Coverage

blib/lib/PDF/TextBlock.pm
Criterion Covered Total %
statement 168 191 87.9
branch 73 96 76.0
condition 27 35 77.1
subroutine 13 14 92.8
pod 2 2 100.0
total 283 338 83.7


line stmt bran cond sub pod time code
1             package PDF::TextBlock;
2              
3 6     6   1792583 use strict;
  6         62  
  6         193  
4 6     6   34 use warnings;
  6         12  
  6         176  
5 6     6   32 use Carp qw( croak );
  6         10  
  6         353  
6 6     6   843 use File::Temp qw(mktemp);
  6         23401  
  6         478  
7 6     6   3173 use Class::Accessor::Fast;
  6         17335  
  6         56  
8 6     6   2913 use PDF::TextBlock::Font;
  6         14  
  6         37  
9              
10 6     6   224 use base qw( Class::Accessor::Fast );
  6         14  
  6         742  
11             __PACKAGE__->mk_accessors(qw( pdf page text fonts x y w h lead parspace align hang flindent fpindent indent ));
12              
13 6     6   54 use constant mm => 25.4 / 72;
  6         19  
  6         378  
14 6     6   37 use constant in => 1 / 72;
  6         24  
  6         314  
15 6     6   34 use constant pt => 1;
  6         13  
  6         13346  
16              
17             my $debug = 0;
18              
19             =head1 NAME
20              
21             PDF::TextBlock - Easier creation of text blocks when using PDF::API2
22             or PDF::Builder
23              
24             =cut
25              
26             our $VERSION = '0.13';
27              
28             =head1 SYNOPSIS
29              
30             use PDF::API2; # PDF::Builder also works
31             use PDF::TextBlock;
32              
33             my $pdf = PDF::API2->new( -file => "40-demo.pdf" );
34             my $tb = PDF::TextBlock->new({
35             pdf => $pdf,
36             fonts => {
37             b => PDF::TextBlock::Font->new({
38             pdf => $pdf,
39             font => $pdf->corefont( 'Helvetica-Bold', -encoding => 'latin1' ),
40             }),
41             },
42             });
43             $tb->text(
44             $tb->garbledy_gook .
45             ' This fairly lengthy, rather verbose sentence is tagged to appear ' .
46             'in a different font, specifically the one we tagged b for "bold". ' .
47             $tb->garbledy_gook .
48             ' Click here to visit Omni Hotels. ' .
49             $tb->garbledy_gook . "\n\n" .
50             "New paragraph.\n\n" .
51             "Another paragraph."
52             );
53             $tb->apply;
54             $pdf->save;
55             $pdf->end;
56              
57             =head1 DESCRIPTION
58              
59             Neither Rick Measham's excellent L tutorial nor L are able to cope with
60             wanting some words inside a text block to be bold. This module makes that task trivial.
61              
62             Simply define whatever tags you want PDF::TextBlock to honor inside the fonts hashref, and
63             then you are free to use HTML-like markup in the text attribute and we'll render those fonts
64             for you.
65              
66             We also honor the HTML-like tag . This means that we add annotation to the PDF for you
67             which makes the word(s) you wrap in clickable, and we underline those words.
68              
69             Note this markup syntax is very rudimentary. We do not support HTML.
70             Tags cannot overlap each other. There is no way to escape tags inside text().
71              
72             The tests in t/ generate .pdf files. You might find those examples helpful.
73             Watch out for 20-demo.pdf. It spits. :)
74              
75             =head1 METHODS
76              
77             =head2 new
78              
79             Our attributes are listed below. They can be set when you call new(),
80             and/or added/changed individually at any time before you call apply().
81              
82             =over
83              
84             =item pdf
85              
86             A L or L object. You must provide this.
87              
88             =item text
89              
90             The text of your TextBlock. Defaults to garbledy_gook().
91              
92             =item x
93              
94             X position from the left of the document. Default is 20/mm.
95              
96             =item y
97              
98             Y position from the bottom of the document. Default is 238/mm.
99              
100             =item w
101              
102             Width of this text block. Default is 175/mm.
103              
104             =item h
105              
106             Height of this text block. Default is 220/mm.
107              
108             =item align
109              
110             Alignment of words in the text block. Default is 'justify'. Legal values:
111              
112             =over
113              
114             =item justify
115              
116             Spreads words out evenly in the text block so that each line ends in the same spot
117             on the right side of the text block. The last line in a paragraph (too short to fill
118             the entire line) will be set to 'left'.
119              
120             =item fulljustify
121              
122             Like justify, except that the last line is also spread across the page. The last
123             line can look very odd with very large gaps.
124              
125             =item left
126              
127             Aligns each line to the left.
128              
129             =item right
130              
131             Aligns each line to the right.
132              
133             =back
134              
135             =item page
136              
137             A L or L object. If you don't set this
138             manually then we create a new page for you when you call apply().
139              
140             If you want multiple PDF::TextBlock objects to all render onto the same
141             page, you could create a PDF::API2 or PDF::Builder page yourself, and pass
142             it in to each PDF::TextBlock object:
143              
144             my $pdf = PDF::API2->new( -file => "mytest.pdf" );
145             my $page = $pdf->page();
146              
147             my $tb = PDF::TextBlock->new({
148             pdf => $pdf,
149             page => $page, # <---
150             ...
151              
152             Or after your first apply() you could grab $page off of $tb.
153              
154             my $pdf = PDF::API2->new( -file => "mytest.pdf" );
155             my $tb = PDF::TextBlock->new({
156             pdf => $pdf,
157             ...
158             });
159             $tb->apply;
160             my $page = $tb->page; # Use the same page
161              
162             my $tb2 = PDF::TextBlock->new({
163             pdf => $pdf,
164             page => $page, # <---
165             ...
166              
167             =item fonts
168              
169             A hashref of HTML-like markup tags and what font objects you want us to use
170             when we see that tag in text().
171              
172             my $tb = PDF::TextBlock->new({
173             pdf => $pdf,
174             fonts => {
175             # font is a PDF::API2::Resource::Font::CoreFont
176             b => PDF::TextBlock::Font->new({
177             pdf => $pdf,
178             font => $pdf->corefont( 'Helvetica-Bold', -encoding => 'latin1' ),
179             fillcolor => '#ff0000', # red
180             }),
181             },
182             });
183              
184             =back
185              
186             The attributes below came from Rick's text_block(). They do things,
187             but I don't really understand them. POD patches welcome. :)
188              
189             L
190              
191             =over
192              
193             =item lead
194              
195             Leading distance (baseline to baseline spacing). Default is 15/pt.
196              
197             =item parspace
198              
199             Extra gap between paragraphs. Default is 0/pt.
200              
201             =item hang
202              
203             =item flindent
204              
205             =item fpindent
206              
207             =item indent
208              
209             =back
210              
211             =head2 apply
212              
213             This is where we do all the L or L heavy lifting
214             for you.
215              
216             Returns $endw, $ypos, $overflow.
217              
218             I'm not sure what $endw is good for, it's straight from Ricks' code. :)
219              
220             $ypos is useful when you have multiple TextBlock objects and you want to start
221             the next one wherever the previous one left off.
222              
223             my ($endw, $ypos) = $tb->apply();
224             $tb->y($ypos);
225             $tb->text("a bunch more text");
226             $tb->apply();
227              
228             $overflow is whatever text() didn't fit inside your TextBlock.
229             (Too much text? Your font was too big? You set w and h too small?)
230              
231             The original version of this method was text_block(), which is (c) Rick Measham, 2004-2007.
232             The latest version of text_block() can be found in the tutorial located at L.
233             text_block() is released under the LGPL v2.1.
234              
235             =cut
236              
237             sub apply {
238 27     27 1 108824 my ($self, %args) = @_;
239              
240 27         627 my $pdf = $self->pdf;
241 27 50 33     382 unless (ref $pdf eq "PDF::API2" ||
242             ref $pdf eq "PDF::Builder") {
243 0         0 croak "pdf attribute (a PDF::API2 or PDF::Builder object) required";
244             }
245              
246 27         134 $self->_apply_defaults();
247              
248 27         684 my $text = $self->text;
249 27         561 my $page = $self->page;
250              
251             # Build %content_texts. A hash of all PDF::API2::Content::Text objects
252             # (or PDF::Builder), one for each tag ( or or whatever) in $text.
253 27         147 my %content_texts;
254 27         206 foreach my $tag (($text =~ /<(\w*)[^\/].*?>/g), "default") {
255 106 100       311 next if ($content_texts{$tag});
256 34         263 my $content_text = $page->text; # PDF::API2::Content::Text obj
257 34         14072 my $font;
258 34 100 66     891 if ($self->fonts && $self->fonts->{$tag}) {
    50 33        
259 29 50       1021 $debug && warn "using the specific font you set for <$tag>";
260 29         660 $font = $self->fonts->{$tag};
261             } elsif ($self->fonts && $self->fonts->{default}) {
262 0 0       0 $debug && warn "using the default font you set for <$tag>";
263 0         0 $font = $self->fonts->{default};
264             } else {
265 5 50       411 $debug && warn "using PDF::TextBlock::Font default font for <$tag> since you specified neither <$tag> nor a 'default'";
266 5         50 $font = PDF::TextBlock::Font->new({ pdf => $pdf });
267 5         138 $self->fonts->{$tag} = $font;
268             }
269 34         347 $font->apply_defaults;
270 34         1097 $content_text->font($font->font, $font->size);
271 34         13187 $content_text->fillcolor($font->fillcolor);
272 34         8307 $content_text->translate($self->x, $self->y);
273 34         21437 $content_texts{$tag} = $content_text;
274             }
275              
276 27         161 my $content_text = $content_texts{default};
277              
278 27 100       762 if ($self->align eq "text_right") {
279             # Special case... Single line of text that we don't paragraph out...
280             # ... why does this exist? TODO: why can't align 'right' do this?
281             # t/20-demo.t doesn't work align 'right', but I don't know why.
282 1         16 $content_text->text_right($text);
283 1         1007 return 1;
284             }
285              
286 26         300 my ($endw, $ypos);
287              
288             # Get the text in paragraphs
289 26         209 my @paragraphs = split( /\n/, $text );
290              
291             # calculate width of all words
292 26         176 my $space_width = $content_text->advancewidth(' ');
293              
294 26         3319 my @words = split( /\s+/, $text );
295              
296             # Build a hash of widths we refer back to later.
297 26         93 my $current_content_text = $content_texts{default};
298 26         60 my $tag;
299 26         73 my %width = ();
300 26         99 foreach my $word (@words) {
301 1268 100       2436 next if exists $width{$word};
302 1266 100       3163 if (($tag) = ($word =~ /<(.*?)>/)) {
303 83 100       236 if ($tag !~ /\//) {
304 78 50       172 unless ($content_texts{$tag}) {
305             # Huh. They didn't declare this one, so we'll put default in here for them.
306 0         0 $content_texts{$tag} = $content_texts{default};
307             }
308 78         145 $current_content_text = $content_texts{$tag};
309             }
310             }
311            
312 1266         1996 my $stripped = $word;
313 1266         2098 $stripped =~ s/<.*?>//g;
314 1266         2585 $width{$word} = $current_content_text->advancewidth($stripped);
315              
316 1266 100 100     200257 if ($tag && $tag =~ /^\//) {
317 4         11 $current_content_text = $content_texts{default};
318             }
319             }
320              
321 26         893 $ypos = $self->y;
322 26         531 my @paragraph = split( / /, shift(@paragraphs) );
323              
324 26         79 my $first_line = 1;
325 26         53 my $first_paragraph = 1;
326              
327 26         50 my ($href);
328 26         89 $current_content_text = $content_texts{default};
329              
330             # while we can add another line
331 26         505 while ( $ypos >= $self->y - $self->h + $self->lead ) {
332              
333 184 100       7962 unless (@paragraph) {
334 29 100       142 last unless scalar @paragraphs;
335              
336 5         22 @paragraph = split( / /, shift(@paragraphs) );
337              
338 5 50       111 $ypos -= $self->parspace if $self->parspace;
339 5 50       120 last unless $ypos >= $self->y - $self->h;
340              
341 5         117 $first_line = 1;
342 5         11 $first_paragraph = 0;
343             }
344              
345 160         2878 my $xpos = $self->x;
346              
347             # while there's room on the line, add another word
348 160         856 my @line = ();
349              
350 160         303 my $line_width = 0;
351 160 50 66     3549 if ( $first_line && defined $self->hang ) {
    50 66        
    50 66        
    50          
352 0         0 my $hang_width = $content_text->advancewidth( $self->hang );
353              
354 0         0 $content_text->translate( $xpos, $ypos );
355 0         0 $content_text->text( $self->hang );
356              
357 0         0 $xpos += $hang_width;
358 0         0 $line_width += $hang_width;
359 0 0       0 $self->indent($self->indent + $hang_width) if $first_paragraph;
360             } elsif ( $first_line && defined $self->flindent ) {
361 0         0 $xpos += $self->flindent;
362 0         0 $line_width += $self->flindent;
363             } elsif ( $first_paragraph && defined $self->fpindent ) {
364 0         0 $xpos += $self->fpindent;
365 0         0 $line_width += $self->fpindent;
366             } elsif ( defined $self->indent ) {
367 0         0 $xpos += $self->indent;
368 0         0 $line_width += $self->indent;
369             }
370              
371 160         6044 @paragraph = grep { length($_) } @paragraph;
  10640         15362  
372 160   100     3554 while (
373             @paragraph &&
374             $line_width +
375             ( scalar(@line) * $space_width ) +
376             $width{ $paragraph[0] }
377             < $self->w
378             ) {
379 1237         7499 $line_width += $width{ $paragraph[0] };
380 1237         23174 push( @line, shift(@paragraph) );
381             }
382              
383             # calculate the space width
384 160         877 my ( $wordspace, $align );
385 160 100 100     2824 if ( $self->align eq 'fulljustify'
      100        
386             or ( $self->align eq 'justify' and @paragraph )
387             ) {
388 122 50       3249 if ( scalar(@line) == 1 ) {
389 0         0 @line = split( //, $line[0] );
390             }
391 122         2095 $wordspace = ( $self->w - $line_width ) / ( scalar(@line) - 1 );
392 122         679 $align = 'justify';
393             } else {
394             # We've run out of words to fill a full line
395 38 100       1664 $align = ( $self->align eq 'justify' ) ? 'left' : $self->align;
396 38         673 $wordspace = $space_width;
397             }
398 160         399 $line_width += $wordspace * ( scalar(@line) - 1 );
399              
400             # If we want to justify this line, or if there are any markup tags
401             # in here we'll have to split the line up word for word.
402 160 100 100     733 if ( $align eq 'justify' or (grep /<.*>/, @line) ) {
403             # TODO: #4 This loop is DOA for align 'right' and 'center' with any tags.
404             # FMCC Fix proposal
405 132 100       429 if ( $align eq 'center' ) {
    100          
406             # Fix $xpos
407 3         55 $xpos += ( $self->w / 2 ) - ( $line_width / 2 );
408             } elsif ( $align eq 'right' ) {
409             # Fix $xpos
410 3         57 $xpos += $self->w - $line_width;;
411             }
412             # END FMCC Fix Proposal
413 132         326 foreach my $word (@line) {
414 1085 100       3225 if (($tag) = ($word =~ /<(.*?)>/)) {
415             # warn "tag is $tag";
416 83 100       306 if ($tag =~ /^href[a-z]?/) {
    100          
417 1         46 ($tag, $href) = ($tag =~ /(href[a-z]?)="(.*?)"/);
418 1 50       9 $current_content_text = $content_texts{$tag} if ref $content_texts{$tag};
419             } elsif ($tag !~ /\//) {
420 78         157 $current_content_text = $content_texts{$tag};
421             }
422             }
423            
424 1085         1749 my $stripped = $word;
425 1085         2170 $stripped =~ s/<.*?>//g;
426 1085 50       2033 $debug && _debug("$tag 1", $xpos, $ypos, $stripped);
427 1085         3459 $current_content_text->translate( $xpos, $ypos );
428              
429 1085 100       526507 if ($href) {
430 6         20 $current_content_text->text($stripped); # -underline => [2,.5]);
431              
432             # It would be nice if we could use -underline above, but it leaves gaps
433             # between each word, which we don't like. So we'll have to draw our own line
434             # that knows how and when to extend into the space between words.
435 6         1378 my $underline = $page->gfx;
436             # $underline->strokecolor('black');
437 6         1386 $underline->linewidth(.5);
438 6         338 $underline->move( $xpos, $ypos - 2);
439 6 100       666 if ($word =~ /<\/href[a-z]?/) {
440 1         6 $underline->line( $xpos + $width{$word}, $ypos - 2);
441             } else {
442 5         23 $underline->line( $xpos + $width{$word} + $wordspace, $ypos - 2);
443             }
444 6         604 $underline->stroke;
445              
446             # Add hyperlink
447 6         246 my $ann = $page->annotation;
448 6         5561 $ann->rect($xpos, $ypos - 3, $xpos + $width{$word} + $wordspace, $ypos + 10);
449 6         413 $ann->url($href);
450             } else {
451 1079         2930 $current_content_text->text($stripped);
452             }
453              
454 1085 50       286412 unless ($width{$word}) {
455 0 0       0 $debug && _debug("Can't find \$width{$word}");
456 0         0 $width{$word} = 0;
457             }
458 1085 50       2784 $xpos += ( $width{$word} + $wordspace ) if (@line);
459              
460 1085 100       3601 if ($word =~ /\//) {
461 80 100       218 if ($word =~ /\/href[a-z]?/) {
462 1         3 undef $href;
463             }
464 80 100       180 unless ($href) {
465 79         202 $current_content_text = $content_texts{default};
466             }
467             }
468             }
469 132         4208 $endw = $self->w;
470             } else {
471             # calculate the left hand position of the line
472 28 100       132 if ( $align eq 'right' ) {
    100          
473 4         72 $xpos += $self->w - $line_width;
474             } elsif ( $align eq 'center' ) {
475 6         110 $xpos += ( $self->w / 2 ) - ( $line_width / 2 );
476             }
477             # render the line
478 28 50       128 $debug && _debug("default 2", $xpos, $ypos, @line);
479 28         124 $content_text->translate( $xpos, $ypos );
480 28         13880 $endw = $content_texts{default}->text( join( ' ', @line ) );
481             }
482 160         19284 $ypos -= $self->lead;
483 160         3635 $first_line = 0;
484             }
485              
486             # Don't yet know why we'd want to return @paragraphs...
487             # unshift( @paragraphs, join( ' ', @paragraph ) ) if scalar(@paragraph);
488             #return ( $endw, $ypos ); # , join( "\n", @paragraphs ) )
489 26 100       197 unshift( @paragraphs, join( ' ', @paragraph ) ) if scalar(@paragraph);
490 26         140 my $overflow = join("\n",@paragraphs);
491 26         1129 return ( $endw, $ypos, $overflow); #$overflow text returned to script
492             }
493              
494              
495             sub _debug{
496 0     0   0 my ($msg, $xpos, $ypos, @line) = @_;
497 0         0 printf("[%s|%d|%d] ", $msg, $xpos, $ypos);
498 0         0 print join ' ', @line;
499 0         0 print "\n";
500             }
501              
502              
503             =head2 garbledy_gook
504              
505             Returns a scalar containing a paragraph of jibberish. Used by test scripts for
506             demonstrations.
507              
508             my $jibberish = $tb->garbledy_gook(50);
509              
510             The integer is the numer of jibberish words you want returned. Default is 100.
511              
512             =cut
513              
514             sub garbledy_gook {
515 10     10 1 148808 my ($self, $words) = @_;
516 10         26 my $rval;
517 10   100     61 $words ||= 100;
518 10         36 for (1..$words) {
519 930         1712 for (1.. int(rand(10)) + 3) {
520 6929         11313 $rval .= ('a'..'z')[ int(rand(26)) ];
521             }
522 930         1277 $rval .= " ";
523             }
524 10         34 chop $rval;
525 10         332 return $rval;
526             }
527              
528              
529             # Applies defaults for you wherever you didn't explicitly set a different value.
530             sub _apply_defaults {
531 27     27   76 my ($self) = @_;
532 27         245 my %defaults = (
533             x => 20 / mm,
534             y => 238 / mm,
535             w => 175 / mm,
536             h => 220 / mm,
537             lead => 15 / pt,
538             parspace => 0 / pt,
539             align => 'justify',
540             fonts => {},
541             );
542 27         133 foreach my $att (keys %defaults) {
543 216 100       5328 $self->$att($defaults{$att}) unless defined $self->$att;
544             }
545              
546             # Create a new page inside our .pdf unless a page was provided.
547 27 100       751 unless (defined $self->page) {
548 4         98 $self->page($self->pdf->page);
549             }
550              
551             # Use garbledy gook unless text was provided.
552 27 100       5188 unless (defined $self->text) {
553 2         21 $self->text($self->garbledy_gook);
554             }
555             }
556              
557              
558             =head1 AUTHOR
559              
560             Jay Hannah, C<< >>
561              
562             =head1 SUPPORT
563              
564             You can find documentation for this module with the perldoc command.
565              
566             perldoc PDF::TextBlock
567              
568             Source code and bug reports on github: L
569              
570             =head1 ACKNOWLEDGEMENTS
571              
572             This module started from, and has grown on top of, Rick Measham's (aka Woosta)
573             "Using PDF::API2" tutorial: http://rick.measham.id.au/pdf-api2/
574              
575             =head1 COPYRIGHT & LICENSE
576              
577             Copyright 2009-2021 Jay Hannah, all rights reserved.
578              
579             This program is free software; you can redistribute it and/or modify it
580             under the same terms as Perl itself.
581              
582             =cut
583              
584             1; # End of PDF::TextBlock