File Coverage

blib/lib/Mxpress/PDF.pm
Criterion Covered Total %
statement 429 472 90.8
branch 48 90 53.3
condition 32 86 37.2
subroutine 41 41 100.0
pod n/a
total 550 689 79.8


line stmt bran cond sub pod time code
1 2     2   106656 use v5.18;
  2         14  
2 2     2   9 use strict;
  2         3  
  2         33  
3 2     2   17 use warnings;
  2         3  
  2         112  
4              
5             package Mxpress::PDF {
6             our $VERSION = '0.01';
7             use MooX::Pression (
8 2         20 version => '0.01',
9             authority => 'cpan:LNATION',
10 2     2   1166 );
  2         5431458  
11 2     2   51452 use Colouring::In;
  2         4231  
  2         11  
12 2     2   98 use constant mm => 25.4 / 72;
  2         5  
  2         144  
13 2     2   11 use constant pt => 1;
  2         3  
  2         143  
14 2     2   634045 class File () {
  2         4  
  2         17  
  1         8  
  1         2  
  1         1  
15 1         5 has file_name (type => Str, required => 1);
  1         16  
16 1         4 has pdf (required => 1, type => Object);
  1         9  
17 1         4 has pages (required => 1, type => ArrayRef);
  1         15  
18 1         3 has page (type => Object);
  1         8  
19 1         3 has page_args (type => HashRef);
  1         12  
20 1 50 33     4646 method add_page (Map %args) {
  1         5  
  1         2  
  1         4  
  1         2  
21             my $page = $self->FACTORY->page(
22             $self->pdf,
23             page_size => 'A4',
24 1         16 %{ $self->page_args },
  1         38  
25             %args,
26             );
27 1         4 push @{$self->pages}, $page;
  1         17  
28 1         20 $self->page($page);
29 1 50       20 $self->boxed->add( fill_colour => $page->background ) if $page->background;
30 1         21 $self;
31 1         15 }
  1         6  
32 1   33     29 method save {
  1         4  
  1         3  
33 1         16 $self->pdf->saveas();
34 1         58423 $self->pdf->end();
35 1         9 }
36 2     2   5180058 }
  2     2   5  
  2         30  
  1         488  
  2         151  
  2         5  
  2         6  
37 2     2   756948 class Page {
  2         5  
  2         16  
  2         17  
38 2         9 with Utils;
  2         20  
39 2         11 has page_size (type => Str, required => 1);
  2         33  
40 2         5 has background (type => Str);
  2         16  
41 2         12 has page_num (type => Num);
  2         17  
42 2         10 has current (type => Object);
  2         15  
43 2         6 has is_rotated (is => 'rw');
  2         10  
44 2         6 has x (is => 'rw', type => Num);
  2         16  
45 2         4 has y (is => 'rw', type => Num);
  2         14  
46 2         4 has w (is => 'rw', type => Num);
  2         17  
47 2         5 has h (is => 'rw', type => Num);
  2         14  
48 1 50       1133 factory page (Object $pdf, Map %args) {
  1         5  
  1         4  
  1         2  
49 1         6 my $page = $pdf->page();
50 1         764 $page->mediabox($args{page_size});
51 1         158 my ($blx, $bly, $trx, $try) = $page->get_mediabox;
52             my $new_page = $class->new(
53             current => $page,
54             ($args{is_rotated} ? (
55 1 50       51 x => 0,
56             w => $try,
57             h => $trx,
58             y => $trx,
59             ) : (
60             x => 0,
61             w => $trx,
62             h => $try,
63             y => $try,
64             )),
65             %args
66             );
67 1         3420 return $new_page;
68 2         29 }
  2         16  
69 0   0     0 method rotate {
  0         0  
  0         0  
70 0         0 my ($h, $w) = ($self->h, $self->w);
71 0         0 $self->current->mediabox(
72             0,
73             0,
74             $self->w($h),
75             $self->h($self->y($w))
76             );
77 0         0 $self->is_rotated(!$self->is_rotated);
78 0         0 return $self;
79 2         15 }
80 2     2   8556952 }
  2     2   5  
  2         22  
  2         180  
  2         5  
  2         15  
81 2     2   584194 role Utils {
  2         5  
  2         29  
  2         16  
82 4   33     1046 method parse_position (ArrayRef $position) {
  4         11  
  4         5  
83             my ($x, $y, $w, $h) = map {
84 6 50       29 $_ =~ m/[^\d\.]/ ? $_ : $_/mm
85 4         5 } @{$position};
  4         9  
86 4 100       46 $x = $self->page->x + $self->padding unless defined $x;
87 4 100       101 $y = $self->page->y unless defined $y;
88 4 50       58 $y = $self->page->y if $y =~ m/current/;
89 4 100       46 $w = $self->page->w - ($x == $self->padding ? $self->padding * 2 : $x + $self->padding) unless defined $w;
    100          
90 4 100       199 $h = $self->page->y - 10/mm unless defined $h;
91 4         100 return ($x, $y, $w, $h);
92 2         26 }
  2         19  
93 4   33     938 method valid_colour (Str $css) {
  4         11  
  4         5  
94 4         25 return Colouring::In->new($css)->toHEX(1);
95 2         12 }
96 2     2   1214888 }
  2     2   4  
  2         23  
  2         199  
  2         3  
  2         7  
97 2     2   1718273 class Plugin {
  2         4  
  2         22  
  2         13  
  0         0  
98 2         32 with Utils;
  2         19  
99 2         10 has file ( is => rw, type => Object );
  2         46  
100 2         5 has page ( required => 1, is => rw, type => Object );
  2         19  
101 0   0     0 method set_page (Object $page) {
  0         0  
  0         0  
102 0         0 $self->page($page);
103 2         24 }
  2         15  
104 4 50 33     856 method set_attrs (Map %args) {
  4         12  
  4         10  
  4         12  
  4         5  
105 4   66     37 $self->can($_) && $self->$_($args{$_}) for keys %args;
106 2         18 }
  2         9  
107 2     2   3486247 class +Font {
  2         6  
  2         17  
  2         18  
108 2         11 has colour ( is => 'rw', type => Str );
  2         36  
109 2         23 has size ( is => 'rw', type => Num );
  2         19  
110 2         5 has family ( is => 'rw', type => Str );
  2         23  
111 2         12 has loaded ( is => 'rw', type => HashRef );
  2         33  
112 3 50       1094 factory font (Object $file, Map %args) {
  3         11  
  3         9  
  3         5  
113             return $class->new(
114             file => $file,
115             page => $file->page,
116 3   50     55 colour => $file->page->valid_colour($args{colour} || '#000'),
117             size => 9/pt,
118             family => 'Times',
119             %args
120             );
121 2         28 }
  2         28  
122 2   33     21 method load () { $self->find($self->family); }
  3         481  
  3         10  
  3         3  
  3         55  
  2         16  
123 3   33     1039 method find (Str $family, Str $enc?) {
  3         46  
  3         5  
124 3         46 my $loaded = $self->loaded;
125 3 50       22 unless ($loaded->{$family}) {
126 3   50     43 $loaded->{$family} = $self->file->pdf->corefont($family, -encoding => $enc || 'latin1');
127 3         78051 $self->loaded($loaded);
128             }
129 3         72 return $loaded->{$family};
130 2         14 }
131 2     2   4900976 }
  2     2   5  
  2         26  
  2         180  
  2         4  
  2         6  
  2         4  
132 2     2   620067 class +Boxed {
  2         5  
  2         21  
  2         17  
133 2         11 has fill_colour ( is => 'rw', type => Str );
  2         34  
134 2         10 has position ( is => 'rw', type => ArrayRef );
  2         34  
135 2         10 has padding ( is => 'rw', type => Num );
  2         16  
136 1 50       1054 factory boxed (Object $file, Map %args) {
  1         4  
  1         3  
  1         3  
137             return $class->new(
138             page => $file->page,
139             fill_colour => $file->page->valid_colour($args{fill_colour} || '#fff'),
140 1   50     15 padding => $args{padding} || 0
      50        
141             );
142 2         29 }
  2         16  
143 1 50 33     811 method add (Map %args) {
  1         5  
  1         4  
  1         5  
  1         2  
144 1         38 $self->set_attrs(%args);
145 1         31 my $box = $self->page->current->gfx;
146 1   50     284 my $boxed = $box->rect($self->parse_position($self->position || [0, 0, $self->page->w * mm, $self->page->h * mm]));
147 1         171 $boxed->fillcolor($self->fill_colour);
148 1         141 $boxed->fill;
149 1         47 return $self->page;
150 2         20 }
151 2     2   3579185 }
  2     2   5  
  2         22  
  2         198  
  2         3  
  2         7  
  2         3  
152 2     2   1254371 class +Text {
  2         5  
  2         23  
  2         15  
153 2         11 has padding ( is => 'rw', type => Num );
  2         33  
154 2         10 has font ( is => 'rw', type => Object );
  2         16  
155 2         5 has paragraph_space ( is => 'rw', type => Num );
  2         14  
156 2         10 has first_line_indent ( is => 'rw', type => Num );
  2         14  
157 2         5 has first_paragraph_indent ( is => 'rw', type => Num );
  2         14  
158 2         11 has align ( is => 'rw', type => Str ); #enum
  2         17  
159 2         4 has margin_top ( is => 'rw', type => Num );
  2         13  
160 2         5 has margin_bottom ( is => 'rw', type => Num );
  2         14  
161 2         6 has indent ( is => 'rw', type => Num );
  2         14  
162 2         6 has next_page;
  2         8  
163 1 50       1108 factory text (Object $file, Map %args) {
  1         4  
  1         4  
  1         2  
164 1         18 $class->generic_new($file, %args);
165 2         28 }
  2         14  
166 3 50 33     1180 method generic_new (Object $file, Map %args) {
  3         9  
  3         8  
  3         13  
  3         4  
167             return $class->new({
168             file => $file,
169 3         14 page => $file->page,
170 0   0     0 next_page => do { method {
  0         0  
  0         0  
171 0         0 my $self = shift;
172 0         0 $file->add_page;
173 0         0 $self->set_page($file->page);
174 0         0 return $file->page;
175 3         33 } },
176             padding => $args{padding} ? $args{padding}/mm : 0,
177             align => 'left',
178             font => $class->FACTORY->font(
179             $file,
180 3 100       43 %{$args{font}}
  3         612  
181             )
182             });
183 2         24 }
  2         14  
184 3 50 33     1133 method add (Str $string, Map %args) {
  3         8  
  3         9  
  3         9  
  3         3  
185 3         56 $self->set_attrs(%args);
186 3         8 my ($xpos, $ypos);
187 3         11 my @paragraphs = split /\n/, $string;
188             # instantiate a new pdf text object
189 3         49 my $text = $self->page->current->text;
190 3         910 $text->font( $self->font->load, $self->font->size );
191 3         678 $text->fillcolor( $self->font->colour );
192 3         455 my ($total_width, $space_width, %width) = $self->_calculate_widths($string, $text);
193             my ($l, $x, $y, $w, $h) = (
194             $self->font->size,
195 3   100     57 $self->parse_position($args{position} || [])
196             );
197 3         6 $ypos = $y - $l;
198 3 50       50 $ypos -= $self->margin_top/mm if $self->margin_top;
199 3   50     33 my ($fl, $fp, @paragraph) = (1, 1, split ( / /, shift(@paragraphs) || '' ));
200             # while we have enough height to add a new line
201 3         10 while ($ypos >= $y - $h) {
202 6 100       22 unless (@paragraph) {
203 3 50       8 last unless scalar @paragraphs;
204 0         0 @paragraph = split( / /, shift(@paragraphs) );
205 0 0       0 $ypos -= $self->paragraph_space/mm if $self->paragraph_space;
206 0 0       0 last unless $ypos >= $y - $h;
207 0         0 ($fl, $fp) = (1, 0);
208             }
209 3         6 $xpos = $x;
210 3         6 my @line = ();
211 3         4 my $line_width = 0;
212 3         51 ($xpos, $line_width) = $self->_set_indent($xpos, $line_width, $fl, $fp);
213 3   66     325 while (@paragraph and ($line_width + (scalar(@line) * $space_width) + $width{$paragraph[0]}) < $w) {
214 12         27 $line_width += $width{$paragraph[0]};
215 12         46 push @line, shift(@paragraph);
216             }
217 3         15 my ($wordspace, $align);
218 3 50 33     55 if ($self->align eq 'fulljustify' or $self->align eq 'justify' and @paragraph) {
      33        
219 0 0       0 if (scalar(@line) == 1) {
220 0         0 @line = split( //, $line[0] );
221             }
222 0         0 $wordspace = ($w - $line_width) / (scalar(@line) - 1);
223 0         0 $align = 'justify';
224             } else {
225 3 50       138 $align = ($self->align eq 'justify') ? 'left' : $self->align;
226 3         61 $wordspace = $space_width;
227             }
228 3         22 $line_width += $wordspace * (scalar(@line) - 1);
229 3 50       10 if ($align eq 'justify') {
230 0         0 foreach my $word (@line) {
231 0         0 $text->translate($xpos, $ypos);
232 0         0 $text->text($word);
233 0 0       0 $xpos += ($width{$word} + $wordspace) if (@line);
234             }
235             } else {
236 3 50       17 if ($align eq 'right') {
    50          
237 0         0 $xpos += $w - $line_width;
238             } elsif ($align eq 'center') {
239 0         0 $xpos += ($w/2) - ($line_width / 2);
240             }
241 3         20 $text->translate($xpos, $ypos);
242 3         1514 $text->text(join(' ', @line));
243             }
244 3 50       893 $ypos -= $l if @paragraph;
245 3         9 $fl = 0;
246             }
247 3 50       5 unshift( @paragraphs, join( ' ', @paragraph ) ) if scalar(@paragraph);
248 3 50       63 $ypos -= $self->margin_bottom/mm if $self->margin_bottom;
249 3         54 $self->page->y($ypos);
250 3 50 33     125 if (scalar @paragraphs && $self->next_page) {
251 0         0 my $next_page = $self->next_page->($self);
252 0         0 return $self->add(join("\n", @paragraphs), %args);
253             }
254 3         58 return $self->file;
255 2         24 }
  2         10  
256 3   33     1700 method _set_indent (Num $xpos, Num $line_width, Num $fl, Num $fp) {
  3         10  
  3         3  
257 3 50 33     54 if ($fl && $self->first_line_indent) {
    50 33        
    50          
258 0         0 $xpos += $self->first_line_indent;
259 0         0 $line_width += $self->first_line_indent;
260             } elsif ($fp && $self->first_paragraph_indent) {
261 0         0 $xpos = $self->first_paragraph_indent;
262 0         0 $line_width = $xpos;
263             } elsif ($self->indent) {
264 0         0 $xpos += $self->indent;
265 0         0 $line_width += $self->indent
266             }
267 3         143 return ($xpos, $line_width);
268 2         14 }
  2         9  
269 3   33     1095 method _calculate_widths (Str $string, Object $text) {
  3         12  
  3         4  
270 3         14 my @words = split /\s+/, $string;
271             # calculate width of space
272 3         15 my $space_width = $text->advancewidth(' ');
273             # calculate the width of each word
274 3         285 my %width = ();
275 3         5 my $total_width = 0;
276 3         6 foreach (@words) {
277 12 50       36 next if exists $width{$_};
278 12         20 $width{$_} = $text->advancewidth($_);
279 12         1017 $total_width += $width{$_} + $space_width;
280             }
281 3         21 return ($total_width, $space_width, %width);
282 2         13 }
283 2     2   12227391 }
  2     2   18  
  2         23  
  2         173  
  2         4  
  2         6  
  2         3  
284 2     2   516946 class +Title {
  2         4  
  2         20  
  2         18  
285 2         10 extends Plugin::Text;
  2         19  
286 1 50       1057 factory title (Object $file, Map %args) {
  1         3  
  1         5  
  1         2  
287 1   50     7 $args{font}->{size} ||= 50/pt;
288 1         26 $class->generic_new($file, %args);
289 2         25 }
290 2     2   777848 }
  2     2   4  
  2         22  
  2         179  
  2         4  
  2         6  
291 2     2   512321 class +Subtitle {
  2         3  
  2         21  
  2         17  
292 2         11 extends Plugin::Text;
  2         20  
293 1 50       1138 factory subtitle (Object $file, Map %args) {
  1         4  
  1         4  
  1         2  
294 1   50     7 $args{font}->{size} ||= 25/pt;
295 1         24 $class->generic_new($file, %args);
296 2         26 }
297 2     2   769483 }
  2     2   4  
  2         25  
  2         190  
  2         9  
  2         8  
298 2     2   193 }
  2     2   5  
  2         12  
  2         113  
  2         4  
  2         7  
299 2     2   675186 class Factory {
  2         5  
  2         21  
300 2     2   1794 use PDF::API2;
  2         363383  
  2         63  
  2         16  
301 1 50       15057 factory new_pdf (Str $name, Map %args) {
  1         4  
  1         4  
  1         2  
302 1 50       18 my @plugins = (qw/font boxed text title subtitle/, ($args{plugins} ? @{$args{plugins}} : ()));
  0         0  
303             # TODO /o\
304 1         23 my $spec = Mxpress::PDF::File->_generate_package_spec;
305 1         11 for my $p (@plugins) {
306 5         10 my $meth = "_store_${p}";
307 5         14 $spec->{has}->{$meth} = { is => 'rw' };
308             $spec->{can}->{$p} = {
309             named => 0,
310             signature => [],
311             caller => 'Mxpress::PDF',
312             optimize => 0,
313             code => sub {
314 4         1637 my $class = $_[0]->$meth;
315 4 50       9 if (!$class) {
316 4         8 $class = $factory->$p($_[0], %{$args{$p}});
  4         79  
317 4         18761 $_[0]->$meth($class)
318             } else {
319 0         0 $class->set_page($_[0]->page);
320             }
321 4         92 return $class;
322             }
323 5         37 };
324             }
325 2     2   877034 { no strict; no warnings; *Mxpress::PDF::File::_generate_package_spec = sub { return $spec } };
  2     2   6  
  2     1   57  
  2         37  
  2         3  
  2         740  
  1         2  
  1         22  
  1         20  
326 1         23 my $file = Mxpress::PDF::File->generate_package();
327             return $file->new(
328             file_name => $name,
329             pages => [],
330             page_num => 0,
331             page_size => 'A4',
332             page_args => $args{page} || {},
333 1   50     10390 pdf => PDF::API2->new( -file => sprintf("%s.pdf", $name)),
334             );
335 2         27 }
336 2     2   31 }
  2     2   4  
  2         24  
  2         194  
  2         4  
  2         6  
337             }
338              
339             1;
340              
341             __END__
342              
343             =head1 NAME
344              
345             Mxpress::PDF - The great new Mxpress::PDF!
346              
347             =head1 VERSION
348              
349             Version 0.01
350              
351             =cut
352              
353             our $VERSION = '0.01';
354              
355             =head1 SYNOPSIS
356              
357             use Mxpress::PDF;
358              
359             Mxpress::PDF->new_pdf('test-pdf',
360             page => { background => '#000' },
361             title => { font => { colour => '#f00' } },
362             subtitle => { padding => 5, font => { colour => '#0ff' } },
363             text => { padding => 5, font => { colour => '#fff' } }
364             )->add_page->title->add(
365             'This is a title',
366             position => [5, 297]
367             )->subtitle->add(
368             'This is a subtitle.'
369             )->text->add(
370             'This is some text.'
371             )->save();
372              
373             =head2 Note
374              
375             experimental.
376              
377             =head1 AUTHOR
378              
379             LNATION, C<< <thisusedtobeanemail at gmail.com> >>
380              
381             =head1 BUGS
382              
383             Please report any bugs or feature requests to C<bug-mxpress-pdf at rt.cpan.org>, or through
384             the web interface at L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Mxpress-PDF>. I will be notified, and then you'll
385             automatically be notified of progress on your bug as I make changes.
386              
387             =head1 SUPPORT
388              
389             You can find documentation for this module with the perldoc command.
390              
391             perldoc Mxpress::PDF
392              
393             You can also look for information at:
394              
395             =over 4
396              
397             =item * RT: CPAN's request tracker (report bugs here)
398              
399             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Mxpress-PDF>
400              
401             =item * AnnoCPAN: Annotated CPAN documentation
402              
403             L<http://annocpan.org/dist/Mxpress-PDF>
404              
405             =item * CPAN Ratings
406              
407             L<https://cpanratings.perl.org/d/Mxpress-PDF>
408              
409             =item * Search CPAN
410              
411             L<https://metacpan.org/release/Mxpress-PDF>
412              
413             =back
414              
415             =head1 ACKNOWLEDGEMENTS
416              
417             =head1 LICENSE AND COPYRIGHT
418              
419             This software is Copyright (c) 2020 by LNATION.
420              
421             This is free software, licensed under:
422              
423             The Artistic License 2.0 (GPL Compatible)
424              
425             =cut
426              
427             1; # End of Mxpress::PDF