The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Template::Flute::PDF;

use strict;
use warnings;

use Data::Dumper;

use File::Basename;
use File::Spec;
use Math::Trig;

use PDF::API2;
use PDF::API2::Util;

use Template::Flute::Style::CSS;

use Template::Flute::PDF::Import;
use Template::Flute::PDF::Box;

=head1 NAME

Template::Flute::PDF - PDF generator for HTML templates

=head1 VERSION

Version 0.0042

=cut

our $VERSION = '0.0042';

=head1 SYNOPSIS

  $flute = Template::Flute->new (specification_file => 'invoice.xml',
                                 template_file => 'invoice.html',
                                 values => \%values);
  $flute->process();

  $pdf = Template::Flute::PDF->new (template => $flute->template(),
                                    file => 'invoice.pdf');

  $pdf->process();

=head1 DESCRIPTION

Template::Flute::PDF is a PDF generator based on L<Template::Flute>
and L<PDF::API2>.

=head2 OUTPUT

To obtain the PDF as a string instead of writing it to a file,
please simply leave out the file parameter when creating the Template::Flute::PDF
object:

   $pdf = Template::Flute::PDF->new (template => $flute->template(),
                                     file => 'invoice.pdf');

   $pdf_as_string = $pdf->process();

=head2 UNITS

Template::Flute::PDF uses the pt unit internally.

In addition, the following units are supported and automatically
converted by this module:

=over 4

=item in

An inch converts to 72 pt.

=item cm

A centimeter converts to approximately 28.3 pt.

=item mm

A millimeter converts to approximately 2.8 pt.

=item px

A pixel converts to 1 pt.

=back

=head1 CONSTRUCTOR

=head2 new

Create a Template::Flute::PDF object with the following parameters:

=over 4

=item template

L<Template::Flute::HTML> object.

=item file

PDF output file.

=item page_size

Page size for the PDF (default: A4).

=item html_base

Base directory for HTML resources like images and stylesheets.

=item import

Import parameters for L<Template::Flute::PDF::Import>.

=back

=head3 Margin parameters

=over 4

=item margin_top

Top margin, defaults to 20.

=item margin_right

Right margin, defaults to 20.

=item margin_bottom

Bottom margin, defaults to 50.

=item margin_left

Left margin, defaults to 20.

=back

=cut

# defaults
use constant FONT_FAMILY => 'Helvetica';
use constant FONT_SIZE => '12';
use constant PAGE_SIZE => 'a4';
use constant MARGINS => (20, 20, 50, 20);

# font map for PDF core fonts (see PDF::API2::Resource::Font::CoreFont)
our %font_map = (Courier => {Bold => 'Courier-Bold',
			     BoldItalic => 'Courier-BoldOblique',
			     Italic => 'Courier-Oblique',
			     Roman => 'Courier',
		 },
		 Georgia => {Bold => 'Georgia,Bold',
			     BoldItalic => 'Georgia,BoldItalic',
			     Italic => 'Georgia,Italic',
			     Roman => 'Georgia',
		 },
		 Helvetica => {Bold => 'Helvetica-Bold',
			       BoldItalic => 'Helvetica-BoldOblique',
			       Italic => 'Helvetica-Oblique',
			       Roman => 'Helvetica',
		 },
		 Symbol => {},
		 Times => {Bold => 'Times-Bold',
			   BoldItalic => 'Times-BoldItalic',
			   Roman => 'Times',
			   Italic => 'Times-Italic'
		 },
		 Verdana => {Bold => 'Verdana,Bold',
			     BoldItalic => 'Verdana,BoldItalic',
			     Italic => 'Verdana,Italic',
			     Roman => 'Verdana',
		 },
		 Webdings => {},
		 Wingdings => {},
		 ZapfDingbats => {},
    );

