File Coverage

blib/lib/PDF/TextBlock.pm
Criterion Covered Total %
statement 30 191 15.7
branch 0 96 0.0
condition 0 35 0.0
subroutine 10 14 71.4
pod 2 2 100.0
total 42 338 12.4


line stmt bran cond sub pod time code
1             package PDF::TextBlock;
2              
3 1     1   70617 use strict;
  1         2  
  1         32  
4 1     1   5 use warnings;
  1         2  
  1         31  
5 1     1   5 use Carp qw( croak );
  1         2  
  1         48  
6 1     1   822 use File::Temp qw(mktemp);
  1         22315  
  1         65  
7 1     1   496 use Class::Accessor::Fast;
  1         2833  
  1         10  
8 1     1   510 use PDF::TextBlock::Font;
  1         3  
  1         6  
9              
10 1     1   37 use base qw( Class::Accessor::Fast );
  1         2  
  1         83  
11             __PACKAGE__->mk_accessors(qw( pdf page text fonts x y w h lead parspace align hang flindent fpindent indent ));
12              
13 1     1   6 use constant mm => 25.4 / 72;
  1         2  
  1         51  
14 1     1   6 use constant in => 1 / 72;
  1         2  
  1         61  
15 1     1   7 use constant pt => 1;
  1         2  
  1         2169  
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.12';
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 0     0 1   my ($self, %args) = @_;
239              
240 0           my $pdf = $self->pdf;
241 0 0 0       unless (ref $pdf eq "PDF::API2" ||
242             ref $pdf eq "PDF::Builder") {
243 0           croak "pdf attribute (a PDF::API2 or PDF::Builder object) required";
244             }
245              
246 0           $self->_apply_defaults();
247              
248 0           my $text = $self->text;
249 0           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 0           my %content_texts;
254 0           foreach my $tag (($text =~ /<(\w*)[^\/].*?>/g), "default") {
255 0 0         next if ($content_texts{$tag});
256 0           my $content_text = $page->text; # PDF::API2::Content::Text obj
257 0           my $font;
258 0 0 0       if ($self->fonts && $self->fonts->{$tag}) {
    0 0        
259 0 0         $debug && warn "using the specific font you set for <$tag>";
260 0           $font = $self->fonts->{$tag};
261             } elsif ($self->fonts && $self->fonts->{default}) {
262 0 0         $debug && warn "using the default font you set for <$tag>";
263 0           $font = $self->fonts->{default};
264             } else {
265 0 0         $debug && warn "using PDF::TextBlock::Font default font for <$tag> since you specified neither <$tag> nor a 'default'";
266 0           $font = PDF::TextBlock::Font->new({ pdf => $pdf });
267 0           $self->fonts->{$tag} = $font;
268             }
269 0           $font->apply_defaults;
270 0           $content_text->font($font->font, $font->size);
271 0           $content_text->fillcolor($font->fillcolor);
272 0           $content_text->translate($self->x, $self->y);
273 0           $content_texts{$tag} = $content_text;
274             }
275              
276 0           my $content_text = $content_texts{default};
277              
278 0 0         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 0           $content_text->text_right($text);
283 0           return 1;
284             }
285              
286 0           my ($endw, $ypos);
287              
288             # Get the text in paragraphs
289 0           my @paragraphs = split( /\n/, $text );
290              
291             # calculate width of all words
292 0           my $space_width = $content_text->advancewidth(' ');
293              
294 0           my @words = split( /\s+/, $text );
295              
296             # Build a hash of widths we refer back to later.
297 0           my $current_content_text = $content_texts{default};
298 0           my $tag;
299 0           my %width = ();
300 0           foreach my $word (@words) {
301 0 0         next if exists $width{$word};
302 0 0         if (($tag) = ($word =~ /<(.*?)>/)) {
303 0 0         if ($tag !~ /\//) {
304 0 0         unless ($content_texts{$tag}) {
305             # Huh. They didn't declare this one, so we'll put default in here for them.
306 0           $content_texts{$tag} = $content_texts{default};
307             }
308 0           $current_content_text = $content_texts{$tag};
309             }
310             }
311            
312 0           my $stripped = $word;
313 0           $stripped =~ s/<.*?>//g;
314 0           $width{$word} = $current_content_text->advancewidth($stripped);
315              
316 0 0 0       if ($tag && $tag =~ /^\//) {
317 0           $current_content_text = $content_texts{default};
318             }
319             }
320              
321 0           $ypos = $self->y;
322 0           my @paragraph = split( / /, shift(@paragraphs) );
323              
324 0           my $first_line = 1;
325 0           my $first_paragraph = 1;
326              
327 0           my ($href);
328 0           $current_content_text = $content_texts{default};
329              
330             # while we can add another line
331 0           while ( $ypos >= $self->y - $self->h + $self->lead ) {
332              
333 0 0         unless (@paragraph) {
334 0 0         last unless scalar @paragraphs;
335              
336 0           @paragraph = split( / /, shift(@paragraphs) );
337              
338 0 0         $ypos -= $self->parspace if $self->parspace;
339 0 0         last unless $ypos >= $self->y - $self->h;
340              
341 0           $first_line = 1;
342 0           $first_paragraph = 0;
343             }
344              
345 0           my $xpos = $self->x;
346              
347             # while there's room on the line, add another word
348 0           my @line = ();
349              
350 0           my $line_width = 0;
351 0 0 0       if ( $first_line && defined $self->hang ) {
    0 0        
    0 0        
    0          
352 0           my $hang_width = $content_text->advancewidth( $self->hang );
353              
354 0           $content_text->translate( $xpos, $ypos );
355 0           $content_text->text( $self->hang );
356              
357 0           $xpos += $hang_width;
358 0           $line_width += $hang_width;
359 0 0         $self->indent($self->indent + $hang_width) if $first_paragraph;
360             } elsif ( $first_line && defined $self->flindent ) {
361 0           $xpos += $self->flindent;
362 0           $line_width += $self->flindent;
363             } elsif ( $first_paragraph && defined $self->fpindent ) {
364 0           $xpos += $self->fpindent;
365 0           $line_width += $self->fpindent;
366             } elsif ( defined $self->indent ) {
367 0           $xpos += $self->indent;
368 0           $line_width += $self->indent;
369             }
370              
371 0           @paragraph = grep { length($_) } @paragraph;
  0            
372 0   0       while (
373             @paragraph &&
374             $line_width +
375             ( scalar(@line) * $space_width ) +
376             $width{ $paragraph[0] }
377             < $self->w
378             ) {
379 0           $line_width += $width{ $paragraph[0] };
380 0           push( @line, shift(@paragraph) );
381             }
382              
383             # calculate the space width
384 0           my ( $wordspace, $align );
385 0 0 0       if ( $self->align eq 'fulljustify'
      0        
386             or ( $self->align eq 'justify' and @paragraph )
387             ) {
388 0 0         if ( scalar(@line) == 1 ) {
389 0           @line = split( //, $line[0] );
390             }
391 0           $wordspace = ( $self->w - $line_width ) / ( scalar(@line) - 1 );
392 0           $align = 'justify';
393             } else {
394             # We've run out of words to fill a full line
395 0 0         $align = ( $self->align eq 'justify' ) ? 'left' : $self->align;
396 0           $wordspace = $space_width;
397             }
398 0           $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 0 0 0       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 0 0         if ( $align eq 'center' ) {
    0          
406             # Fix $xpos
407 0           $xpos += ( $self->w / 2 ) - ( $line_width / 2 );
408             } elsif ( $align eq 'right' ) {
409             # Fix $xpos
410 0           $xpos += $self->w - $line_width;;
411             }
412             # END FMCC Fix Proposal
413 0           foreach my $word (@line) {
414 0 0         if (($tag) = ($word =~ /<(.*?)>/)) {
415             # warn "tag is $tag";
416 0 0         if ($tag =~ /^href[a-z]?/) {
    0          
417 0           ($tag, $href) = ($tag =~ /(href[a-z]?)="(.*?)"/);
418 0 0         $current_content_text = $content_texts{$tag} if ref $content_texts{$tag};
419             } elsif ($tag !~ /\//) {
420 0           $current_content_text = $content_texts{$tag};
421             }
422             }
423            
424 0           my $stripped = $word;
425 0           $stripped =~ s/<.*?>//g;
426 0 0         $debug && _debug("$tag 1", $xpos, $ypos, $stripped);
427 0           $current_content_text->translate( $xpos, $ypos );
428              
429 0 0         if ($href) {
430 0           $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 0           my $underline = $page->gfx;
436             # $underline->strokecolor('black');
437 0           $underline->linewidth(.5);
438 0           $underline->move( $xpos, $ypos - 2);
439 0 0         if ($word =~ /<\/href[a-z]?/) {
440 0           $underline->line( $xpos + $width{$word}, $ypos - 2);
441             } else {
442 0           $underline->line( $xpos + $width{$word} + $wordspace, $ypos - 2);
443             }
444 0           $underline->stroke;
445              
446             # Add hyperlink
447 0           my $ann = $page->annotation;
448 0           $ann->rect($xpos, $ypos - 3, $xpos + $width{$word} + $wordspace, $ypos + 10);
449 0           $ann->url($href);
450             } else {
451 0           $current_content_text->text($stripped);
452             }
453              
454 0 0         unless ($width{$word}) {
455 0 0         $debug && _debug("Can't find \$width{$word}");
456 0           $width{$word} = 0;
457             }
458 0 0         $xpos += ( $width{$word} + $wordspace ) if (@line);
459              
460 0 0         if ($word =~ /\//) {
461 0 0         if ($word =~ /\/href[a-z]?/) {
462 0           undef $href;
463             }
464 0 0         unless ($href) {
465 0           $current_content_text = $content_texts{default};
466             }
467             }
468             }
469 0           $endw = $self->w;
470             } else {
471             # calculate the left hand position of the line
472 0 0         if ( $align eq 'right' ) {
    0          
473 0           $xpos += $self->w - $line_width;
474             } elsif ( $align eq 'center' ) {
475 0           $xpos += ( $self->w / 2 ) - ( $line_width / 2 );
476             }
477             # render the line
478 0 0         $debug && _debug("default 2", $xpos, $ypos, @line);
479 0           $content_text->translate( $xpos, $ypos );
480 0           $endw = $content_texts{default}->text( join( ' ', @line ) );
481             }
482 0           $ypos -= $self->lead;
483 0           $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 0 0         unshift( @paragraphs, join( ' ', @paragraph ) ) if scalar(@paragraph);
490 0           my $overflow = join("\n",@paragraphs);
491 0           return ( $endw, $ypos, $overflow); #$overflow text returned to script
492             }
493              
494              
495             sub _debug{
496 0     0     my ($msg, $xpos, $ypos, @line) = @_;
497 0           printf("[%s|%d|%d] ", $msg, $xpos, $ypos);
498 0           print join ' ', @line;
499 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 0     0 1   my ($self, $words) = @_;
516 0           my $rval;
517 0   0       $words ||= 100;
518 0           for (1..$words) {
519 0           for (1.. int(rand(10)) + 3) {
520 0           $rval .= ('a'..'z')[ int(rand(26)) ];
521             }
522 0           $rval .= " ";
523             }
524 0           chop $rval;
525 0           return $rval;
526             }
527              
528              
529             # Applies defaults for you wherever you didn't explicitly set a different value.
530             sub _apply_defaults {
531 0     0     my ($self) = @_;
532 0           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 0           foreach my $att (keys %defaults) {
543 0 0         $self->$att($defaults{$att}) unless defined $self->$att;
544             }
545              
546             # Create a new page inside our .pdf unless a page was provided.
547 0 0         unless (defined $self->page) {
548 0           $self->page($self->pdf->page);
549             }
550              
551             # Use garbledy gook unless text was provided.
552 0 0         unless (defined $self->text) {
553 0           $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