package Template::Flute::PDF::Box;
use strict;
use warnings;
use Data::Dumper;
use Template::Flute::PDF::Image;
=head1 NAME
Template::Flute::PDF::Box - PDF boxes class
=head1 CONSTRUCTOR
=head2 new
Creates a Template::Flute:PDF::Box object with the following parameters:
=over 4
=item pdf
Template::Flute::PDF object (required).
=item elt
Corresponding HTML template element for the box (required).
=back
=cut
sub new {
my ($proto, @args) = @_;
my ($class, $self);
my ($elt_class, $elt_id, @p);
$class = ref($proto) || $proto;
$self = {@args};
unless ($self->{pdf}) {
die "Missing PDF object\n";
}
unless ($self->{elt}) {
die "Missing Twig element for PDF box\n";
}
# Record corresponding GI for box
$self->{gi} = $self->{elt}->gi();
# Record corresponding CLASS for box
$elt_class = $self->{elt}->att('class');
if (defined $elt_class) {
$self->{class} = $elt_class;
}
else {
$self->{class} = '';
}
# Record corresponding ID for box
$elt_id = $self->{elt}->att('id');
if (defined $elt_id) {
$self->{id} = $elt_id;
}
else {
$self->{id} = '';
}
# Page for element
$self->{page} ||= 1;
# Mapping child elements to box objects
$self->{eltmap} = {};
# Stack of child elements
$self->{eltstack} = [];
# Positions of child elements
$self->{eltpos} = [];
# Stripes with child elements
$self->{stripes} = [];
bless ($self, $class);
# Create selector map
@p = (id => $self->{id}, class => $self->{class}, parent => $self->{selector_map});
$self->{selector_map} = $self->{pdf}->{css}->descendant_properties(@p);
unless ($self->{specs}) {
$self->setup_specs();
}
# Determine our window from bounding box
%{$self->{window}} = %{$self->{bounding}};
if ($self->{specs}->{props}->{width}) {
# print "Reducing WINDOW width to GI $self->{gi} CLASS $self->{class} to $self->{specs}->{props}->{width}\n";
$self->{window}->{max_w} = $self->{specs}->{props}->{width};
}
if ($self->{specs}->{props}->{height}) {
# print "Reducing WINDOW height to GI $self->{gi} CLASS $self->{class} to $self->{specs}->{props}->{height}\n";
$self->{window}->{max_h} = $self->{specs}->{props}->{height};
}
return $self;
}
=head1 FUNCTIONS
=head2 calculate
Calculates dimensions of the box.
=head3 Images
The width and height of an image is determined according to
the following priority list.
=over 4
=item 1.
From width/height attribute of the
HTML tag.
=item 2.
From corresponding CSS width/height property.
=item 3.
From width/height of the image.
=back
=cut
sub calculate {
my ($self) = @_;
my ($gi, $class, $text, @parms, $childbox, $dim);
if ($self->{elt}->is_text()) {
# simple text box
$text = $self->{elt}->text();
# filter text and break into chunks to remove unnecessary whitespace
$text = $self->{pdf}->text_filter($text, $self->property('text', 'transform'));
# break text first
my @frags;
while ($text =~ s/^(.+?)\s+//) {
push (@frags, $1, ' ');
}
if (length($text)) {
push (@frags, $text);
}
$self->{box} = $self->{pdf}->calculate($self->{elt}, text => \@frags,
specs => $self->{specs});
# print "Check width $self->{box}->{width}, height $self->{box}->{height}, $self->{box}->{overflow}->{x} vs $self->{window}->{max_w} for $text\n";
if ($self->{box}->{overflow}->{x}) {
warn "Uh oh, out of bounds for $text: $self->{box}->{overflow}->{x}\n";
}
return $self->{box};
}
if ($self->{gi} eq 'img') {
my (@info, $file, %size);
$file = $self->{elt}->att('src');
$self->{object} = new Template::Flute::PDF::Image(file => $file,
pdf => $self->{pdf});
for my $extent (qw/width height/) {
# size from HTML
if ($size{$extent}= $self->{elt}->att($extent)) {
next;
}
# size from CSS
if ($size{$extent} = $self->property($extent)) {
next;
}
# size from image
$size{$extent} = $self->{object}->$extent();
}
$self->{box} = {width => $size{width},
height => $size{height},
clear => {after => 0, before => 0},
size => $self->{specs}->{size}};
return;
}
for my $child ($self->{elt}->children()) {
# discard elements we won't use anyway
next if $self->{gi} eq 'style';
next if $self->{gi} eq 'head';
unless (exists $self->{eltmap}->{$child}) {
@parms = (elt => $child, pdf => $self->{pdf},
parent => $self);
if ($child->is_text()) {
# inheriting specifications of parent
push (@parms, specs => $self->{specs});
}
else {
push (@parms, selector_map => $self->{selector_map});
}
push (@parms, bounding => {%{$self->{window}}});
$childbox = new Template::Flute::PDF::Box(@parms);
$self->{eltmap}->{$child} = $childbox;
push (@{$self->{eltstack}}, $childbox);
}
$dim = $self->{eltmap}->{$child}->calculate();
}
# processed all childs, now determine my size itself
my ($max_width, $max_height, $vpos, $hpos, $max_stripe_height, $child) = (0,0,0,0);
my ($hpos_next, $vpos_next, @stripes, $stripe_pos, $stripe_base, $clear_after);
$stripe_base = 0;
$clear_after = 0;
$stripe_pos = 0;
for (my $i = 0; $i < @{$self->{eltstack}}; $i++) {
$child = $self->{eltstack}->[$i];
$child->{stackpos} = $i;
if ($hpos > 0 && ! $child->{box}->{clear}->{before}
&& ! $clear_after) {
# check if item fits horizontally
$hpos_next = $hpos + $child->{box}->{width};
if ($self->{specs}->{props}->{width}
&& $self->{specs}->{props}->{width} < $hpos_next) {
# doesn't fit in fixed width of this box
# print "NO HORIZ FIT for GI $child->{gi} CLASS $child->{class} ID $child->{id}: too wide for H $hpos_next\n";
$hpos = 0;
$hpos_next = 0;
}
if ($hpos_next > $self->{bounding}->{max_w}) {
# doesn't fit in bounding box
# print "NO HORIZ FIT for GI $child->{gi} CLASS $child->{class} ID $child->{id}: H $hpos HN $hpos_next MAX_W $self->{bounding}->{max_w}\n";
$hpos = 0;
$hpos_next = 0;
}
}
else {
$hpos = 0;
$hpos_next = 0;
# print "NO HORIZ FIT for GI $child->{gi} CLASS $child->{class} ID $child->{id}: CLR AFTER $clear_after\n";
}
# keep vertical position
$vpos_next = $vpos;
if ($hpos_next > 0) {
# print "HORIZ FIT for GI $child->{gi} CLASS $child->{class} ID $child->{id}\n";
if ($child->property('float') eq 'right'
&& $self->property('float') ne 'right') {
# push it to the right border
if ($self->property('width')) {
$max_width = $self->property('width');
}
else {
$max_width = $self->{bounding}->{max_w};
}
$hpos = $max_width - $child->{box}->{width};
$hpos_next = $max_width;
}
else {
# add to current width
$max_width += $child->{box}->{width};
}
# check whether we need to extend the height
my $height_extend = 0;
if ($child->{box}->{height} > $max_stripe_height) {
$height_extend = $child->{box}->{height} - $max_stripe_height;
}
$max_stripe_height += $height_extend;
$max_height += $height_extend;
}
else {
# starting new stripe now
$stripe_pos++;
$max_stripe_height = 0;
# stripe base moves to max_height
$stripe_base = $max_height;
if ($child->{box}->{width} > $max_width) {
$max_width = $child->{box}->{width};
}
# add to current height
$max_height += $child->{box}->{height};
if ($stripe_base) {
$vpos_next = $stripe_base;
}
$vpos = $stripe_base;
# stripe height is simply height of this child
$max_stripe_height = $child->{box}->{height};
if ($child->property('float') eq 'right'
&& $self->property('float') ne 'right') {
# push it to the right border
if ($self->property('width')) {
$max_width = $self->property('width');
}
else {
$max_width = $self->{bounding}->{max_w};
}
$hpos = $max_width - $child->{box}->{width};
$hpos_next = $max_width;
}
# print "NEW HPOS from GI $child->{gi} CLASS $child->{class}: $child->{box}->{width}, VPOS $vpos\n";
$hpos_next = $child->{box}->{width};
}
$self->{eltpos}->[$i] = {hpos => $hpos, vpos => -$vpos};
# if ($child->{elt}->is_text()) {
# print "POS (relative) for TEXT '" . $child->{elt}->text() . "': " . Dumper($self->{eltpos}->[$i]);
# }
# else {
# print "POS (relative) for GI $child->{gi} CLASS $child->{class}: " . Dumper($self->{eltpos}->[$i]);
# }
# record child within its stripe
push (@{$self->{stripes}->[$stripe_pos]}, $child);
# advance to new relative position
$hpos = $hpos_next;
$vpos = $vpos_next;
$clear_after = $child->{box}->{clear}->{after};
}
# add offsets
$max_width += $self->{specs}->{offset}->{left} + $self->{specs}->{offset}->{right};
$max_height += $self->{specs}->{offset}->{top} + $self->{specs}->{offset}->{bottom};
# apply fixed dimensions
if ($self->{specs}->{props}->{width} > $max_width) {
$max_width = $self->{specs}->{props}->{width};
}
if ($self->{specs}->{props}->{height} > $max_height) {
$max_height = $self->{specs}->{props}->{height};
}
# set up clear properties
my $clear = {after => 0, before => 0};
if ($self->{gi} eq 'hr') {
$clear->{before} = $clear->{after} = 1;
$max_width ||= $self->{bounding}->{max_w};
}
elsif ($self->{gi} eq 'br') {
$clear->{before} = 1;
}
elsif ($self->{gi} =~ /^h\d$/
|| $self->{gi} eq 'p'
|| ($self->{gi} eq 'li' && $self->property('list_style') ne 'none')) {
$clear->{before} = $clear->{after} = 1;
}
$self->{box} = {width => $max_width,
height => $max_height,
clear => $clear,
size => $self->{specs}->{size}};
# print "DIM for GI $self->{gi}, CLASS $self->{class}, ID $self->{id}: " . Dumper($self->{box});
return $self->{box};
}
=head2 align OFFSET
Aligns boxes (center, left, right).
=cut
sub align {
my ($self, $offset) = @_;
my ($avail_width, $avail_width_text, $textprops, $child, $box_pos);
$offset ||= 0;
for (my $i = 0; $i < @{$self->{stripes}}; $i++) {
for my $child (@{$self->{stripes}->[$i]}) {
# skip over text elements (align only applies to grand children)
next if $child->{elt}->is_text();
if ($textprops = $child->property('text')) {
if ($child->property('width')) {
$avail_width = $child->property('width');
}
elsif (@{$self->{stripes}->[$i]} == 1) {
# single box in a stripe can take over all the space
# in the bounding box
$avail_width = $child->{bounding}->{max_w} - $offset - $self->{specs}->{offset}->{left} - $self->{specs}->{offset}->{right};
# $avail_width = $self->{box}->{width};
}
else {
$avail_width = $child->{box}->{width};
}
for (my $cpos = 0; $cpos < @{$child->{eltstack}}; $cpos++) {
next unless $child->{eltstack}->[$cpos]->{elt}->is_text();
$avail_width_text = $avail_width - $child->{eltstack}->[$cpos]->{box}->{text_width};
if ($avail_width_text > 0 && exists $textprops->{align}) {
if ($textprops->{align} eq 'right') {
$child->{eltstack}->[$cpos]->{hoff} += $avail_width_text;
}
elsif ($textprops->{align} eq 'center') {
$child->{eltstack}->[$cpos]->{hoff} += $avail_width_text / 2;
}
}
}
}
}
}
for (my $i = 0; $i < @{$self->{eltstack}}; $i++) {
$child = $self->{eltstack}->[$i];
$child->align($offset + $self->{eltpos}->[$i]->{hpos});
}
}
=head2 partition PAGE_NUM HEIGHT_BASE
Partitions boxes through pages.
=cut
sub partition {
my ($self, $page_num, $height_base) = @_;
my (@children, $children_height, $vpos_diff, $page_num_max);
$children_height = 0;
$page_num_max = $page_num;
if ($page_num > $self->{page}) {
$self->{page} = $page_num;
}
# print "PART $self->{gi} $self->{class}, PAGE $page_num, BASE $height_base BOX: " . Dumper($self->{box});
if ($height_base + $self->{box}->{height} > $self->{pdf}->content_height()) {
# print "SPLIT required due to H " . $self->{pdf}->content_height() . "\n";
@children = @{$self->{eltstack}};
if (@children > 1) {
# partition children
# print "MULTIPLE\n";
for (my $i = 0; $i < @children; $i++) {
my $c_info = "GI " . $children[$i]->{gi} . ", CLASS " . $children[$i]->{class};
if ($height_base + $children_height + $children[$i]->{box}->{height} > $self->{pdf}->content_height()) {
# print "CALL CHILD FROM BASE $height_base WITH CH $children_height, $c_info\n";
$page_num_max = $children[$i]->partition($page_num_max, $height_base + $children_height);
# adjust positions of children
# print "PAGE NUM GI $self->{gi} CLASS $self->{class}: FROM $page_num TO $page_num_max (CH: $children_height, HB: $height_base)\n";
# print "OLD ELT POS $c_info: " . Dumper($self->{eltpos}->[$i]) . "\n";
$vpos_diff = - $self->{eltpos}->[$i]->{vpos};
unless ($children[$i]->{box}->{height} > $self->{pdf}->content_height()) {
$self->{eltpos}->[$i]->{vpos} = 0;
$self->{eltpos}->[$i]->{page} = $page_num_max;
$children[$i]->adjust_page($page_num_max);
}
# print "NEW ELT POS: " . Dumper($self->{eltpos}->[$i]) . "\n";
# if ($page_num_max == $page_num) {
# # advance page for following element
# $page_num++;
# }
# reset heights
$height_base = 0;
$children_height = $children[$i]->{box}->{height};
next;
}
elsif ($vpos_diff) {
$self->{eltpos}->[$i]->{vpos} += $vpos_diff;
$self->{eltpos}->[$i]->{page} = $page_num_max;
$children[$i]->adjust_page($page_num_max);
# print "ADJUST CHILD ON PAGE $page_num_max FROM BASE $height_base WITH CH $children_height, GI " . $children[$i]->{gi} . ", CLASS " . $children[$i]->{class} . " TO: " . Dumper($self->{eltpos}->[$i]) . "\n";
}
else {
# print "CHILD FIT FROM BASE ON PAGE $page_num_max $height_base WITH CH $children_height, GI " . $children[$i]->{gi} . ", CLASS " . $children[$i]->{class} . "\n";
$self->{eltpos}->[$i]->{page} = $page_num_max;
}
$children_height += $children[$i]->{box}->{height};
}
}
elsif (@children) {
# print "SINGLE\n";
$children[0]->partition($page_num, $height_base);
$page_num_max++;
# $page_num += 1;
# $self->{page} = $page_num;
}
else {
$page_num_max++;
# print "Advance page for element without children to $page_num_max\n";
}
}
return $page_num_max;
}
=head2 property NAME NAME ...
Returns property.
=cut
sub property {
my ($self, @names) = @_;
my $ptr;
$ptr = $self->{specs}->{props};
for my $name (@names) {
if (exists $ptr->{$name}) {
$ptr = $ptr->{$name};
}
else {
return;
}
}
return $ptr;
}
=head2 render PARAMETERS
Renders box.
=cut
sub render {
my ($self, %parms) = @_;
my ($child, $pos, $page_before, $page_cur);
# print "RENDER for GI $self->{gi}, CLASS $self->{class} on PAGE $self->{page}: " . Dumper(\%parms);
if (exists $parms{page}
&& $parms{page} > $self->{page}) {
$self->{pdf}->select_page($parms{page});
}
else {
$self->{pdf}->select_page($self->{page});
}
$page_before = $self->{page};
# loop through our stack
for (my $i = 0; $i < @{$self->{eltstack}}; $i++) {
$child = $self->{eltstack}->[$i];
$pos = $self->{eltpos}->[$i];
$page_cur = $pos->{page} || $page_before;
if ($page_cur > $page_before) {
if ($i > 0) {
# page turn, adjust position
my $c_info = "GI " . $child->{gi} . ", CLASS " . $child->{class};
# print "PAGE TURN FROM $page_before TO $page_cur CLASS $self->{class} GI $self->{gi} FOR $c_info\n";
$parms{vpos} = $self->{pdf}->{border_top};
$parms{hpos} = $self->{pdf}->{border_left};
}
$page_before = $page_cur;
}
$child->render(hpos => $parms{hpos} + $self->{specs}->{offset}->{left} + $pos->{hpos},
vpos => $parms{vpos} - $self->{specs}->{offset}->{top} + $pos->{vpos},
page => $pos->{page} || $self->{page},
);
}
if ($self->{elt}->is_text()) {
# render text
my $chunks = $self->{box}->{chunks};
# print "Chunks: " . Dumper($chunks) . "\n";
for (my $i = 0; $i < @$chunks; $i++) {
$self->{pdf}->textbox($self->{elt}, $chunks->[$i],
$self->{specs}, {%parms, hpos => $parms{hpos} + ($self->{hoff} || 0), vpos => $parms{vpos} - ($i * $self->{specs}->{size})},
noborder => 1);
}
}
elsif ($self->{gi} eq 'img') {
# rendering image
if ($self->{object}->{type}) {
$self->{pdf}->image($self->{object},
$parms{hpos},
$parms{vpos} - $self->{box}->{height},
$self->{box}->{width},
$self->{box}->{height},
$self->{specs});
}
}
elsif ($self->{gi} eq 'hr') {
# rendering horizontal line
$self->{pdf}->hline($self->{specs}, $parms{hpos},
$parms{vpos} - $self->{specs}->{offset}->{top},
$self->{box}->{width}, $self->{specs}->{props}->{height});
}
else {
# render borders
my ($hpos, $vpos, $width, $height, $margins);
$margins = $self->{specs}->{margins};
# adjust border dimensions by margins
$hpos = $parms{hpos} + $margins->{left};
$vpos = $parms{vpos} - $margins->{top};
$width = $self->{box}->{width} - $margins->{left} - $margins->{right};
$height = $self->{box}->{height} - $margins->{top} - $margins->{bottom};
$self->{pdf}->borders($hpos, $vpos, $width, $height, $self->{specs});
}
}
=head2 adjust_page PAGE_NUM
Adjust page number for all descendants to PAGE_NUM.
=cut
sub adjust_page {
my ($self, $page_num) = @_;
my (@children);
@children = @{$self->{eltstack}};
for (my $i = 0; $i < @children; $i++) {
$self->{eltpos}->[$i]->{page} = $page_num;
$children[$i]->adjust_page($page_num);
}
}
=head2 setup_specs
Setup specifications for this box.
=cut
sub setup_specs {
my ($self) = @_;
my ($inherit);
if ($self->{parent}) {
$inherit = $self->{parent}->{specs}->{props};
}
# lookup ourselves in selector map from ancestors
if ($self->{selector_map}) {
my (@selectors);
if ($self->{class}) {
push (@selectors, ".$self->{class}");
}
if ($self->{id}) {
push (@selectors, "#$self->{id}");
}
if ($self->{gi}) {
push (@selectors, $self->{gi});
}
for my $key (@selectors) {
if ($self->{selector_map}->{$key}) {
$self->{specs} = $self->{pdf}->setup_text_props($self->{elt},
$self->{selector_map}->{$key}, $inherit);
}
}
}
$self->{specs} ||= $self->{pdf}->setup_text_props($self->{elt}, undef, $inherit);
return;
}
=head1 AUTHOR
Stefan Hornburg (Racke),
=head1 LICENSE AND COPYRIGHT
Copyright 2010-2011 Stefan Hornburg (Racke) .
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;