sub new {
	my ($proto, @args) = @_;
	my ($class, $self);

	$class = ref($proto) || $proto;
	$self = {@args};
	bless ($self, $class);
	
	if ($self->{template}) {
		$self->{xml} = $self->{template}->root();
		$self->{css} = new Template::Flute::Style::CSS(template => $self->{template});
	}

	# create PDF::API2 object
	if ($self->{file}) {
		$self->{pdf} = new PDF::API2(-file => $self->{file});
	}
	else {
		$self->{pdf} = new PDF::API2();
	}

	# font cache
	$self->{_font_cache} = {};

	# page size
	if ($self->{page_size}) {
		$self->set_page_size(delete $self->{page_size});
	}
	else {
		$self->set_page_size(PAGE_SIZE);
	}

	# page orientation
	unless ($self->{orientation}) {
	    $self->{orientation} = '';
	}

	# margins
	my @sides = qw(top right bottom left);
	
	for (my $i = 0; $i < @sides; $i++) {
	    $self->{'margin_' . $sides[$i]} ||= (MARGINS)[$i];
	}
	
	bless ($self, $class);
}

=head1 METHODS

=head2 process

Processes HTML template and creates PDF file.

=cut

sub process {
	my $self = shift;
	my ($file, $font);

    if (@_) {
        $file = shift;
    }
    else {
        $file = $self->{file};
    }
    
	$self->{cur_page} = 1;

	$self->{border_left} = to_points($self->{margin_left}, 'pt');
	$self->{border_right} = $self->{page_width} - to_points($self->{margin_right}, 'pt');

	$self->{border_top} = $self->{page_height} - to_points($self->{margin_top}, 'pt');
	$self->{border_bottom} = to_points($self->{margin_bottom}, 'pt');

	$self->{vpos_next} = $self->{border_top};
	
	$self->{hpos} = $self->{border_left};

	if ($self->{verbose}) {
		print "Starting page at X $self->{hpos} Y $self->{y}.\n";
		print "Borders are T $self->{border_top} R $self->{border_right} B $self->{border_bottom} L $self->{border_left}.\n\n";
	}

	my %h = $self->{pdf}->info(
	    'Producer' => "Template::Flute::PDF $VERSION",
	);

	if ($self->{import}) {
		my ($obj, $ret, %import_parms);

		if (ref($self->{import})) {
			%import_parms = %{$self->{import}};
		}
		else {
			%import_parms = (file => $self->{import});
		}

		$import_parms{pdf} = $self->{pdf};
		
		$obj = new Template::Flute::PDF::Import;
		
		unless ($ret = $obj->import(%import_parms)) {
			die "Failed to import file $import_parms{file}.\n";
		}

#		if ($self->{verbose} || 1) {
#			print "Imported PDF $self->{import}: $ret->{pages} pages.\n\n";
#		}

		$self->{page} = $ret->{cur_page};
#		$pdf->saveas();
#		return;
	}

	# Open first page
	$self->{page} ||= $self->{pdf}->page($self->{cur_page});

	$self->{pdf}->preferences(
					  -fullscreen => 0,
					  -singlepage => 1,
					  -afterfullscreenoutlines => 1,
					  -firstpage => [ $self->{page} , -fit => 0],
					  -displaytitle => 1,
					  -fitwindow => 0,
					  -centerwindow => 1,
					  -printscalingnone => 1,
	);
	
	# retrieve default settings for font etc from CSS
	my $css_defaults = $self->{css}->properties(tag => 'body');

	# set font
	if ($css_defaults->{font}->{family}) {
		$self->{fontfamily} = $self->_font_select($css_defaults->{font}->{family});
	}
	else {
		$self->{fontfamily} = FONT_FAMILY;
	}
	
	if ($css_defaults->{font}->{size}) {
		$self->{fontsize} = to_points($css_defaults->{font}->{size});
	}
	else {
		$self->{fontsize} = FONT_SIZE;
	}

	if ($css_defaults->{font}->{weight}) {
		$self->{fontweight} = $css_defaults->{font}->{weight};
	}
	else {
		$self->{fontweight} = '';
	}

	$font = $self->font($self->{fontfamily}, $self->{fontweight});
	
	$self->{page}->text->font($font, $self->{fontsize});

	# move to starting point
	$self->{page}->text->translate($self->{border_left}, $self->{border_top});

	# page orientation
	if ($self->{orientation} eq 'landscape') {
	    $self->{page}->rotate(90);
	}

	# now walk HTML document and add appropriate parts
	my ($root_box, @root_parms);

	@root_parms = (pdf => $self,
				   elt => $self->{xml},
				   bounding => {vpos => $self->{border_top},
								hpos => $self->{border_left},
								max_w => $self->{border_right} - $self->{border_left},
								max_h => $self->{border_top} - $self->{border_bottom}});

	$root_box = new Template::Flute::PDF::Box(@root_parms);

	# calculate sizes
	$root_box->calculate();

	# align
	$root_box->align();
	
	# page partitioning
	$root_box->partition(1, 0);

	# render
	$root_box->render(vpos => $self->{border_top},
					  hpos => $self->{border_left});
	
#	$self->walk_template($self->{xml});

	if ($file) {
	    $self->{pdf}->saveas($file);
	    return 1;
	}

	return $self->{pdf}->stringify;
}

