File Coverage

blib/lib/Template/Flute/PDF.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Template::Flute::PDF;
2              
3 1     1   45342 use strict;
  1         3  
  1         40  
4 1     1   6 use warnings;
  1         1  
  1         28  
5              
6 1     1   15570 use Data::Dumper;
  1         20059  
  1         643  
7              
8 1     1   13 use File::Basename;
  1         2  
  1         78  
9 1     1   6 use File::Spec;
  1         1  
  1         20  
10 1     1   5940 use Math::Trig;
  1         48577  
  1         327  
11              
12 1     1   1946 use PDF::API2;
  1         382767  
  1         38  
13 1     1   12 use PDF::API2::Util;
  1         2  
  1         213  
14              
15 1     1   480 use Template::Flute::Style::CSS;
  0            
  0            
16              
17             use Template::Flute::PDF::Import;
18             use Template::Flute::PDF::Box;
19              
20             =head1 NAME
21              
22             Template::Flute::PDF - PDF generator for HTML templates
23              
24             =head1 VERSION
25              
26             Version 0.0042
27              
28             =cut
29              
30             our $VERSION = '0.0042';
31              
32             =head1 SYNOPSIS
33              
34             $flute = Template::Flute->new (specification_file => 'invoice.xml',
35             template_file => 'invoice.html',
36             values => \%values);
37             $flute->process();
38              
39             $pdf = Template::Flute::PDF->new (template => $flute->template(),
40             file => 'invoice.pdf');
41              
42             $pdf->process();
43              
44             =head1 DESCRIPTION
45              
46             Template::Flute::PDF is a PDF generator based on L
47             and L.
48              
49             =head2 OUTPUT
50              
51             To obtain the PDF as a string instead of writing it to a file,
52             please simply leave out the file parameter when creating the Template::Flute::PDF
53             object:
54              
55             $pdf = Template::Flute::PDF->new (template => $flute->template(),
56             file => 'invoice.pdf');
57              
58             $pdf_as_string = $pdf->process();
59              
60             =head2 UNITS
61              
62             Template::Flute::PDF uses the pt unit internally.
63              
64             In addition, the following units are supported and automatically
65             converted by this module:
66              
67             =over 4
68              
69             =item in
70              
71             An inch converts to 72 pt.
72              
73             =item cm
74              
75             A centimeter converts to approximately 28.3 pt.
76              
77             =item mm
78              
79             A millimeter converts to approximately 2.8 pt.
80              
81             =item px
82              
83             A pixel converts to 1 pt.
84              
85             =back
86              
87             =head1 CONSTRUCTOR
88              
89             =head2 new
90              
91             Create a Template::Flute::PDF object with the following parameters:
92              
93             =over 4
94              
95             =item template
96              
97             L object.
98              
99             =item file
100              
101             PDF output file.
102              
103             =item page_size
104              
105             Page size for the PDF (default: A4).
106              
107             =item html_base
108              
109             Base directory for HTML resources like images and stylesheets.
110              
111             =item import
112              
113             Import parameters for L.
114              
115             =back
116              
117             =head3 Margin parameters
118              
119             =over 4
120              
121             =item margin_top
122              
123             Top margin, defaults to 20.
124              
125             =item margin_right
126              
127             Right margin, defaults to 20.
128              
129             =item margin_bottom
130              
131             Bottom margin, defaults to 50.
132              
133             =item margin_left
134              
135             Left margin, defaults to 20.
136              
137             =back
138              
139             =cut
140              
141             # defaults
142             use constant FONT_FAMILY => 'Helvetica';
143             use constant FONT_SIZE => '12';
144             use constant PAGE_SIZE => 'a4';
145             use constant MARGINS => (20, 20, 50, 20);
146              
147             # font map for PDF core fonts (see PDF::API2::Resource::Font::CoreFont)
148             our %font_map = (Courier => {Bold => 'Courier-Bold',
149             BoldItalic => 'Courier-BoldOblique',
150             Italic => 'Courier-Oblique',
151             Roman => 'Courier',
152             },
153             Georgia => {Bold => 'Georgia,Bold',
154             BoldItalic => 'Georgia,BoldItalic',
155             Italic => 'Georgia,Italic',
156             Roman => 'Georgia',
157             },
158             Helvetica => {Bold => 'Helvetica-Bold',
159             BoldItalic => 'Helvetica-BoldOblique',
160             Italic => 'Helvetica-Oblique',
161             Roman => 'Helvetica',
162             },
163             Symbol => {},
164             Times => {Bold => 'Times-Bold',
165             BoldItalic => 'Times-BoldItalic',
166             Roman => 'Times',
167             Italic => 'Times-Italic'
168             },
169             Verdana => {Bold => 'Verdana,Bold',
170             BoldItalic => 'Verdana,BoldItalic',
171             Italic => 'Verdana,Italic',
172             Roman => 'Verdana',
173             },
174             Webdings => {},
175             Wingdings => {},
176             ZapfDingbats => {},
177             );
178              
179             sub new {
180             my ($proto, @args) = @_;
181             my ($class, $self);
182              
183             $class = ref($proto) || $proto;
184             $self = {@args};
185             bless ($self, $class);
186            
187             if ($self->{template}) {
188             $self->{xml} = $self->{template}->root();
189             $self->{css} = new Template::Flute::Style::CSS(template => $self->{template});
190             }
191              
192             # create PDF::API2 object
193             if ($self->{file}) {
194             $self->{pdf} = new PDF::API2(-file => $self->{file});
195             }
196             else {
197             $self->{pdf} = new PDF::API2();
198             }
199              
200             # font cache
201             $self->{_font_cache} = {};
202              
203             # page size
204             if ($self->{page_size}) {
205             $self->set_page_size(delete $self->{page_size});
206             }
207             else {
208             $self->set_page_size(PAGE_SIZE);
209             }
210              
211             # page orientation
212             unless ($self->{orientation}) {
213             $self->{orientation} = '';
214             }
215              
216             # margins
217             my @sides = qw(top right bottom left);
218            
219             for (my $i = 0; $i < @sides; $i++) {
220             $self->{'margin_' . $sides[$i]} ||= (MARGINS)[$i];
221             }
222            
223             bless ($self, $class);
224             }
225              
226             =head1 METHODS
227              
228             =head2 process
229              
230             Processes HTML template and creates PDF file.
231              
232             =cut
233              
234             sub process {
235             my $self = shift;
236             my ($file, $font);
237              
238             if (@_) {
239             $file = shift;
240             }
241             else {
242             $file = $self->{file};
243             }
244            
245             $self->{cur_page} = 1;
246              
247             $self->{border_left} = to_points($self->{margin_left}, 'pt');
248             $self->{border_right} = $self->{page_width} - to_points($self->{margin_right}, 'pt');
249              
250             $self->{border_top} = $self->{page_height} - to_points($self->{margin_top}, 'pt');
251             $self->{border_bottom} = to_points($self->{margin_bottom}, 'pt');
252              
253             $self->{vpos_next} = $self->{border_top};
254            
255             $self->{hpos} = $self->{border_left};
256              
257             if ($self->{verbose}) {
258             print "Starting page at X $self->{hpos} Y $self->{y}.\n";
259             print "Borders are T $self->{border_top} R $self->{border_right} B $self->{border_bottom} L $self->{border_left}.\n\n";
260             }
261              
262             my %h = $self->{pdf}->info(
263             'Producer' => "Template::Flute::PDF $VERSION",
264             );
265              
266             if ($self->{import}) {
267             my ($obj, $ret, %import_parms);
268              
269             if (ref($self->{import})) {
270             %import_parms = %{$self->{import}};
271             }
272             else {
273             %import_parms = (file => $self->{import});
274             }
275              
276             $import_parms{pdf} = $self->{pdf};
277            
278             $obj = new Template::Flute::PDF::Import;
279            
280             unless ($ret = $obj->import(%import_parms)) {
281             die "Failed to import file $import_parms{file}.\n";
282             }
283              
284             # if ($self->{verbose} || 1) {
285             # print "Imported PDF $self->{import}: $ret->{pages} pages.\n\n";
286             # }
287              
288             $self->{page} = $ret->{cur_page};
289             # $pdf->saveas();
290             # return;
291             }
292              
293             # Open first page
294             $self->{page} ||= $self->{pdf}->page($self->{cur_page});
295              
296             $self->{pdf}->preferences(
297             -fullscreen => 0,
298             -singlepage => 1,
299             -afterfullscreenoutlines => 1,
300             -firstpage => [ $self->{page} , -fit => 0],
301             -displaytitle => 1,
302             -fitwindow => 0,
303             -centerwindow => 1,
304             -printscalingnone => 1,
305             );
306            
307             # retrieve default settings for font etc from CSS
308             my $css_defaults = $self->{css}->properties(tag => 'body');
309              
310             # set font
311             if ($css_defaults->{font}->{family}) {
312             $self->{fontfamily} = $self->_font_select($css_defaults->{font}->{family});
313             }
314             else {
315             $self->{fontfamily} = FONT_FAMILY;
316             }
317            
318             if ($css_defaults->{font}->{size}) {
319             $self->{fontsize} = to_points($css_defaults->{font}->{size});
320             }
321             else {
322             $self->{fontsize} = FONT_SIZE;
323             }
324              
325             if ($css_defaults->{font}->{weight}) {
326             $self->{fontweight} = $css_defaults->{font}->{weight};
327             }
328             else {
329             $self->{fontweight} = '';
330             }
331              
332             $font = $self->font($self->{fontfamily}, $self->{fontweight});
333            
334             $self->{page}->text->font($font, $self->{fontsize});
335              
336             # move to starting point
337             $self->{page}->text->translate($self->{border_left}, $self->{border_top});
338              
339             # page orientation
340             if ($self->{orientation} eq 'landscape') {
341             $self->{page}->rotate(90);
342             }
343              
344             # now walk HTML document and add appropriate parts
345             my ($root_box, @root_parms);
346              
347             @root_parms = (pdf => $self,
348             elt => $self->{xml},
349             bounding => {vpos => $self->{border_top},
350             hpos => $self->{border_left},
351             max_w => $self->{border_right} - $self->{border_left},
352             max_h => $self->{border_top} - $self->{border_bottom}});
353              
354             $root_box = new Template::Flute::PDF::Box(@root_parms);
355              
356             # calculate sizes
357             $root_box->calculate();
358              
359             # align
360             $root_box->align();
361            
362             # page partitioning
363             $root_box->partition(1, 0);
364              
365             # render
366             $root_box->render(vpos => $self->{border_top},
367             hpos => $self->{border_left});
368            
369             # $self->walk_template($self->{xml});
370              
371             if ($file) {
372             $self->{pdf}->saveas($file);
373             return 1;
374             }
375              
376             return $self->{pdf}->stringify;
377             }
378              
379             sub template {
380             my $self = shift;
381            
382             return $self->{template};
383             }
384              
385             =head2 set_page_size
386              
387             Sets the page size for the PDF.
388              
389             =cut
390              
391             sub set_page_size {
392             my ($self, @args) = @_;
393             my ($ret, @ps);
394              
395             if (ref($args[0]) eq 'ARRAY') {
396             @args = @{$args[0]};
397             }
398            
399             if (@args > 1) {
400             # passing page size as numbers
401             @ps = map {to_points($_, 'pt')} @args;
402             ($self->{page_width}, $self->{page_height}) = @ps;
403             }
404             else {
405             # resolve page size
406             unless ($self->{_paper_sizes}) {
407             $self->{_paper_sizes} = {getPaperSizes()};
408             }
409              
410             if (exists $self->{_paper_sizes}->{lc($args[0])}) {
411             ($self->{page_width}, $self->{page_height})
412             = @{$self->{_paper_sizes}->{lc($args[0])}};
413             }
414             else {
415             die "Invalid paper size $args[0]";
416             }
417            
418             $ps[0] = $args[0];
419             }
420            
421             $self->{_page_size} = \@ps;
422              
423             $self->{pdf}->mediabox(@ps);
424             }
425              
426             =head2 select_page PAGE_NUM
427            
428             Selects page with the given PAGE_NUM. Creates new page if necessary.
429              
430             =cut
431              
432             sub select_page {
433             my ($self, $page_num) = @_;
434             my ($diff, $cur_page);
435            
436             if ($page_num > $self->{pdf}->pages()) {
437             $diff = $page_num - $self->{pdf}->pages();
438              
439             for (my $i = 0; $i < $diff; $i++) {
440             $cur_page = $self->{pdf}->page();
441             }
442             }
443             else {
444             $cur_page = $self->{pdf}->openpage($page_num);
445             }
446              
447             $self->{page} = $cur_page;
448             }
449              
450             =head2 content_height
451              
452             Returns the height of the content part of the page.
453              
454             =cut
455            
456             sub content_height {
457             my ($self) = @_;
458             my ($height);
459              
460             return $self->{page_height};
461             }
462              
463             =head2 content_width
464              
465             Returns the width of the content part of the page.
466              
467             =cut
468            
469             sub content_width {
470             my ($self) = @_;
471             my ($width);
472            
473             $width = $self->{page_width} - to_points($self->{margin_left}, 'pt')
474             - to_points($self->{margin_right}, 'pt');
475              
476             return $width;
477             }
478              
479             =head2 bounding
480              
481             Returns the bounding box for the PDF as a hash reference
482             with the following key/value pairs:
483              
484             =over 4
485              
486             =item vpos
487              
488             Top vertical position.
489              
490             =item pos
491              
492             Left horizonal position.
493              
494             =item max_w
495              
496             Maximum width.
497              
498             =item max_h
499              
500             Maximum height.
501              
502             =back
503              
504             The bounding box defines the available space without
505             the borders.
506              
507             =cut
508              
509             sub bounding {
510             my $self = shift;
511            
512             return {vpos => $self->{border_top},
513             hpos => $self->{border_left},
514             max_w => $self->{border_right} - $self->{border_left},
515             max_h => $self->{border_top} - $self->{border_bottom},
516             };
517             }
518              
519             =head2 font NAME [weight] [style]
520              
521             Returns PDF::API2 font object for font NAME, WEIGHT and STYLE are optional.
522              
523             =cut
524            
525             sub font {
526             my ($self, $name, $weight, $style) = @_;
527             my ($key, $obj);
528              
529             if ($weight eq 'normal') {
530             # default font weight
531             $weight = '';
532             }
533              
534             # determine font name from supplied name and optional weight
535             if ($weight) {
536             if ($style) {
537             $key = "$name-$weight$style";
538             }
539             else {
540             $key = "$name-$weight";
541             }
542             }
543             elsif ($style) {
544             if (exists $font_map{$name}->{$style}) {
545             $key = $font_map{$name}->{$style};
546             }
547             else {
548             $key = "$name-$style";
549             }
550             }
551             else {
552             $key = $name;
553             }
554            
555             if (exists $self->{_font_cache}->{$key}) {
556             # return font object from cache
557             return $self->{_font_cache}->{$key};
558             }
559              
560             # create new font object
561             $obj = $self->{pdf}->corefont($key, -encoding => 'latin1');
562              
563             $self->{_font_cache}->{$key} = $obj;
564            
565             return $obj;
566             }
567              
568             =head2 text_filter TEXT
569              
570             Adjusts whitespace in TEXT for output in PDF.
571              
572             =cut
573            
574             sub text_filter {
575             my ($self, $text, $transform) = @_;
576             my ($orig);
577            
578             # fall back to empty string
579             unless (defined $text) {
580             return '';
581             }
582              
583             $orig = $text;
584            
585             # replace newlines with blanks
586             $text =~ s/\n/ /gs;
587              
588             # collapse blanks
589             $text =~ s/\s+/ /g;
590              
591             if (length $orig && ! length $text) {
592             # reduce not further than a single whitespace
593             return ' ';
594             }
595              
596             # transform text analogous to CSS specification
597             if (defined $transform) {
598             if ($transform eq 'uppercase') {
599             $text = uc($text);
600             }
601             elsif ($transform eq 'lowercase') {
602             $text = lc($text);
603             }
604             elsif ($transform eq 'capitalize') {
605             $text =~ s/\b(\w)/\u$1/g;
606             }
607             elsif ($transform ne 'none') {
608             die "Unknown transformation $transform\n";
609             }
610             }
611            
612             return $text;
613             }
614              
615             =head2 setup_text_props ELT SELECTOR [INHERIT]
616              
617             Determines text properties for HTML template element ELT, CSS selector SELECTOR
618             and INHERIT flag.
619              
620             =cut
621              
622             sub setup_text_props {
623             my ($self, $elt, $selector, $inherit) = @_;
624             my ($props, %borders, %padding, %margins, %offset, $fontsize, $fontfamily,
625             $fontweight, $fontstyle, $txeng);
626              
627             my $class = $elt->att('class') || '';
628             my $id = $elt->att('id') || '';
629             my $gi = $elt->gi();
630              
631             $selector ||= '';
632            
633             # get properties from CSS
634             $props = $self->{css}->properties(id => $id,
635             class => $elt->att('class'),
636             tag => $elt->gi(),
637             selector => $selector,
638             inherit => $inherit,
639             );
640            
641             $txeng = $self->{page}->text;
642              
643             if ($props->{font}->{size}) {
644             if ($props->{font}->{size} =~ s/^(\d+)(pt)?$/$1/) {
645             $fontsize = $props->{font}->{size};
646             }
647             else {
648             $fontsize = to_points($props->{font}->{size});
649             }
650             }
651             else {
652             $fontsize = $self->{fontsize};
653             }
654              
655             if ($props->{font}->{family}) {
656             $fontfamily = $self->_font_select($props->{font}->{family});
657             }
658             else {
659             $fontfamily = $self->{fontfamily};
660             }
661              
662             if ($props->{font}->{weight}) {
663             $fontweight = $props->{font}->{weight};
664             }
665             else {
666             $fontweight = $self->{fontweight};
667             }
668              
669             if ($props->{font}->{style}) {
670             $fontstyle = $props->{font}->{style};
671             }
672             elsif ($gi eq 'i') {
673             $fontstyle = 'Italic';
674             }
675              
676             $self->{font} = $self->font($fontfamily, $fontweight,
677             $fontstyle);
678            
679             $txeng->font($self->{font}, $fontsize);
680              
681             if ($gi eq 'hr') {
682             unless (keys %{$props->{margin}}) {
683             # default margins for horizontal rule
684             my $margin;
685              
686             $margin = 0.5 * $fontsize;
687              
688             $props->{margin} = {top => $margin,
689             bottom => $margin};
690             }
691             }
692            
693             # offsets from border, padding etc.
694             for my $s (qw/top right bottom left/) {
695             $borders{$s} = to_points($props->{border}->{$s}->{width});
696             $margins{$s} = to_points($props->{margin}->{$s});
697             $padding{$s} = to_points($props->{padding}->{$s});
698              
699             $offset{$s} += $margins{$s} + $borders{$s} + $padding{$s};
700             }
701              
702             # height and width
703             $props->{width} = to_points($props->{width});
704             $props->{height} = to_points($props->{height});
705            
706             return {font => $self->{font}, size => $fontsize, offset => \%offset,
707             borders => \%borders, margins => \%margins, padding => \%padding, props => $props,
708             # for debugging
709             class => $class, selector => $selector
710             };
711             }
712              
713             =head2 calculate ELT [PARAMETERS]
714              
715             Calculates width and height for HTML template element ELT.
716              
717             =cut
718            
719             sub calculate {
720             my ($self, $elt, %parms) = @_;
721             my ($text, $chunk_width, $text_width, $max_width, $avail_width, $height, $specs, $txeng,
722             $overflow_x, $overflow_y, $clear_before, $clear_after, @chunks, $buf, $lines);
723            
724             $txeng = $self->{page}->text();
725             $max_width = 0;
726             $height = 0;
727             $overflow_x = $overflow_y = 0;
728             $clear_before = $clear_after = 0;
729             $lines = 1;
730              
731             if ($parms{specs}) {
732             $specs = $parms{specs};
733             }
734             else {
735             $specs = $self->setup_text_props($elt);
736             }
737              
738             if ($specs->{props}->{width}) {
739             $avail_width = $specs->{props}->{width};
740             }
741             else {
742             $avail_width = $self->content_width();
743             }
744              
745             if (ref($parms{text}) eq 'ARRAY') {
746             $buf = '';
747             $text_width = 0;
748            
749             for my $text (@{$parms{text}}) {
750             if ($text eq "\n") {
751             # force newline
752             push (@chunks, $buf . $text);
753             $buf = '';
754             $text_width = 0;
755             $lines++;
756             }
757             elsif ($text =~ /\S/) {
758             $chunk_width = $txeng->advancewidth($text, font => $specs->{font},
759             fontsize => $specs->{size});
760             }
761             else {
762             # whitespace
763             $chunk_width = $txeng->advancewidth("\x20", font => $specs->{font},
764             fontsize => $specs->{size});
765             }
766              
767             if ($avail_width
768             && $text_width + $chunk_width > $avail_width) {
769             # print "Line break by long text: $buf + $text\n";
770              
771             push (@chunks, $buf);
772             $buf = $text;
773             $text_width = 0;
774             $lines++;
775             }
776             else {
777             $buf .= $text;
778             }
779              
780             $text_width += $chunk_width;
781            
782             if ($text_width > $max_width) {
783             $max_width = $text_width;
784             }
785             }
786              
787             if (length($buf)) {
788             push (@chunks, $buf);
789             }
790             }
791              
792             if ($parms{clear} || $specs->{props}->{clear} eq 'both') {
793             $clear_before = $clear_after = 1;
794             }
795             elsif ($specs->{props}->{clear} eq 'left') {
796             $clear_before = 1;
797             }
798             elsif ($specs->{props}->{clear} eq 'right') {
799             $clear_after = 1;
800             }
801            
802             # print "Before offset: MW $max_width H $height S $specs->{size}, ", Dumper($specs->{offset}) . "\n";
803            
804             # print "PW $avail_width, PH $specs->{props}->{height}, MW $max_width H $height\n";
805              
806             # line height
807             if (exists $specs->{props}->{line_height}) {
808             $height = $lines * to_points($specs->{props}->{line_height});
809             }
810             else {
811             $height = $lines * $specs->{size};
812             }
813              
814             # adjust to fixed width
815             if ($avail_width) {
816             if ($avail_width < $max_width) {
817             $overflow_x = $max_width - $avail_width;
818             $max_width = $avail_width;
819             }
820             }
821              
822             # adjust to fixed height
823             if ($specs->{props}->{height}) {
824             if ($specs->{props}->{height} < $height) {
825             $overflow_y = $height - $specs->{props}->{height};
826             $height = $specs->{props}->{height};
827             }
828             else {
829             $height = $specs->{props}->{height};
830             }
831             }
832            
833             return {width => $max_width, height => $height, size => $specs->{size},
834             clear => {before => $clear_before, after => $clear_after},
835             overflow => {x => $overflow_x, y => $overflow_y},
836             text_width => $text_width,
837             chunks => \@chunks,
838             };
839             }
840              
841             =head2 check_out_of_bounds POS DIM
842              
843             Check whether we are out of bounds with position POS and dimensions DIM.
844              
845             =cut
846              
847             sub check_out_of_bounds {
848             my ($self, $pos, $dim) = @_;
849              
850             if ($pos->{hpos} == $self->{border_right}) {
851             # we are on the left border, so even if the box is out
852             # of bounds, we have no better idea :-)
853             return;
854             }
855            
856             # print "COB pos: " . Dumper($pos) . "COB dim: " . Dumper($dim);
857             # print "NEXT: $self->{vpos_next}.\n";
858              
859             if ($pos->{hpos} + $dim->{width} > $self->{border_right}) {
860             return {hpos => $self->{border_left}, vpos => $self->{vpos_next}};
861             }
862            
863             return;
864             }
865              
866             =head2 textbox ELT TEXT PROPS BOX ATTRIBUTES
867              
868             Adds textbox for HTML template element ELT to the PDF.
869              
870             =cut
871              
872             sub textbox {
873             my ($self, $elt, $boxtext, $boxprops, $box, %atts) = @_;
874             my ($width_last, $y_top, $y_last, $left_over, $text_width, $text_height, $box_height);
875             my (@tb_parms, %parms, $txeng, %offset, %borders, %padding, $props,
876             $paragraph, $specs, %text_options, $decoration);
877              
878             if ($boxprops) {
879             $specs = $boxprops;
880             }
881             else {
882             # get specifications from CSS
883             $specs = $self->setup_text_props($elt);
884             }
885              
886             # unless ($specs->{borders}) {
887             # delete $specs->{font};
888             # print "Elt: ", $elt->sprint(), "\n";
889             # print "Specs for textbox: " . Dumper($specs) . "\n";
890             # }
891            
892             $props = $specs->{props};
893             %borders = %{$specs->{borders}};
894             %offset = %{$specs->{offset}};
895             %padding = %{$specs->{padding}};
896              
897             if ($box) {
898             # print "Set from box: " . Dumper($box) . " for size $specs->{size}\n";
899             $self->{hpos} = $box->{hpos};
900             $self->{y} = $box->{vpos};
901             }
902              
903             $txeng = $self->{page}->text;
904             $txeng->font($specs->{font}, $specs->{size});
905            
906             #print "Starting pos: X $self->{hpos} Y $self->{y}\n";
907             $txeng->translate($self->{hpos}, $self->{y});
908            
909             # determine resulting horizontal position
910             $text_width = $txeng->advancewidth($boxtext);
911             #print "Hpos after: " . $text_width . "\n";
912              
913             # now draw the background for text box
914             if ($props->{background}->{color}) {
915             # print "Background for text box: $props->{background}->{color}\n";
916             $self->rect($self->{hpos}, $self->{y},
917             $self->{hpos} + $text_width, $self->{y} - $padding{top} - $specs->{size} - $padding{bottom},
918             $props->{background}->{color});
919             }
920              
921             # colors
922             if ($props->{color}) {
923             $txeng->fillcolor($props->{color});
924             }
925            
926             %parms = (x => $self->{hpos},
927             y => $self->{y} - $specs->{size},
928             w => $self->content_width(),
929             h => to_points(100),
930             lead => $specs->{size},
931             # align => $props->{text}->{align} || 'left',
932             align => 'left',
933             );
934            
935             @tb_parms = ($txeng, $boxtext, %parms);
936              
937             #print "Add textbox (class " . ($elt->att('class') || "''") . ") with content '$boxtext' at $parms{y} x $parms{x}, border $offset{top}\n";
938              
939             if ($decoration = $props->{text}->{decoration}) {
940             if ($decoration eq 'underline') {
941             $text_options{'-underline'} = 1;
942             }
943             }
944              
945             if (length($boxtext) && $boxtext =~ /\S/) {
946             if ($props->{line_height}) {
947             # adjust text position accordingly
948             $parms{y} -= (to_points($props->{line_height}) - $specs->{size}) / 2;
949             }
950             # try different approach
951             if (exists $props->{rotate}) {
952             $txeng->translate($parms{x}, $parms{y});
953             $txeng->transform_rel(-rotate => 360 - $props->{rotate});
954             }
955             else {
956             $txeng->translate($parms{x}, $parms{y});
957             }
958              
959             $txeng->text($boxtext, %text_options);
960             }
961             else {
962             $y_last = $parms{y};
963             }
964              
965             $txeng->fill();
966             }
967              
968             =head2 hline SPECS HPOS VPOS LENGTH WIDTH
969              
970             Add horizontal line to PDF.
971              
972             =cut
973            
974             sub hline {
975             my ($self, $specs, $hpos, $vpos, $width, $height) = @_;
976             my ($gfx);
977              
978             $gfx = $self->{page}->gfx;
979              
980             $self->begin_transform($gfx, $hpos, $vpos - $height / 2, $width, $height || 1, $specs->{props});
981              
982             # set line color
983             $gfx->strokecolor($specs->{props}->{color});
984              
985             # set line width
986             $gfx->linewidth($height || 1);
987              
988             $gfx->line($width, 0);
989            
990             # draw line
991             $gfx->stroke();
992              
993             $self->end_transform($gfx, $hpos, $vpos - $height / 2, $width, $height || 1, $specs->{props});
994              
995             return;
996             }
997              
998             =head2 borders X_LEFT Y_TOP WIDTH HEIGHT
999              
1000             Adds borders to the PDF.
1001              
1002             =cut
1003              
1004             sub borders {
1005             my ($self, $x_left, $y_top, $width, $height, $specs) = @_;
1006             my ($gfx);
1007            
1008             $gfx = $self->{page}->gfx;
1009            
1010             if ($specs->{borders}->{top}) {
1011             $gfx->strokecolor($specs->{props}->{border}->{top}->{color});
1012             $gfx->linewidth($specs->{borders}->{top});
1013             $gfx->move($x_left, $y_top - $specs->{borders}->{top} * 0.5);
1014             $gfx->line($x_left + $width, $y_top - $specs->{borders}->{top} * 0.5);
1015             $gfx->stroke();
1016             }
1017              
1018             if ($specs->{borders}->{left}) {
1019             $gfx->strokecolor($specs->{props}->{border}->{left}->{color});
1020             $gfx->linewidth($specs->{borders}->{left});
1021             $gfx->move($x_left + 0.5 * $specs->{borders}->{left}, $y_top);
1022             $gfx->line($x_left + 0.5 * $specs->{borders}->{left} , $y_top - $height); #- $specs->{borders}->{top});
1023            
1024             $gfx->stroke();
1025             }
1026            
1027             if ($specs->{borders}->{bottom}) {
1028             $gfx->strokecolor($specs->{props}->{border}->{bottom}->{color});
1029             $gfx->linewidth($specs->{borders}->{bottom});
1030             $gfx->move($x_left, $y_top - $height + 0.5 * $specs->{borders}->{bottom} );
1031             $gfx->line($x_left + $width, $y_top - $height + 0.5 * $specs->{borders}->{bottom} );
1032             $gfx->stroke();
1033             }
1034              
1035             if ($specs->{borders}->{right}) {
1036             $gfx->strokecolor($specs->{props}->{border}->{right}->{color});
1037             $gfx->linewidth($specs->{borders}->{right});
1038             $gfx->move($x_left + $width - 0.5 * $specs->{borders}->{right}, $y_top);
1039             $gfx->line($x_left + $width - 0.5 * $specs->{borders}->{right}, $y_top - $height);
1040             $gfx->stroke();
1041             }
1042             }
1043              
1044             =head2 rect X_LEFT Y_TOP X_RIGHT Y_BOTTOM COLOR
1045              
1046             Adds rectangle to the PDF.
1047              
1048             =cut
1049              
1050             # primitives
1051             sub rect {
1052             my ($self, $x_left, $y_top, $x_right, $y_bottom, $color) = @_;
1053             my ($gfx);
1054              
1055             $gfx = $self->{page}->gfx;
1056              
1057             if ($color) {
1058             $gfx->fillcolor($color);
1059             }
1060              
1061             $gfx->rectxy($x_left, $y_top, $x_right, $y_bottom);
1062              
1063             if ($color) {
1064             $gfx->fill();
1065             }
1066             }
1067              
1068             =head2 locate_image
1069              
1070             Determines location of an image file from the C HTML
1071             attribute.
1072              
1073             $imgfile = $pdf->locate_image('images/cart.png');
1074              
1075             The location is based on the current directory, or on
1076             the C constructor parameter if the C HTML
1077             attribute contains a single file name only.
1078            
1079             =cut
1080              
1081             sub locate_image {
1082             my ($self, $src) = @_;
1083             my ($img_dir, $template_dir, $img_file);
1084              
1085             if ($self->{swap_images}) {
1086             for my $href (@{$self->{swap_images}}) {
1087             if ($href->{src} eq $src) {
1088             $src = $href->{path};
1089             }
1090             }
1091             }
1092              
1093             $img_dir = dirname($src);
1094             $img_file = $src;
1095              
1096             if ($img_dir eq '.') {
1097             # check whether HTML template is located in another directory
1098             $template_dir = dirname($self->template()->file());
1099              
1100             if ($template_dir ne '.') {
1101             if ($self->{html_base}) {
1102             $img_file = File::Spec->catfile($self->{html_base},
1103             basename($src));
1104             }
1105             else {
1106             $img_file = File::Spec->catfile($template_dir,
1107             basename($src));
1108             }
1109             }
1110             }
1111              
1112             return $img_file;
1113             }
1114              
1115             =head2 image OBJECT HPOS VPOS WIDTH HEIGHT
1116              
1117             Add image OBJECT to the PDF.
1118              
1119             =cut
1120              
1121             sub image {
1122             my ($self, $object, $x_left, $y_top, $width, $height, $specs) = @_;
1123             my ($gfx, $method, $image_object);
1124              
1125             $gfx = $self->{page}->gfx;
1126            
1127             $method = 'image_' . $object->{type};
1128              
1129             $image_object = $self->{pdf}->$method($object->{file});
1130              
1131             $gfx->image($image_object, $x_left, $y_top, $width, $height);
1132             }
1133              
1134             =head2 begin_transform
1135              
1136             Starts transformation of current content object.
1137              
1138             =cut
1139              
1140             sub begin_transform {
1141             my ($self, $gfx, $hpos, $vpos, $width, $height, $props) = @_;
1142              
1143             $gfx->move(0,0);
1144            
1145             if (exists $props->{translate}->{x}) {
1146             $hpos += to_points($props->{translate}->{x});
1147             }
1148              
1149             if (exists $props->{translate}->{y}) {
1150             $vpos -= to_points($props->{translate}->{y});
1151             }
1152              
1153             $gfx->translate($hpos, $vpos);
1154            
1155             if ($props->{rotate}) {
1156             $gfx->rotate(- $props->{rotate});
1157             }
1158             }
1159              
1160             =head2 end_transform
1161              
1162             Ends transformation of current content object.
1163              
1164             =cut
1165              
1166             sub end_transform {
1167             my ($self, $gfx, $hpos, $vpos, $width, $height, $props) = @_;
1168            
1169             if ($props->{rotate}) {
1170             $gfx->rotate($props->{rotate});
1171             }
1172              
1173             if (exists $props->{translate}->{x}) {
1174             $hpos += to_points($props->{translate}->{x});
1175             }
1176              
1177             if (exists $props->{translate}->{y}) {
1178             $vpos -= to_points($props->{translate}->{y});
1179             }
1180              
1181             $gfx->translate(-$hpos, -$vpos);
1182             }
1183              
1184             =head1 FUNCTIONS
1185              
1186             =head2 to_points [DEFAULT_UNIT]
1187            
1188             Converts widths to points, default unit is mm.
1189              
1190             =cut
1191            
1192             sub to_points {
1193             my ($width, $default_unit) = @_;
1194             my ($unit, $points, $negative);
1195              
1196             return 0 unless defined $width;
1197              
1198             if ($width =~ s/^(-?)(\d+(\.\d+)?)\s?(in|px|pt|cm|mm)?$/$2/) {
1199             $negative = $1;
1200             $unit = $4 || $default_unit || 'mm';
1201             }
1202             else {
1203             warn "Invalid width $width\n";
1204             return;
1205             }
1206              
1207             if ($unit eq 'in') {
1208             # 72 points per inch
1209             $points = 72 * $width;
1210             }
1211             elsif ($unit eq 'cm') {
1212             $points = 72 * $width / 2.54;
1213             }
1214             elsif ($unit eq 'mm') {
1215             $points = 72 * $width / 25.4;
1216             }
1217             elsif ($unit eq 'pt') {
1218             $points = $width;
1219             }
1220             elsif ($unit eq 'px') {
1221             $points = $width;
1222             }
1223              
1224             if ($negative) {
1225             return - $points;
1226             }
1227              
1228             return $points;
1229             }
1230              
1231             # auxiliary methods
1232              
1233             # select font from list provided by CSS (currently just the first)
1234              
1235             sub _font_select {
1236             my ($self, $font_string) = @_;
1237             my (@fonts);
1238              
1239             @fonts = split(/,/, $font_string);
1240              
1241             return $fonts[0];
1242             }
1243              
1244             =head1 SUPPORTED HTML/CSS SYNTAX
1245              
1246             This is an incomplete list of supported HTML/CSS syntax.
1247              
1248             =head2 HTML tags and attributes
1249              
1250            
1251              
1252             =head3 style
1253              
1254             The HTML attribute "style" is not supported.
1255              
1256             =head2 CSS properties
1257              
1258             =head3 display
1259              
1260             The CSS property "display" is not supported.
1261              
1262             =head3 font-weight
1263              
1264             The values "normal" and "bold" are supported.
1265              
1266             =head3 min-height
1267              
1268             The CSS property "min-height" is supported.
1269              
1270             =head3 min-width
1271              
1272             The CSS property "min-width" is supported.
1273            
1274             =head3 text-transformation
1275              
1276             The CSS property "text-transformation" is supported with
1277             the exception of the value "inherit".
1278              
1279             =head1 AUTHOR
1280              
1281             Stefan Hornburg (Racke),
1282              
1283             =head1 BUGS
1284              
1285             Certainly a lot, as converting from HTML to PDF is quite complicated and challenging.
1286              
1287             Please report any unknown bugs or feature requests to C,
1288             or through the web interface at L.
1289              
1290             =head2 KNOWN BUGS
1291              
1292             =over 4
1293              
1294             =item Background color
1295              
1296             Using background color hides text.
1297              
1298             =item Vertical align
1299              
1300             We currently support only aligning to top or bottom of the available space.
1301             This is in contradiction to HTML, where the default vertical align
1302             is baseline (of the text).
1303              
1304             =back
1305              
1306             =head1 SUPPORT
1307              
1308             You can find documentation for this module with the perldoc command.
1309              
1310             perldoc Template::Flute::PDF
1311              
1312             You can also look for information at:
1313              
1314             =over 4
1315              
1316             =item * RT: CPAN's request tracker
1317              
1318             L
1319              
1320             =item * AnnoCPAN: Annotated CPAN documentation
1321              
1322             L
1323              
1324             =item * CPAN Ratings
1325              
1326             L
1327              
1328             =item * Search CPAN
1329              
1330             L
1331              
1332             =back
1333              
1334             =head1 LICENSE AND COPYRIGHT
1335              
1336             Copyright 2010-2012 Stefan Hornburg (Racke) .
1337              
1338             This program is free software; you can redistribute it and/or modify it
1339             under the terms of either: the GNU General Public License as published
1340             by the Free Software Foundation; or the Artistic License.
1341              
1342             See http://dev.perl.org/licenses/ for more information.
1343              
1344             =cut
1345              
1346             1;