| 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 |