sub template {
	my $self = shift;
	
	return $self->{template};
}

=head2 set_page_size

Sets the page size for the PDF.

=cut

sub set_page_size {
	my ($self, @args) = @_;
	my ($ret, @ps);

	if (ref($args[0]) eq 'ARRAY') {
		@args = @{$args[0]};
	}
	
	if (@args > 1) {
		# passing page size as numbers
		@ps = map {to_points($_, 'pt')} @args;
		($self->{page_width}, $self->{page_height}) = @ps;
	}
	else {
		# resolve page size
		unless ($self->{_paper_sizes}) {
			$self->{_paper_sizes} = {getPaperSizes()};
		}

		if (exists $self->{_paper_sizes}->{lc($args[0])}) {
			($self->{page_width}, $self->{page_height})
				= @{$self->{_paper_sizes}->{lc($args[0])}};
		}
		else {
			die "Invalid paper size $args[0]";
		}
			
		$ps[0] = $args[0];
	}
	
	$self->{_page_size} = \@ps;

	$self->{pdf}->mediabox(@ps);
}

=head2 select_page PAGE_NUM
	
Selects page with the given PAGE_NUM. Creates new page if necessary.

=cut

sub select_page {
	my ($self, $page_num) = @_;
	my ($diff, $cur_page);
	
	if ($page_num > $self->{pdf}->pages()) {
		$diff = $page_num - $self->{pdf}->pages();

		for (my $i = 0; $i < $diff; $i++) {
			$cur_page = $self->{pdf}->page();
		}
	}
	else {
		$cur_page = $self->{pdf}->openpage($page_num);
	}

	$self->{page} = $cur_page;
}

=head2 content_height

Returns the height of the content part of the page.

=cut
	
sub content_height {
	my ($self) = @_;
	my ($height);

	return $self->{page_height};
}

=head2 content_width

Returns the width of the content part of the page.

=cut
	
sub content_width {
	my ($self) = @_;
	my ($width);
	
	$width = $self->{page_width} - to_points($self->{margin_left}, 'pt') 
	    - to_points($self->{margin_right}, 'pt');

	return $width;
}

=head2 bounding

Returns the bounding box for the PDF as a hash reference
with the following key/value pairs:

=over 4

=item vpos

Top vertical position.

=item pos

Left horizonal position.

=item max_w

Maximum width.

=item max_h

Maximum height.

=back

The bounding box defines the available space without
the borders.

=cut

sub bounding {
    my $self = shift;
    
    return {vpos => $self->{border_top},
            hpos => $self->{border_left},
            max_w => $self->{border_right} - $self->{border_left},
            max_h => $self->{border_top} - $self->{border_bottom},
    };
}

=head2 font NAME [weight] [style]

Returns PDF::API2 font object for font NAME, WEIGHT and STYLE are optional.

=cut
	
sub font {
	my ($self, $name, $weight, $style) = @_;
	my ($key, $obj);

    if ($weight eq 'normal') {
        # default font weight
        $weight = '';
    }

	# determine font name from supplied name and optional weight
	if ($weight) {
	    if ($style) {
            $key = "$name-$weight$style";
        }
        else {
            $key = "$name-$weight";
        }
	}
	elsif ($style) {
	    if (exists $font_map{$name}->{$style}) {
            $key = $font_map{$name}->{$style};
	    }
	    else {
            $key = "$name-$style";
	    }
	}
	else {
		$key = $name;
	}
		
	if (exists $self->{_font_cache}->{$key}) {
		# return font object from cache
		return $self->{_font_cache}->{$key};
	}

	# create new font object
	$obj = $self->{pdf}->corefont($key, -encoding => 'latin1');

	$self->{_font_cache}->{$key} = $obj;
	
	return $obj;
}

