File Coverage

blib/lib/PDF/TextBlock.pm
Criterion Covered Total %
statement 168 191 87.9
branch 73 96 76.0
condition 26 32 81.2
subroutine 13 14 92.8
pod 2 2 100.0
total 282 335 84.1


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