=head2 text_filter TEXT

Adjusts whitespace in TEXT for output in PDF.

=cut
	
sub text_filter {
	my ($self, $text, $transform) = @_;
	my ($orig);
	
	# fall back to empty string
	unless (defined $text) {
		return '';
	}

	$orig = $text;
	
	# replace newlines with blanks
	$text =~ s/\n/ /gs;

	# collapse blanks
	$text =~ s/\s+/ /g;

	if (length $orig && ! length $text) {
		# reduce not further than a single whitespace
		return ' ';
	}

	# transform text analogous to CSS specification
	if (defined $transform) {
		if ($transform eq 'uppercase') {
			$text = uc($text);
		}
		elsif ($transform eq 'lowercase') {
			$text = lc($text);
		}
		elsif ($transform eq 'capitalize') {
			$text =~ s/\b(\w)/\u$1/g;
		}
		elsif ($transform ne 'none') {
			die "Unknown transformation $transform\n";
		}
	}
	
	return $text;
}

=head2 setup_text_props ELT SELECTOR [INHERIT]

Determines text properties for HTML template element ELT, CSS selector SELECTOR
and INHERIT flag.

=cut

sub setup_text_props {
	my ($self, $elt, $selector, $inherit) = @_;
	my ($props, %borders, %padding, %margins, %offset, $fontsize, $fontfamily,
		$fontweight, $fontstyle, $txeng);

	my $class = $elt->att('class') || '';
	my $id = $elt->att('id') || '';
	my $gi = $elt->gi();

	$selector ||= '';
	
	# get properties from CSS
	$props = $self->{css}->properties(id => $id,
									  class => $elt->att('class'),
									  tag => $elt->gi(),
									  selector => $selector,
									  inherit => $inherit,
									 );
			
	$txeng = $self->{page}->text;

	if ($props->{font}->{size}) {
	    if ($props->{font}->{size} =~ s/^(\d+)(pt)?$/$1/) {
		$fontsize =  $props->{font}->{size};
	    }
	    else {
		$fontsize = to_points($props->{font}->{size});
	    }
	}
	else {
		$fontsize = $self->{fontsize};
	}

	if ($props->{font}->{family}) {
		$fontfamily = $self->_font_select($props->{font}->{family});
	}
	else {
		$fontfamily = $self->{fontfamily};
	}

	if ($props->{font}->{weight}) {
		$fontweight = $props->{font}->{weight};
	}
	else {
		$fontweight = $self->{fontweight};
	}

    if ($props->{font}->{style}) {
        $fontstyle = $props->{font}->{style};
    }
    elsif ($gi eq 'i') {
        $fontstyle = 'Italic';
    }

	$self->{font} = $self->font($fontfamily, $fontweight,
	    $fontstyle);
	
	$txeng->font($self->{font}, $fontsize);

	if ($gi eq 'hr') {
		unless (keys %{$props->{margin}}) {
			# default margins for horizontal rule
			my $margin;

			$margin = 0.5 * $fontsize;

			$props->{margin} = {top => $margin,
								bottom => $margin};
		}
	}
				
	# offsets from border, padding etc.
	for my $s (qw/top right bottom left/) {
		$borders{$s} = to_points($props->{border}->{$s}->{width});
		$margins{$s} = to_points($props->{margin}->{$s});
		$padding{$s} = to_points($props->{padding}->{$s});

		$offset{$s} += $margins{$s} + $borders{$s} + $padding{$s};
	}

	# height and width
	$props->{width} = to_points($props->{width});
	$props->{height} = to_points($props->{height});
	
	return {font => $self->{font}, size => $fontsize, offset => \%offset,
			borders => \%borders, margins => \%margins, padding => \%padding, props => $props,
			# for debugging
			class => $class, selector => $selector
		   };
}

=head2 calculate ELT [PARAMETERS]

Calculates width and height for HTML template element ELT.

=cut	
	
sub calculate {
	my ($self, $elt, %parms) = @_;
	my ($text, $chunk_width, $text_width, $max_width, $avail_width, $height, $specs, $txeng,
		$overflow_x, $overflow_y, $clear_before, $clear_after, @chunks, $buf, $lines);
	
	$txeng = $self->{page}->text();
	$max_width = 0;
	$height = 0;
	$overflow_x = $overflow_y = 0;
	$clear_before = $clear_after = 0;
	$lines = 1;

	if ($parms{specs}) {
		$specs = $parms{specs};
	}
	else {
		$specs = $self->setup_text_props($elt);
	}

	if ($specs->{props}->{width}) {
		$avail_width = $specs->{props}->{width};
	}
	else {
		$avail_width = $self->content_width();
	}

	if (ref($parms{text}) eq 'ARRAY') {
		$buf = '';
		$text_width = 0;
		
		for my $text (@{$parms{text}}) {
			if ($text eq "\n") {
				# force newline
				push (@chunks, $buf . $text);
				$buf = '';
				$text_width = 0;
				$lines++;
			}
			elsif ($text =~ /\S/) {
				$chunk_width = $txeng->advancewidth($text, font => $specs->{font},
												   fontsize => $specs->{size});
			}
			else {
				# whitespace
				$chunk_width = $txeng->advancewidth("\x20", font => $specs->{font},
												   fontsize => $specs->{size});
			}

			if ($avail_width
				&& $text_width + $chunk_width > $avail_width) {
#				print "Line break by long text: $buf + $text\n";

				push (@chunks, $buf);
				$buf = $text;
				$text_width = 0;
				$lines++;
			}
			else {
				$buf .= $text;
			}

			$text_width += $chunk_width;
			
			if ($text_width > $max_width) {
				$max_width = $text_width;
			}
		}

		if (length($buf)) {
			push (@chunks, $buf);
		}
	}

	if ($parms{clear} || $specs->{props}->{clear} eq 'both') {
		$clear_before = $clear_after = 1;
	}
	elsif ($specs->{props}->{clear} eq 'left') {
		$clear_before = 1;		
	}
	elsif ($specs->{props}->{clear} eq 'right') {
		$clear_after = 1;
	}
	
#	print "Before offset: MW $max_width H $height S $specs->{size}, ", Dumper($specs->{offset}) . "\n";
	
#	print "PW $avail_width, PH $specs->{props}->{height}, MW $max_width H $height\n";

	# line height
	if (exists $specs->{props}->{line_height}) {
		$height = $lines * to_points($specs->{props}->{line_height});
	}
	else {
		$height = $lines * $specs->{size};
	}

	# adjust to fixed width
	if ($avail_width) {
		if ($avail_width < $max_width) {
			$overflow_x = $max_width - $avail_width;
			$max_width = $avail_width;
		}
	}

	# adjust to fixed height
	if ($specs->{props}->{height}) {
		if ($specs->{props}->{height} < $height) {
			$overflow_y = $height - $specs->{props}->{height};
			$height = $specs->{props}->{height};
		}
		else {
			$height = $specs->{props}->{height};
		}
	}
	
	return {width => $max_width, height => $height, size => $specs->{size},
			clear => {before => $clear_before, after => $clear_after},
			overflow => {x => $overflow_x, y => $overflow_y},
			text_width => $text_width,
			chunks => \@chunks,
		   };
}

=head2 check_out_of_bounds POS DIM

Check whether we are out of bounds with position POS and dimensions DIM.

=cut

sub check_out_of_bounds {
	my ($self, $pos, $dim) = @_;

	if ($pos->{hpos} == $self->{border_right}) {
		# we are on the left border, so even if the box is out
		# of bounds, we have no better idea :-)
		return;
	}
	
#	print "COB pos: " . Dumper($pos) . "COB dim: " . Dumper($dim);
#	print "NEXT: $self->{vpos_next}.\n";

	if ($pos->{hpos} + $dim->{width} > $self->{border_right}) {
		return {hpos => $self->{border_left}, vpos => $self->{vpos_next}};
	}
	
	return;
}

=head2 textbox ELT TEXT PROPS BOX ATTRIBUTES

Adds textbox for HTML template element ELT to the PDF.

=cut

sub textbox {
	my ($self, $elt, $boxtext, $boxprops, $box, %atts) = @_;
	my ($width_last, $y_top, $y_last, $left_over, $text_width, $text_height, $box_height);
	my (@tb_parms, %parms, $txeng, %offset, %borders, %padding, $props,
		$paragraph, $specs, %text_options, $decoration);

	if ($boxprops) {
		$specs = $boxprops;
	}
	else {
		# get specifications from CSS
		$specs = $self->setup_text_props($elt);
	}

#	unless ($specs->{borders}) {
#		delete $specs->{font};
#		print "Elt: ", $elt->sprint(), "\n";
#		print "Specs for textbox: " . Dumper($specs) . "\n";
#	}
	
	$props = $specs->{props};
	%borders = %{$specs->{borders}};
	%offset = %{$specs->{offset}};
	%padding = %{$specs->{padding}};

	if ($box) {
#		print "Set from box: " . Dumper($box) . " for size $specs->{size}\n";
		$self->{hpos} = $box->{hpos};
		$self->{y} = $box->{vpos};
	}

	$txeng = $self->{page}->text;
	$txeng->font($specs->{font}, $specs->{size});
	
#print "Starting pos: X $self->{hpos} Y $self->{y}\n";
	$txeng->translate($self->{hpos}, $self->{y});
	
	# determine resulting horizontal position
	$text_width = $txeng->advancewidth($boxtext);
#print "Hpos after: " . $text_width . "\n";

	# now draw the background for text box
	if ($props->{background}->{color}) {
#		print "Background for text box: $props->{background}->{color}\n";
		$self->rect($self->{hpos}, $self->{y},
					$self->{hpos} + $text_width, $self->{y} - $padding{top} - $specs->{size} - $padding{bottom},
					$props->{background}->{color});
	}

	# colors
	if ($props->{color}) {
		$txeng->fillcolor($props->{color});		
	}
	
	%parms = (x => $self->{hpos},
			  y => $self->{y} - $specs->{size},
			  w => $self->content_width(),
			  h => to_points(100),
			  lead => $specs->{size},
#			  align => $props->{text}->{align} || 'left',
			  align => 'left',
			 );
		
	@tb_parms = ($txeng,  $boxtext, %parms);

#print "Add textbox (class " . ($elt->att('class') || "''") . ") with content '$boxtext' at $parms{y} x $parms{x}, border $offset{top}\n";

	if ($decoration = $props->{text}->{decoration}) {
	    if ($decoration eq 'underline') {
                $text_options{'-underline'} = 1;
            }
	}

	if (length($boxtext) && $boxtext =~ /\S/) {
	    if ($props->{line_height}) {
		# adjust text position accordingly
		$parms{y} -= (to_points($props->{line_height}) - $specs->{size}) / 2;
	    }
	    # try different approach
	    if (exists $props->{rotate}) {
		$txeng->translate($parms{x}, $parms{y});
		$txeng->transform_rel(-rotate => 360 - $props->{rotate});
	    }
	    else {
		$txeng->translate($parms{x}, $parms{y});
	    }

	    $txeng->text($boxtext, %text_options);
	}
	else {
		$y_last = $parms{y};
	}

	$txeng->fill();
}

=head2 hline SPECS HPOS VPOS LENGTH WIDTH

Add horizontal line to PDF.

=cut
	
sub hline {
	my ($self, $specs, $hpos, $vpos, $width, $height) = @_;
	my ($gfx);

	$gfx = $self->{page}->gfx;

	$self->begin_transform($gfx, $hpos, $vpos - $height / 2, $width, $height || 1, $specs->{props});

	# set line color
	$gfx->strokecolor($specs->{props}->{color});

	# set line width
	$gfx->linewidth($height || 1);

	$gfx->line($width, 0);
	
	# draw line
	$gfx->stroke();

	$self->end_transform($gfx, $hpos, $vpos - $height / 2, $width, $height || 1, $specs->{props});

	return;
}

=head2 borders X_LEFT Y_TOP WIDTH HEIGHT

Adds borders to the PDF.

=cut

sub borders {
	my ($self, $x_left, $y_top, $width, $height, $specs) = @_;
	my ($gfx);
	
	$gfx = $self->{page}->gfx;
	
	if ($specs->{borders}->{top}) {
		$gfx->strokecolor($specs->{props}->{border}->{top}->{color});
		$gfx->linewidth($specs->{borders}->{top});
		$gfx->move($x_left, $y_top - $specs->{borders}->{top} * 0.5);
		$gfx->line($x_left + $width, $y_top - $specs->{borders}->{top} * 0.5);
		$gfx->stroke();
	}

	if ($specs->{borders}->{left}) {
		$gfx->strokecolor($specs->{props}->{border}->{left}->{color});
		$gfx->linewidth($specs->{borders}->{left});
		$gfx->move($x_left + 0.5 * $specs->{borders}->{left}, $y_top);
		$gfx->line($x_left + 0.5 * $specs->{borders}->{left} , $y_top - $height); #- $specs->{borders}->{top});
	    
		$gfx->stroke();
	}
	
	if ($specs->{borders}->{bottom}) {
		$gfx->strokecolor($specs->{props}->{border}->{bottom}->{color});
		$gfx->linewidth($specs->{borders}->{bottom});
		$gfx->move($x_left, $y_top - $height + 0.5 * $specs->{borders}->{bottom} );
		$gfx->line($x_left + $width, $y_top - $height +  0.5 * $specs->{borders}->{bottom} );
		$gfx->stroke();
	}

	if ($specs->{borders}->{right}) {
		$gfx->strokecolor($specs->{props}->{border}->{right}->{color});
		$gfx->linewidth($specs->{borders}->{right});
		$gfx->move($x_left + $width - 0.5 * $specs->{borders}->{right}, $y_top);
		$gfx->line($x_left + $width  - 0.5 * $specs->{borders}->{right}, $y_top - $height);
		$gfx->stroke();
	}
}

=head2 rect X_LEFT Y_TOP X_RIGHT Y_BOTTOM COLOR

Adds rectangle to the PDF.

=cut

# primitives
sub rect {
	my ($self, $x_left, $y_top, $x_right, $y_bottom, $color) = @_;
	my ($gfx);

	$gfx = $self->{page}->gfx;

	if ($color) {
		$gfx->fillcolor($color);
	}

	$gfx->rectxy($x_left, $y_top, $x_right, $y_bottom);

	if ($color) {
		$gfx->fill();
	}
}

=head2 locate_image

Determines location of an image file from the C<src> HTML
attribute.

    $imgfile = $pdf->locate_image('images/cart.png');

The location is based on the current directory, or on
the C<html_base> constructor parameter if the C<src> HTML
attribute contains a single file name only.
 
=cut

sub locate_image {
    my ($self, $src) = @_;
    my ($img_dir, $template_dir, $img_file);

    if ($self->{swap_images}) {
	for my $href (@{$self->{swap_images}}) {
	    if ($href->{src} eq $src) {
		$src = $href->{path};
	    }
	}
    }

    $img_dir = dirname($src);
    $img_file = $src;

    if ($img_dir eq '.') {
	# check whether HTML template is located in another directory
	$template_dir = dirname($self->template()->file());

	if ($template_dir ne '.') {
	    if ($self->{html_base}) {
		$img_file = File::Spec->catfile($self->{html_base},
					   basename($src));
	    }
	    else {
		$img_file = File::Spec->catfile($template_dir,
						basename($src));
	    }
	}
    }

    return $img_file;
}

=head2 image OBJECT HPOS VPOS WIDTH HEIGHT

Add image OBJECT to the PDF.

=cut

sub image {
	my ($self, $object, $x_left, $y_top, $width, $height, $specs) = @_;
	my ($gfx, $method, $image_object);

	$gfx = $self->{page}->gfx;
	
	$method = 'image_' . $object->{type};

	$image_object = $self->{pdf}->$method($object->{file});

	$gfx->image($image_object, $x_left, $y_top, $width, $height);
}

=head2 begin_transform

Starts transformation of current content object.

=cut

sub begin_transform {
    my ($self, $gfx, $hpos, $vpos, $width, $height, $props) = @_;

    $gfx->move(0,0);
 
    if (exists $props->{translate}->{x}) {
	$hpos += to_points($props->{translate}->{x});
    }

    if (exists $props->{translate}->{y}) {
	$vpos -= to_points($props->{translate}->{y});
    }

    $gfx->translate($hpos, $vpos);
    
    if ($props->{rotate}) {
	$gfx->rotate(- $props->{rotate});
    }
}

=head2 end_transform

Ends transformation of current content object.

=cut

sub end_transform {
    my ($self, $gfx, $hpos, $vpos, $width, $height, $props) = @_;
    
    if ($props->{rotate}) {
	$gfx->rotate($props->{rotate});
    }

    if (exists $props->{translate}->{x}) {
	$hpos += to_points($props->{translate}->{x});
    }

    if (exists $props->{translate}->{y}) {
	$vpos -= to_points($props->{translate}->{y});
    }

    $gfx->translate(-$hpos, -$vpos);
}

=head1 FUNCTIONS

=head2 to_points [DEFAULT_UNIT]
	
Converts widths to points, default unit is mm.

=cut
	
sub to_points {
	my ($width, $default_unit) = @_;
	my ($unit, $points, $negative);

	return 0 unless defined $width;

	if ($width =~ s/^(-?)(\d+(\.\d+)?)\s?(in|px|pt|cm|mm)?$/$2/) {
	    $negative = $1;
	    $unit = $4 || $default_unit || 'mm';
	}
	else {
		warn "Invalid width $width\n";
		return;
	}

	if ($unit eq 'in') {
		# 72 points per inch
		$points = 72 * $width;
	}
	elsif ($unit eq 'cm') {
		$points = 72 * $width / 2.54;
	}
	elsif ($unit eq 'mm') {
		$points = 72 * $width / 25.4;
	}
	elsif ($unit eq 'pt') {
		$points = $width;
	}
	elsif ($unit eq 'px') {
		$points = $width;
	}

	if ($negative) {
	    return - $points;
	}

	return $points;
}

# auxiliary methods

# select font from list provided by CSS (currently just the first)

sub _font_select {
	my ($self, $font_string) = @_;
	my (@fonts);

	@fonts = split(/,/, $font_string);

	return $fonts[0];
}

=head1 SUPPORTED HTML/CSS SYNTAX

This is an incomplete list of supported HTML/CSS syntax.

=head2 HTML tags and attributes

<i>

=head3 style

The HTML attribute "style" is not supported.

=head2 CSS properties

=head3 display

The CSS property "display" is not supported.

=head3 font-weight

The values "normal" and "bold" are supported.

=head3 min-height

The CSS property "min-height" is supported.

=head3 min-width

The CSS property "min-width" is supported.
    
=head3 text-transformation

The CSS property "text-transformation" is supported with
the exception of the value "inherit".

=head1 AUTHOR

Stefan Hornburg (Racke), <racke@linuxia.de>

=head1 BUGS

Certainly a lot, as converting from HTML to PDF is quite complicated and challenging.

Please report any unknown bugs or feature requests to C<bug-template-flute-pdf at rt.cpan.org>,
or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Template-Flute-PDF>.

=head2 KNOWN BUGS

=over 4

=item Background color

Using background color hides text.

=item Vertical align

We currently support only aligning to top or bottom of the available space.
This is in contradiction to HTML, where the default vertical align 
is baseline (of the text).

=back

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Template::Flute::PDF

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Template-Flute-PDF>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/Template-Flute-PDF>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/Template-Flute-PDF>

=item * Search CPAN

L<http://search.cpan.org/dist/Template-Flute-PDF/>

=back

=head1 LICENSE AND COPYRIGHT

Copyright 2010-2012 Stefan Hornburg (Racke) <racke@linuxia.de>.

This program is free software; you can redistribute it and/or modify it
under the terms of either: the GNU General Public License as published
by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.

=cut

1;