ReportGenerator: Unterstützung für das Verbinden von Tabellenzellen mit dem "colspan...
[kivitendo-erp.git] / modules / override / PDF / Table.pm
1 #!/usr/bin/env perl
2 # vim: softtabstop=4 tabstop=4 shiftwidth=4 ft=perl expandtab smarttab
3
4 use 5.006;
5 use strict;
6 use warnings;
7
8 package PDF::Table;
9
10 use Carp;
11 use List::Util qw(sum);
12
13 our $VERSION = '0.10.1';
14
15 print __PACKAGE__.' is version: '.$VERSION.$/ if($ENV{'PDF_TABLE_DEBUG'});
16
17 ############################################################
18 #
19 # new - Constructor
20 #
21 # Parameters are meta information about the PDF
22 #
23 # $pdf = PDF::Table->new();
24 #
25 ############################################################
26
27 sub new
28 {
29     my $type = shift(@_);
30     my $class = ref($type) || $type;
31     my $self  = {};
32     bless ($self, $class);
33
34     # Pass all the rest to init for validation and initialisation
35     $self->_init(@_);
36
37     return $self;
38 }
39
40 sub _init
41 {
42     my ($self, $pdf, $page, $data, %options ) = @_;
43
44     # Check and set default values
45     $self->set_defaults();
46
47     # Check and set mandatory params
48     $self->set_pdf($pdf);
49     $self->set_page($page);
50     $self->set_data($data);
51     $self->set_options(\%options);
52
53     return;
54 }
55
56 sub set_defaults{
57     my $self = shift;
58
59     $self->{'font_size'} = 12;
60 }
61
62 sub set_pdf{
63     my ($self, $pdf) = @_;
64     $self->{'pdf'} = $pdf;
65 }
66
67 sub set_page{
68     my ($self, $page) = @_;
69     if ( defined($page) && ref($page) ne 'PDF::API2::Page' ){
70
71         if( ref($self->{'pdf'}) eq 'PDF::API2' ){
72             $self->{'page'} = $self->{'pdf'}->page();
73         } else {
74             carp 'Warning: Page must be a PDF::API2::Page object but it seems to be: '.ref($page).$/;
75             carp 'Error: Cannot set page from passed PDF object either as it is invalid!'.$/;
76         }
77         return;
78     }
79     $self->{'page'} = $page;
80
81 }
82
83 sub set_data{
84     my ($self, $data) = @_;
85     #TODO: implement
86 }
87
88 sub set_options{
89     my ($self, $options) = @_;
90     #TODO: implement
91 }
92
93 ############################################################
94 #
95 # text_block - utility method to build multi-paragraph blocks of text
96 #
97 ############################################################
98
99 sub text_block
100 {
101     my $self        = shift;
102     my $text_object = shift;
103     my $text        = shift;    # The text to be displayed
104     my %arg         = @_;       # Additional Arguments
105
106     my  ( $align, $xpos, $ypos, $xbase, $ybase, $line_width, $wordspace, $endw , $width, $height) =
107         ( undef , undef, undef, undef , undef , undef      , undef     , undef , undef , undef  );
108     my @line        = ();       # Temp data array with words on one line
109     my %width       = ();       # The width of every unique word in the givven text
110
111     # Try to provide backward compatibility
112     foreach my $key (keys %arg)
113     {
114         my $newkey = $key;
115         if($newkey =~ s#^-##)
116         {
117             $arg{$newkey} = $arg{$key};
118             delete $arg{$key};
119         }
120     }
121     #####
122
123     #---
124     # Lets check mandatory parameters with no default values
125     #---
126     $xbase  = $arg{'x'} || -1;
127     $ybase  = $arg{'y'} || -1;
128     $width  = $arg{'w'} || -1;
129     $height = $arg{'h'} || -1;
130     unless( $xbase  > 0 ){ carp "Error: Left Edge of Block is NOT defined!\n";  return; }
131     unless( $ybase  > 0 ){ carp "Error: Base Line of Block is NOT defined!\n"; return; }
132     unless( $width  > 0 ){ carp "Error: Width of Block is NOT defined!\n";  return; }
133     unless( $height > 0 ){ carp "Error: Height of Block is NOT defined!\n"; return; }
134     # Check if any text to display
135     unless( defined( $text) and length($text) > 0 )
136     {
137         carp "Warning: No input text found. Trying to add dummy '-' and not to break everything.\n";
138         $text = '-';
139     }
140
141     # Strip any <CR> and Split the text into paragraphs
142     $text =~ s/\r//g;
143     my @paragraphs  = split(/\n/, $text);
144
145     # Width between lines in pixels
146     my $line_space = defined $arg{'lead'} && $arg{'lead'} > 0 ? $arg{'lead'} : 12;
147
148     # Calculate width of all words
149     my $space_width = $text_object->advancewidth("\x20");
150     my @words = split(/\s+/, $text);
151     foreach (@words)
152     {
153         next if exists $width{$_};
154         $width{$_} = $text_object->advancewidth($_);
155     }
156
157     my @paragraph = split(' ', shift(@paragraphs));
158     my $first_line = 1;
159     my $first_paragraph = 1;
160
161     # Little Init
162     $xpos = $xbase;
163     $ypos = $ybase;
164     $ypos = $ybase + $line_space;
165     my $bottom_border = $ypos - $height;
166     # While we can add another line
167     while ( $ypos >= $bottom_border + $line_space )
168     {
169         # Is there any text to render ?
170         unless (@paragraph)
171         {
172             # Finish if nothing left
173             last unless scalar @paragraphs;
174             # Else take one line from the text
175             @paragraph = split(' ', shift( @paragraphs ) );
176
177             $ypos -= $arg{'parspace'} if $arg{'parspace'};
178             last unless $ypos >= $bottom_border;
179         }
180         $ypos -= $line_space;
181         $xpos = $xbase;
182
183         # While there's room on the line, add another word
184         @line = ();
185         $line_width = 0;
186         if( $first_line && exists $arg{'hang'} )
187         {
188             my $hang_width = $text_object->advancewidth($arg{'hang'});
189
190             $text_object->translate( $xpos, $ypos );
191             $text_object->text( $arg{'hang'} );
192
193             $xpos         += $hang_width;
194             $line_width   += $hang_width;
195             $arg{'indent'} += $hang_width if $first_paragraph;
196         }
197         elsif( $first_line && exists $arg{'flindent'} && $arg{'flindent'} > 0 )
198         {
199             $xpos += $arg{'flindent'};
200             $line_width += $arg{'flindent'};
201         }
202         elsif( $first_paragraph && exists $arg{'fpindent'} && $arg{'fpindent'} > 0 )
203         {
204             $xpos += $arg{'fpindent'};
205             $line_width += $arg{'fpindent'};
206         }
207         elsif (exists $arg{'indent'} && $arg{'indent'} > 0 )
208         {
209             $xpos += $arg{'indent'};
210             $line_width += $arg{'indent'};
211         }
212
213         # Lets take from paragraph as many words as we can put into $width - $indent;
214         while ( @paragraph and $text_object->advancewidth( join("\x20", @line)."\x20" . $paragraph[0]) +
215                                 $line_width < $width )
216         {
217             push(@line, shift(@paragraph));
218         }
219         $line_width += $text_object->advancewidth(join('', @line));
220
221         # calculate the space width
222         if( $arg{'align'} eq 'fulljustify' or ($arg{'align'} eq 'justify' and @paragraph))
223         {
224             @line = split(//,$line[0]) if (scalar(@line) == 1) ;
225             $wordspace = ($width - $line_width) / (scalar(@line) - 1);
226             $align='justify';
227         }
228         else
229         {
230             $align=($arg{'align'} eq 'justify') ? 'left' : $arg{'align'};
231             $wordspace = $space_width;
232         }
233         $line_width += $wordspace * (scalar(@line) - 1);
234
235         if( $align eq 'justify')
236         {
237             foreach my $word (@line)
238             {
239                 $text_object->translate( $xpos, $ypos );
240                 $text_object->text( $word );
241                 $xpos += ($width{$word} + $wordspace) if (@line);
242             }
243             $endw = $width;
244         }
245         else
246         {
247             # calculate the left hand position of the line
248             if( $align eq 'right' )
249             {
250                 $xpos += $width - $line_width;
251             }
252             elsif( $align eq 'center' )
253             {
254                 $xpos += ( $width / 2 ) - ( $line_width / 2 );
255             }
256
257             # render the line
258             $text_object->translate( $xpos, $ypos );
259             $endw = $text_object->text( join("\x20", @line));
260         }
261         $first_line = 0;
262     }#End of while(
263     unshift(@paragraphs, join(' ',@paragraph)) if scalar(@paragraph);
264     return ($endw, $ypos, join("\n", @paragraphs))
265 }
266
267
268 ################################################################
269 # table - utility method to build multi-row, multicolumn tables
270 ################################################################
271 sub table
272 {
273     my $self    = shift;
274     my $pdf     = shift;
275     my $page    = shift;
276     my $data    = shift;
277     my %arg     = @_;
278
279     #=====================================
280     # Mandatory Arguments Section
281     #=====================================
282     unless($pdf and $page and $data)
283     {
284         carp "Error: Mandatory parameter is missing pdf/page/data object!\n";
285         return;
286     }
287
288     # Validate mandatory argument data type
289     croak "Error: Invalid pdf object received."  unless (ref($pdf) eq 'PDF::API2');
290     croak "Error: Invalid page object received." unless (ref($page) eq 'PDF::API2::Page');
291     croak "Error: Invalid data received."        unless ((ref($data) eq 'ARRAY') && scalar(@$data));
292     croak "Error: Missing required settings."    unless (scalar(keys %arg));
293
294     # Validate settings key
295     my %valid_settings_key = (
296         x                     => 1,
297         w                     => 1,
298         start_y               => 1,
299         start_h               => 1,
300         next_y                => 1,
301         next_h                => 1,
302         lead                  => 1,
303         padding               => 1,
304         padding_right         => 1,
305         padding_left          => 1,
306         padding_top           => 1,
307         padding_bottom        => 1,
308         background_color      => 1,
309         background_color_odd  => 1,
310         background_color_even => 1,
311         border                => 1,
312         border_color          => 1,
313         horizontal_borders    => 1,
314         vertical_borders      => 1,
315         font                  => 1,
316         font_size             => 1,
317         font_underline        => 1,
318         font_color            => 1,
319         font_color_even       => 1,
320         font_color_odd        => 1,
321         background_color_odd  => 1,
322         background_color_even => 1,
323         row_height            => 1,
324         new_page_func         => 1,
325         header_props          => 1,
326         column_props          => 1,
327         cell_props            => 1,
328         max_word_length       => 1,
329         cell_render_hook      => 1,
330         default_text          => 1,
331         num_header_rows       => 1,
332     );
333     foreach my $key (keys %arg)
334     {
335         # Provide backward compatibility
336         $arg{$key} = delete $arg{"-$key"} if $key =~ s/^-//;
337
338         croak "Error: Invalid setting key '$key' received."
339             unless exists $valid_settings_key{$key};
340     }
341
342
343     ######
344     #TODO: Add code for header props compatibility and col_props comp....
345     ######
346     my ( $xbase, $ybase, $width, $height ) = ( undef, undef, undef, undef );
347     # Could be 'int' or 'real' values
348     $xbase  = $arg{'x'      } || -1;
349     $ybase  = $arg{'start_y'} || -1;
350     $width  = $arg{'w'      } || -1;
351     $height = $arg{'start_h'} || -1;
352
353     # Global geometry parameters are also mandatory.
354     unless( $xbase  > 0 ){ carp "Error: Left Edge of Table is NOT defined!\n";  return; }
355     unless( $ybase  > 0 ){ carp "Error: Base Line of Table is NOT defined!\n"; return; }
356     unless( $width  > 0 ){ carp "Error: Width of Table is NOT defined!\n";  return; }
357     unless( $height > 0 ){ carp "Error: Height of Table is NOT defined!\n"; return; }
358
359     # Ensure default values for -next_y and -next_h
360     my $next_y  = $arg{'next_y'} || $arg{'start_y'} || 0;
361     my $next_h  = $arg{'next_h'} || $arg{'start_h'} || 0;
362
363     # Create Text Object
364     my $txt     = $page->text;
365
366     # Set Default Properties
367     my $fnt_name       = $arg{'font'            } || $pdf->corefont('Times',-encode => 'utf8');
368     my $fnt_size       = $arg{'font_size'       } || 12;
369     my $fnt_underline  = $arg{'font_underline'  } || undef; # merely stating undef is the intended default
370     my $max_word_len   = $arg{'max_word_length' } || 20;
371
372     #=====================================
373     # Table Header Section
374     #=====================================
375     # Disable header row into the table
376     my $header_props = undef;
377     my (@header_rows, @header_row_cell_props);
378     # Check if the user enabled it ?
379     if(defined $arg{'header_props'} and ref( $arg{'header_props'}) eq 'HASH')
380     {
381         # Transfer the reference to local variable
382         $header_props = $arg{'header_props'};
383
384         # Check other params and put defaults if needed
385         $header_props->{'repeat'        } = $header_props->{'repeat'        } || 0;
386         $header_props->{'font'          } = $header_props->{'font'          } || $fnt_name;
387         $header_props->{'font_color'    } = $header_props->{'font_color'    } || '#000066';
388         $header_props->{'font_size'     } = $header_props->{'font_size'     } || $fnt_size + 2;
389         $header_props->{'font_underline'} = $header_props->{'font_underline'} || $fnt_underline;
390         $header_props->{'bg_color'      } = $header_props->{'bg_color'      } || '#FFFFAA';
391         $header_props->{'justify'       } = $header_props->{'justify'       };
392         $header_props->{num_header_rows } = $arg{num_header_rows } || 1;
393     }
394     #=====================================
395     # Other Parameters check
396     #=====================================
397     my $lead          = $arg{'lead'          } || $fnt_size;
398     my $pad_left      = $arg{'padding_left'  } || $arg{'padding'} || 0;
399     my $pad_right     = $arg{'padding_right' } || $arg{'padding'} || 0;
400     my $pad_top       = $arg{'padding_top'   } || $arg{'padding'} || 0;
401     my $pad_bot       = $arg{'padding_bottom'} || $arg{'padding'} || 0;
402     my $default_text  = $arg{'default_text'  } // '-';
403     my $line_w        = defined $arg{'border'} ? $arg{'border'} : 1 ;
404     my $horiz_borders = defined $arg{'horizontal_borders'}
405         ? $arg{'horizontal_borders'}
406         : $line_w;
407     my $vert_borders  = defined $arg{'vertical_borders'}
408         ? $arg{'vertical_borders'}
409         : $line_w;
410
411     my $background_color_even   = $arg{'background_color_even'  } || $arg{'background_color'} || undef;
412     my $background_color_odd    = $arg{'background_color_odd'   } || $arg{'background_color'} || undef;
413     my $font_color_even         = $arg{'font_color_even'        } || $arg{'font_color'      } || 'black';
414     my $font_color_odd          = $arg{'font_color_odd'         } || $arg{'font_color'      } || 'black';
415     my $border_color            = $arg{'border_color'           } || 'black';
416
417     my $min_row_h   = $fnt_size + $pad_top + $pad_bot;
418     my $row_h       = defined ($arg{'row_height'})
419                                 &&
420                     ($arg{'row_height'} > $min_row_h)
421                                 ?
422                      $arg{'row_height'} : $min_row_h;
423
424     my $pg_cnt      = 1;
425     my $cur_y       = $ybase;
426     my $cell_props  = $arg{cell_props} || [];   # per cell properties
427
428     #If there is no valid data array reference warn and return!
429     if(ref $data ne 'ARRAY')
430     {
431         carp "Passed table data is not an ARRAY reference. It's actually a ref to ".ref($data);
432         return ($page,0,$cur_y);
433     }
434
435     # Copy the header row if header is enabled
436     if (defined $header_props) {
437       map { push @header_rows,           $$data[$_]       } (0..$header_props->{num_header_rows} - 1);
438       map { push @header_row_cell_props, $$cell_props[$_] } (0..$header_props->{num_header_rows} - 1);
439     }
440     # Determine column widths based on content
441
442     #  an arrayref whose values are a hashref holding
443     #  the minimum and maximum width of that column
444     my $col_props =  $arg{'column_props'} || [];
445
446     # An array ref of arrayrefs whose values are
447     #  the actual widths of the column/row intersection
448     my $row_col_widths = [];
449     # An array ref with the widths of the header row
450     my @header_row_widths;
451
452     # Scalars that hold sum of the maximum and minimum widths of all columns
453     my ( $max_col_w  , $min_col_w   ) = ( 0,0 );
454     my ( $row, $col_name, $col_fnt_size, $col_fnt_underline, $space_w );
455
456     my $word_widths  = {};
457     my $rows_height  = [];
458     my $first_row    = 1;
459
460     for( my $row_idx = 0; $row_idx < scalar(@$data) ; $row_idx++ )
461     {
462         push @header_row_widths, [] if $row_idx < $header_props->{num_header_rows};
463
464         my $column_widths = []; #holds the width of each column
465         # Init the height for this row
466         $rows_height->[$row_idx] = 0;
467
468         for( my $column_idx = 0; $column_idx < scalar(@{$data->[$row_idx]}) ; $column_idx++ )
469         {
470             # look for font information for this column
471             my ($cell_font, $cell_font_size, $cell_font_underline);
472
473             if( !$row_idx and ref $header_props )
474             {
475                 $cell_font           = $header_props->{'font'};
476                 $cell_font_size      = $header_props->{'font_size'};
477                 $cell_font_underline = $header_props->{'font_underline'};
478             }
479
480             # Get the most specific value if none was already set from header_props
481             $cell_font      ||= $cell_props->[$row_idx][$column_idx]->{'font'}
482                             ||  $col_props->[$column_idx]->{'font'}
483                             ||  $fnt_name;
484
485             $cell_font_size ||= $cell_props->[$row_idx][$column_idx]->{'font_size'}
486                             ||  $col_props->[$column_idx]->{'font_size'}
487                             ||  $fnt_size;
488
489             $cell_font_underline ||= $cell_props->[$row_idx][$column_idx]->{'font_underline'}
490                                  ||  $col_props->[$column_idx]->{'font_underline'}
491                                  ||  $fnt_underline;
492
493             # Set Font
494
495             # Set Font
496             $txt->font( $cell_font, $cell_font_size );
497
498             # Set row height to biggest font size from row's cells
499             if( $cell_font_size  > $rows_height->[$row_idx] )
500             {
501                 $rows_height->[$row_idx] = $cell_font_size;
502             }
503
504             # This should fix a bug with very long words like serial numbers etc.
505             if( $max_word_len > 0 )
506             {
507                 $data->[$row_idx][$column_idx] =~ s#(\S{$max_word_len})(?=\S)#$1 #g;
508             }
509
510             # Init cell size limits
511             $space_w                      = $txt->advancewidth( "\x20" );
512             $column_widths->[$column_idx] = 0;
513             $max_col_w                    = 0;
514             $min_col_w                    = 0;
515
516             my @words = split( /\s+/, $data->[$row_idx][$column_idx] );
517
518             foreach( @words )
519             {
520                 unless( exists $word_widths->{$_} )
521                 {   # Calculate the width of every word and add the space width to it
522                     $word_widths->{$_} = $txt->advancewidth( $_ ) + $space_w;
523                 }
524
525                 $column_widths->[$column_idx] += $word_widths->{$_};
526                 $min_col_w                     = $word_widths->{$_} if( $word_widths->{$_} > $min_col_w );
527                 $max_col_w                    += $word_widths->{$_};
528             }
529
530             $min_col_w                    += $pad_left + $pad_right;
531             $max_col_w                    += $pad_left + $pad_right;
532             $column_widths->[$column_idx] += $pad_left + $pad_right;
533
534             # Keep a running total of the overall min and max widths
535             $col_props->[$column_idx]->{'min_w'} ||= 0;
536             $col_props->[$column_idx]->{'max_w'} ||= 0;
537
538             if( $min_col_w > $col_props->[$column_idx]->{'min_w'} )
539             {   # Calculated Minimum Column Width is more than user-defined
540                 $col_props->[$column_idx]->{'min_w'} = $min_col_w ;
541             }
542
543             if( $max_col_w > $col_props->[$column_idx]->{'max_w'} )
544             {   # Calculated Maximum Column Width is more than user-defined
545                 $col_props->[$column_idx]->{'max_w'} = $max_col_w ;
546             }
547         }#End of for(my $column_idx....
548
549         $row_col_widths->[$row_idx] = $column_widths;
550
551         # Copy the calculated row properties of header row.
552         if (ref $header_props && $row_idx < $header_props->{num_header_rows}) {
553           push @header_row_widths, [ @{ $column_widths } ];
554         }
555     }
556
557     # Calc real column widths and expand table width if needed.
558     my $calc_column_widths;
559     ($calc_column_widths, $width) = CalcColumnWidths( $col_props, $width );
560     my $num_cols = scalar @{ $calc_column_widths };
561
562     # Lets draw what we have!
563     my $row_index    = 0;
564     # Store header row height for later use if headers have to be repeated
565     my @header_row_heights = @$rows_height[0 .. $header_props->{num_header_rows}-1];
566
567     my ( $gfx, $gfx_bg, $background_color, $font_color, $bot_marg, $table_top_y, $text_start);
568
569     my $remaining_header_rows = $header_props ? $header_props->{num_header_rows} : 0;
570
571     # Each iteration adds a new page as neccessary
572     while(scalar(@{$data}))
573     {
574         my ($page_header);
575         my $columns_number = 0;
576
577         if($pg_cnt == 1)
578         {
579             $table_top_y = $ybase;
580             $bot_marg = $table_top_y - $height;
581
582             # Check for safety reasons
583             if( $bot_marg < 0 )
584             {   # This warning should remain i think
585                 #carp "!!! Warning: !!! Incorrect Table Geometry! start_h (${height}) is above start_y (${table_top_y}). Setting bottom margin to end of sheet!\n";
586                 $bot_marg = 0;
587             }
588
589         }
590         else
591         {
592             if(ref $arg{'new_page_func'})
593             {
594                 $page = &{$arg{'new_page_func'}};
595             }
596             else
597             {
598                 $page = $pdf->page;
599             }
600
601             $table_top_y = $next_y;
602             $bot_marg = $table_top_y - $next_h;
603
604             # Check for safety reasons
605             if( $bot_marg < 0 )
606             {   # This warning should remain i think
607                 #carp "!!! Warning: !!! Incorrect Table Geometry! next_y or start_y (${next_y}) is above next_h or start_h (${next_h}). Setting bottom margin to end of sheet!\n";
608                 $bot_marg = 0;
609             }
610
611             if( ref $header_props and $header_props->{'repeat'})
612             {
613                 for my $idx (0 .. $header_props->{num_header_rows} - 1) {
614                   unshift @$data,           [ @{ $header_rows[$idx]      } ];
615                   unshift @$row_col_widths, [ @{ $header_row_widths[$idx] } ];
616                   unshift @$rows_height,    $header_row_heights[$idx];
617                 }
618                 $remaining_header_rows = $header_props->{num_header_rows};
619                 $first_row = 1;
620             }
621         }
622
623         $gfx_bg = $page->gfx;
624         $txt = $page->text;
625         $txt->font($fnt_name, $fnt_size);
626
627         $cur_y = $table_top_y;
628
629         if ($line_w)
630         {
631             $gfx = $page->gfx;
632             $gfx->strokecolor($border_color);
633             $gfx->linewidth($line_w);
634
635             # Draw the top line
636             if ($horiz_borders)
637             {
638                 $gfx->move( $xbase , $cur_y );
639                 $gfx->hline($xbase + $width );
640             }
641         }
642         else
643         {
644             $gfx = undef;
645         }
646
647         # Each iteration adds a row to the current page until the page is full
648         #  or there are no more rows to add
649         # Row_Loop
650         while(scalar(@{$data}) and $cur_y-$row_h > $bot_marg)
651         {
652             # Remove the next item from $data
653             my $record = shift @{$data};
654
655             # Get max columns number to know later how many vertical lines to draw
656             $columns_number = scalar(@$record)
657                 if scalar(@$record) > $columns_number;
658
659             # Get the next set of row related settings
660             # Row Height
661             my $pre_calculated_row_height = shift @$rows_height;
662
663             # Row cell widths
664             my $record_widths = shift @$row_col_widths;
665
666             # Row coloumn props - TODO in another commit
667
668             # Row cell props - TODO in another commit
669
670
671             # Choose colors for this row
672             $background_color = $row_index % 2 ? $background_color_even  : $background_color_odd;
673             $font_color       = $row_index % 2 ? $font_color_even        : $font_color_odd;
674
675             #Determine current row height
676             my $current_row_height = $pad_top + $pre_calculated_row_height + $pad_bot;
677
678             # $row_h is the calculated global user requested row height.
679             # It will be honored, only if it has bigger value than the calculated one.
680             # TODO: It's questionable if padding should be inclided in this calculation or not
681             if($current_row_height < $row_h){
682                 $current_row_height = $row_h;
683             }
684
685             # Define the font y base position for this line.
686             $text_start      = $cur_y - ($current_row_height - $pad_bot);
687
688             my $cur_x        = $xbase;
689             my $leftovers    = undef;   # Reference to text that is returned from textblock()
690             my $do_leftovers = 0;
691             my ($colspan, @vertical_lines);
692
693             # Process every cell(column) from current row
694             for( my $column_idx = 0; $column_idx < scalar( @$record); $column_idx++ )
695             {
696                 next unless $col_props->[$column_idx]->{'max_w'};
697                 next unless $col_props->[$column_idx]->{'min_w'};
698                 $leftovers->[$column_idx] = undef;
699
700                 # look for font information for this cell
701                 my ($cell_font, $cell_font_size, $cell_font_color, $cell_font_underline, $justify);
702
703                 if( $remaining_header_rows and ref $header_props)
704                 {
705                     $cell_font           = $header_props->{'font'};
706                     $cell_font_size      = $header_props->{'font_size'};
707                     $cell_font_color     = $header_props->{'font_color'};
708                     $cell_font_underline = $header_props->{'font_underline'};
709                     $justify             = $header_props->{'justify'};
710                 }
711
712                 # Get the most specific value if none was already set from header_props
713                 $cell_font       ||= $cell_props->[$row_index][$column_idx]->{'font'}
714                                  ||  $col_props->[$column_idx]->{'font'}
715                                  ||  $fnt_name;
716
717                 $cell_font_size  ||= $cell_props->[$row_index][$column_idx]->{'font_size'}
718                                  ||  $col_props->[$column_idx]->{'font_size'}
719                                  ||  $fnt_size;
720
721                 $cell_font_color ||= $cell_props->[$row_index][$column_idx]->{'font_color'}
722                                  ||  $col_props->[$column_idx]->{'font_color'}
723                                  ||  $font_color;
724
725                 $cell_font_underline ||= $cell_props->[$row_index][$column_idx]->{'font_underline'}
726                                      ||  $col_props->[$column_idx]->{'font_underline'}
727                                      ||  $fnt_underline;
728
729
730                 $justify         ||= $cell_props->[$row_index][$column_idx]->{'justify'}
731                                  ||  $col_props->[$column_idx]->{'justify'}
732                                  ||  $arg{'justify'}
733                                  ||  'left';
734
735                 # Init cell font object
736                 $txt->font( $cell_font, $cell_font_size );
737                 $txt->fillcolor($cell_font_color);
738
739                 # Added to resolve infite loop bug with returned undef values
740                 $record->[$column_idx] //= $cell_props->[$row_index][$column_idx]->{'default_text'}
741                                        //  $col_props->[$column_idx]->{'default_text'}
742                                        //  $default_text;
743
744                 my $this_width;
745                 if (!$remaining_header_rows && $cell_props->[$row_index][$column_idx]->{colspan}) {
746                     $colspan = $cell_props->[$row_index][$column_idx]->{colspan};
747                 } elsif ($remaining_header_rows && $header_row_cell_props[$header_props->{num_header_rows} - $remaining_header_rows][$column_idx]->{colspan}) {
748                     $colspan = $header_row_cell_props[$header_props->{num_header_rows} - $remaining_header_rows][$column_idx]->{colspan};
749                 }
750
751                 if ($colspan) {
752                     $colspan     = $num_cols - $column_idx if (-1 == $colspan);
753                     my $last_idx = $column_idx + $colspan - 1;
754                     $this_width  = sum @{ $calc_column_widths }[$column_idx..$last_idx];
755                 } else {
756                     $this_width = $calc_column_widths->[$column_idx];
757                 }
758
759                 # If the content is wider than the specified width, we need to add the text as a text block
760                 if( $record->[$column_idx] !~ m/(.\n.)/ and
761                     $record_widths->[$column_idx] and
762                     $record_widths->[$column_idx] <= $this_width
763                 ){
764                     my $space = $pad_left;
765                     if ($justify eq 'right')
766                     {
767                         $space = $this_width -($txt->advancewidth($record->[$column_idx]) + $pad_right);
768                     }
769                     elsif ($justify eq 'center')
770                     {
771                         $space = ($this_width - $txt->advancewidth($record->[$column_idx])) / 2;
772                     }
773                     $txt->translate( $cur_x + $space, $text_start );
774                     my %text_options;
775                     $text_options{'-underline'} = $cell_font_underline if $cell_font_underline;
776                     $txt->text( $record->[$column_idx], %text_options );
777                 }
778                 # Otherwise just use the $page->text() method
779                 else
780                 {
781                     my ($width_of_last_line, $ypos_of_last_line, $left_over_text) = $self->text_block(
782                         $txt,
783                         $record->[$column_idx],
784                         x        => $cur_x + $pad_left,
785                         y        => $text_start,
786                         w        => $this_width - $pad_left - $pad_right,
787                         h        => $cur_y - $bot_marg - $pad_top - $pad_bot,
788                         align    => $justify,
789                         lead     => $lead
790                     );
791                     # Desi - Removed $lead because of fixed incorrect ypos bug in text_block
792                     my  $current_cell_height = $cur_y - $ypos_of_last_line + $pad_bot;
793                     if( $current_cell_height > $current_row_height )
794                     {
795                         $current_row_height = $current_cell_height;
796                     }
797
798                     if( $left_over_text )
799                     {
800                         $leftovers->[$column_idx] = $left_over_text;
801                         $do_leftovers = 1;
802                     }
803                 }
804
805                 # Hook to pass coordinates back - http://www.perlmonks.org/?node_id=754777
806                 if (ref $arg{cell_render_hook} eq 'CODE') {
807                    $arg{cell_render_hook}->(
808                                             $page,
809                                             $first_row,
810                                             $row_index,
811                                             $column_idx,
812                                             $cur_x,
813                                             $cur_y-$row_h,
814                                             $calc_column_widths->[$column_idx],
815                                             $row_h
816                                            );
817                 }
818
819                 $cur_x += $calc_column_widths->[$column_idx];
820
821                 push @vertical_lines, (!$colspan || (1 >= $colspan)) ? 1 : 0;
822                 $colspan-- if $colspan;
823             }
824             if( $do_leftovers )
825             {
826                 unshift @$data, $leftovers;
827                 unshift @$row_col_widths, $record_widths;
828                 unshift @$rows_height, $pre_calculated_row_height;
829             }
830
831             # Draw cell bgcolor
832             # This has to be separately from the text loop
833             #  because we do not know the final height of the cell until all text has been drawn
834             $cur_x = $xbase;
835             for(my $column_idx = 0 ; $column_idx < scalar(@$record) ; $column_idx++)
836             {
837                 my $cell_bg_color;
838
839                 if( $remaining_header_rows and ref $header_props)
840                 {                                  #Compatibility                 Consistency with other props
841                     $cell_bg_color = $header_props->{'bg_color'} || $header_props->{'background_color'};
842                 }
843
844                 # Get the most specific value if none was already set from header_props
845                 $cell_bg_color ||= $cell_props->[$row_index][$column_idx]->{'background_color'}
846                                ||  $col_props->[$column_idx]->{'background_color'}
847                                ||  $background_color;
848
849                 if ($cell_bg_color)
850                 {
851                     $gfx_bg->rect( $cur_x, $cur_y-$current_row_height, $calc_column_widths->[$column_idx], $current_row_height);
852                     $gfx_bg->fillcolor($cell_bg_color);
853                     $gfx_bg->fill();
854                 }
855                 $cur_x += $calc_column_widths->[$column_idx];
856
857                 if ($line_w && $vertical_lines[$column_idx] && ($column_idx != (scalar(@{ $record }) - 1))) {
858                     $gfx->move($cur_x, $cur_y);
859                     $gfx->vline($cur_y - $row_h);
860                     $gfx->fillcolor($border_color);
861                 }
862             }#End of for(my $column_idx....
863
864             $cur_y -= $current_row_height;
865             if ($gfx && $horiz_borders)
866             {
867                 $gfx->move(  $xbase , $cur_y );
868                 $gfx->hline( $xbase + $width );
869             }
870
871             $first_row = 0;
872             if ($remaining_header_rows) {
873               $remaining_header_rows--;
874             } else {
875               $row_index++ unless $do_leftovers;
876             }
877         }# End of Row_Loop
878
879         if ($gfx)
880         {
881             # Draw vertical lines
882             if ($vert_borders)
883             {
884                 $gfx->move(  $xbase, $table_top_y);
885                 $gfx->vline( $cur_y );
886                 $gfx->move($xbase + sum(@{ $calc_column_widths }[0..$num_cols - 1]), $table_top_y);
887                 $gfx->vline( $cur_y );
888             }
889
890             # ACTUALLY draw all the lines
891             $gfx->fillcolor( $border_color);
892             $gfx->stroke;
893         }
894         $pg_cnt++;
895     }# End of while(scalar(@{$data}))
896
897     return ($page,--$pg_cnt,$cur_y);
898 }
899
900
901 # calculate the column widths
902 sub CalcColumnWidths
903 {
904     my $col_props   = shift;
905     my $avail_width = shift;
906     my $min_width   = 0;
907
908     my $calc_widths ;
909
910     for(my $j = 0; $j < scalar( @$col_props); $j++)
911     {
912         $min_width += $col_props->[$j]->{min_w} || 0;
913     }
914
915     # I think this is the optimal variant when good view can be guaranateed
916     if($avail_width < $min_width)
917     {
918         carp "!!! Warning !!!\n Calculated Mininal width($min_width) > Table width($avail_width).\n",
919             ' Expanding table width to:',int($min_width)+1,' but this could lead to unexpected results.',"\n",
920             ' Possible solutions:',"\n",
921             '  0)Increase table width.',"\n",
922             '  1)Decrease font size.',"\n",
923             '  2)Choose a more narrow font.',"\n",
924             '  3)Decrease "max_word_length" parameter.',"\n",
925             '  4)Rotate page to landscape(if it is portrait).',"\n",
926             '  5)Use larger paper size.',"\n",
927             '!!! --------- !!!',"\n";
928         $avail_width = int( $min_width) + 1;
929
930     }
931
932     # Calculate how much can be added to every column to fit the available width.
933     for(my $j = 0; $j < scalar(@$col_props); $j++ )
934     {
935         $calc_widths->[$j] = $col_props->[$j]->{min_w} || 0;;
936     }
937
938     # Allow columns to expand to max_w before applying extra space equally.
939     my $is_last_iter;
940     for (;;)
941     {
942         my $span = ($avail_width - $min_width) / scalar( @$col_props);
943         last if $span <= 0;
944
945         $min_width = 0;
946         my $next_will_be_last_iter = 1;
947         for(my $j = 0; $j < scalar(@$col_props); $j++ )
948         {
949             my $new_w = $calc_widths->[$j] + $span;
950
951             if (!$is_last_iter && $new_w > $col_props->[$j]->{max_w})
952             {
953                 $new_w = $col_props->[$j]->{max_w}
954             }
955             if ($calc_widths->[$j] != $new_w )
956             {
957                 $calc_widths->[$j] = $new_w;
958                 $next_will_be_last_iter = 0;
959             }
960             $min_width += $new_w;
961         }
962         last if $is_last_iter;
963         $is_last_iter = $next_will_be_last_iter;
964     }
965
966     return ($calc_widths,$avail_width);
967 }
968 1;
969
970 __END__
971
972 =pod
973
974 =head1 NAME
975
976 PDF::Table - A utility class for building table layouts in a PDF::API2 object.
977
978 =head1 SYNOPSIS
979
980  use PDF::API2;
981  use PDF::Table;
982
983  my $pdftable = new PDF::Table;
984  my $pdf = new PDF::API2(-file => "table_of_lorem.pdf");
985  my $page = $pdf->page;
986
987  # some data to layout
988  my $some_data =[
989     ["1 Lorem ipsum dolor",
990     "Donec odio neque, faucibus vel",
991     "consequat quis, tincidunt vel, felis."],
992     ["Nulla euismod sem eget neque.",
993     "Donec odio neque",
994     "Sed eu velit."],
995     #... and so on
996  ];
997
998  $left_edge_of_table = 50;
999  # build the table layout
1000  $pdftable->table(
1001      # required params
1002      $pdf,
1003      $page,
1004      $some_data,
1005      x => $left_edge_of_table,
1006      w => 495,
1007      start_y => 500,
1008      start_h => 300,
1009      # some optional params
1010      next_y  => 750,
1011      next_h  => 500,
1012      padding => 5,
1013      padding_right => 10,
1014      background_color_odd  => "gray",
1015      background_color_even => "lightblue", #cell background color for even rows
1016   );
1017
1018  # do other stuff with $pdf
1019  $pdf->saveas();
1020 ...
1021
1022 =head1 EXAMPLE
1023
1024 For a complete working example or initial script look into distribution`s 'examples' folder.
1025
1026
1027 =head1 DESCRIPTION
1028
1029 This class is a utility for use with the PDF::API2 module from CPAN.
1030 It can be used to display text data in a table layout within a PDF.
1031 The text data must be in a 2D array (such as returned by a DBI statement handle fetchall_arrayref() call).
1032 The PDF::Table will automatically add as many new pages as necessary to display all of the data.
1033 Various layout properties, such as font, font size, and cell padding and background color can be specified for each column and/or for even/odd rows.
1034 Also a (non)repeated header row with different layout properties can be specified.
1035
1036 See the L</METHODS> section for complete documentation of every parameter.
1037
1038 =head1 METHODS
1039
1040 =head2 new()
1041
1042     my $pdf_table = new PDF::Table;
1043
1044 =over
1045
1046 =item Description
1047
1048 Creates a new instance of the class. (to be improved)
1049
1050 =item Parameters
1051
1052 There are no parameters.
1053
1054 =item Returns
1055
1056 Reference to the new instance
1057
1058 =back
1059
1060 =head2 table()
1061
1062     my ($final_page, $number_of_pages, $final_y) = table($pdf, $page, $data, %settings)
1063
1064 =over
1065
1066 =item Description
1067
1068 Generates a multi-row, multi-column table into an existing PDF document based on provided data set and settings.
1069
1070 =item Parameters
1071
1072     $pdf      - a PDF::API2 instance representing the document being created
1073     $page     - a PDF::API2::Page instance representing the current page of the document
1074     $data     - an ARRAY reference to a 2D data structure that will be used to build the table
1075     %settings - HASH with geometry and formatting parameters.
1076
1077 For full %settings description see section L</Table settings> below.
1078
1079 This method will add more pages to the pdf instance as required based on the formatting options and the amount of data.
1080
1081 =item Returns
1082
1083 The return value is a 3 items list where
1084
1085     $final_page - The first item is a PDF::API2::Page instance that the table ends on
1086     $number_of_pages - The second item is the count of pages that the table spans on
1087     $final_y - The third item is the Y coordinate of the table bottom so that additional content can be added in the same document.
1088
1089 =item Example
1090
1091     my $pdf  = new PDF::API2;
1092     my $page = $pdf->page();
1093     my $data = [
1094         ['foo1','bar1','baz1'],
1095         ['foo2','bar2','baz2']
1096     ];
1097     my %settings = (
1098         x       => 10,
1099         w       => 570,
1100         start_y => 220,
1101         start_h => 180,
1102     );
1103
1104     my ($final_page, $number_of_pages, $final_y) = $pdftable->table( $pdf, $page, $data, %options );
1105
1106 =back
1107
1108 =head3 Table settings
1109
1110 =head4 Mandatory
1111
1112 There are some mandatory parameteres for setting table geometry and position across page(s)
1113
1114 =over
1115
1116 =item B<x> - X coordinate of upper left corner of the table. Left edge of the sheet is 0.
1117
1118 B<Value:> can be any whole number satisfying 0 =< X < PageWidth
1119 B<Default:> No default value
1120
1121     x => 10
1122
1123 =item B<start_y> - Y coordinate of upper left corner of the table at the initial page.
1124
1125 B<Value:> can be any whole number satisfying 0 < start_y < PageHeight (depending on space availability when embedding a table)
1126 B<Default:> No default value
1127
1128     start_y => 327
1129
1130 =item B<w> - width of the table starting from X.
1131
1132 B<Value:> can be any whole number satisfying 0 < w < PageWidth - x
1133 B<Default:> No default value
1134
1135     w  => 570
1136
1137 =item B<start_h> - Height of the table on the initial page
1138
1139 B<Value:> can be any whole number satisfying 0 < start_h < PageHeight - Current Y position
1140 B<Default:> No default value
1141
1142     start_h => 250
1143
1144 =back
1145
1146 =head4 Optional
1147
1148 =over
1149
1150 =item B<next_h> - Height of the table on any additional page
1151
1152 B<Value:> can be any whole number satisfying 0 < next_h < PageHeight
1153 B<Default:> Value of param B<'start_h'>
1154
1155     next_h  => 700
1156
1157 =item B<next_y> - Y coordinate of upper left corner of the table at any additional page.
1158
1159 B<Value:> can be any whole number satisfying 0 < next_y < PageHeight
1160 B<Default:> Value of param B<'start_y'>
1161
1162     next_y  => 750
1163
1164 =item B<max_word_length> - Breaks long words (like serial numbers hashes etc.) by adding a space after every Nth symbol
1165
1166 B<Value:> can be any whole positive number
1167 B<Default:> 20
1168
1169     max_word_length => 20    # Will add a space after every 20 symbols
1170
1171 =item B<padding> - Padding applied to every cell
1172
1173 =item B<padding_top>    - top cell padding, overrides 'padding'
1174
1175 =item B<padding_right>  - right cell padding, overrides 'padding'
1176
1177 =item B<padding_left>   - left cell padding, overrides 'padding'
1178
1179 =item B<padding_bottom> - bottom padding, overrides 'padding'
1180
1181 B<Value:> can be any whole positive number
1182
1183 B<Default padding:> 0
1184
1185 B<Default padding_*> $padding
1186
1187     padding        => 5      # all sides cell padding
1188     padding_top    => 8,     # top cell padding, overrides 'padding'
1189     padding_right  => 6,     # right cell padding, overrides 'padding'
1190     padding_left   => 2,     # left cell padding, overrides 'padding'
1191     padding_bottom => undef  # bottom padding will be 5 as it will fallback to 'padding'
1192
1193 =item B<border> - Width of table border lines.
1194
1195 =item B<horizontal_borders> - Width of horizontal border lines. Overrides 'border' value.
1196
1197 =item B<vertical_borders> -  Width of vertical border lines. Overrides 'border' value.
1198
1199 B<Value:> can be any whole positive number. When set to 0 will disable border lines.
1200 B<Default:> 1
1201
1202     border             => 3     # border width is 3
1203     horizontal_borders => 1     # horizontal borders will be 1 overriding 3
1204     vertical_borders   => undef # vertical borders will be 3 as it will fallback to 'border'
1205
1206 =item B<vertical_borders> -  Width of vertical border lines. Overrides 'border' value.
1207
1208 B<Value:> Color specifier as 'name' or 'HEX'
1209 B<Default:> 'black'
1210
1211     border_color => 'red'
1212
1213 =item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in the table
1214
1215 B<Value:> can be any PDF::API2::Resource::* type of font
1216 B<Default:> 'Times' with UTF8 encoding
1217
1218     font => $pdf->corefont("Helvetica", -encoding => "utf8")
1219
1220 =item B<font_size> - Default size of the font that will be used across the table
1221
1222 B<Value:> can be any positive number
1223 B<Default:> 12
1224
1225     font_size => 16
1226
1227 =item B<font_color> - Font color for all rows
1228
1229 =item B<font_color_odd> - Font color for odd rows
1230
1231 =item B<font_color_even> - Font color for even rows
1232
1233 =item B<font_underline> - Font underline of the header row
1234
1235 B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
1236 B<Default:> none
1237
1238 =item B<background_color_odd> - Background color for odd rows
1239
1240 =item B<background_color_even> - Background color for even rows
1241
1242 B<Value:> Color specifier as 'name' or 'HEX'
1243 B<Default:> 'black' font on 'white' background
1244
1245     font_color            => '#333333'
1246     font_color_odd        => 'purple'
1247     font_color_even       => '#00FF00'
1248     background_color_odd  => 'gray'
1249     background_color_even => 'lightblue'
1250
1251 =item B<row_height> - Desired row height but it will be honored only if row_height > font_size + padding_top + padding_bottom
1252
1253 B<Value:> can be any whole positive number
1254 B<Default:> font_size + padding_top + padding_bottom
1255
1256     row_height => 24
1257
1258 =item B<new_page_func> - CODE reference to a function that returns a PDF::API2::Page instance.
1259
1260 If used the parameter 'new_page_func' must be a function reference which when executed will create a new page and will return the object back to the module.
1261 For example you can use it to put Page Title, Page Frame, Page Numbers and other staff that you need.
1262 Also if you need some different type of paper size and orientation than the default A4-Portrait for example B2-Landscape you can use this function ref to set it up for you. For more info about creating pages refer to PDF::API2 PAGE METHODS Section.
1263 Don't forget that your function must return a page object created with PDF::API2 page() method.
1264
1265     new_page_func  => $code_ref
1266
1267 =item B<header_props> - HASH reference to specific settings for the Header row of the table. See section L</Header Row Properties> below
1268
1269     header_props => $hdr_props
1270
1271 =item B<column_props> - HASH reference to specific settings for each column of the table. See section L</Column Properties> below
1272
1273     column_props => $col_props
1274
1275 =item B<cell_props> - HASH reference to specific settings for each column of the table. See section L</Cell Properties> below
1276
1277     cell_props => $cel_props
1278
1279 =item B<cell_render_hook> - CODE reference to a function called with the current cell coordinates.  If used the parameter 'cell_render_hook' must be a function reference. It is most useful for creating a url link inside of a cell. The following example adds a link in the first column of each non-header row:
1280
1281     cell_render_hook  => sub {
1282         my ($page, $first_row, $row, $col, $x, $y, $w, $h) = @_;
1283
1284         # Do nothing except for first column (and not a header row)
1285         return unless ($col == 0);
1286         return if ($first_row);
1287
1288         # Create link
1289         my $value = $list_of_vals[$row-1];
1290         my $url = "https://${hostname}/app/${value}";
1291
1292         my $annot = $page->annotation();
1293         $annot->url( $url, -rect => [$x, $y, $x+$w, $y+$h] );
1294     },
1295
1296 =back
1297
1298 =head4 Header Row Properties
1299
1300 If the 'header_props' parameter is used, it should be a hashref. Passing an empty HASH will trigger a header row initialised with Default values.
1301 There is no 'data' variable for the content, because the module asumes that first table row will become the header row. It will copy this row and put it on every new page if 'repeat' param is set.
1302
1303 =over
1304
1305 =item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in the header row
1306
1307 B<Value:> can be any PDF::API2::Resource::* type of font
1308 B<Default:> 'font' of the table. See table parameter 'font' for more details.
1309
1310 =item B<font_size> - Font size of the header row
1311
1312 B<Value:> can be any positive number
1313 B<Default:> 'font_size' of the table + 2
1314
1315 =item B<font_color> - Font color of the header row
1316
1317 B<Value:> Color specifier as 'name' or 'HEX'
1318 B<Default:> '#000066'
1319
1320 =item B<font_underline> - Font underline of the header row
1321
1322 B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
1323 B<Default:> none
1324
1325 =item B<bg_color> - Background color of the header row
1326
1327 B<Value:> Color specifier as 'name' or 'HEX'
1328 B<Default:> #FFFFAA
1329
1330 =item B<repeat> - Flag showing if header row should be repeated on every new page
1331
1332 B<Value:> 0,1   1-Yes/True, 0-No/False
1333 B<Default:> 0
1334
1335 =item B<justify> - Alignment of text in the header row.
1336
1337 B<Value:> One of 'left', 'right', 'center'
1338 B<Default:> Same as column alignment (or 'left' if undefined)
1339
1340     my $hdr_props =
1341     {
1342         font       => $pdf->corefont("Helvetica", -encoding => "utf8"),
1343         font_size  => 18,
1344         font_color => '#004444',
1345         bg_color   => 'yellow',
1346         repeat     => 1,
1347         justify    => 'center'
1348     };
1349
1350 =back
1351
1352 =head4 Column Properties
1353
1354 If the 'column_props' parameter is used, it should be an arrayref of hashrefs,
1355 with one hashref for each column of the table. The columns are counted from left to right so the hash reference at $col_props[0] will hold properties for the first column from left to right.
1356 If you DO NOT want to give properties for a column but to give for another just insert and empty hash reference into the array for the column that you want to skip. This will cause the counting to proceed as expected and the properties to be applyed at the right columns.
1357
1358 Each hashref can contain any of the keys shown below:
1359
1360 =over
1361
1362 =item B<min_w> - Minimum width of this column. Auto calculation will try its best to honour this param but aplying it is NOT guaranteed.
1363
1364 B<Value:> can be any whole number satisfying 0 < min_w < w
1365 B<Default:> Auto calculated
1366
1367 =item B<max_w> - Maximum width of this column. Auto calculation will try its best to honour this param but aplying it is NOT guaranteed.
1368
1369 B<Value:> can be any whole number satisfying 0 < max_w < w
1370 B<Default:> Auto calculated
1371
1372 =item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in this column
1373
1374 B<Value:> can be any PDF::API2::Resource::* type of font
1375 B<Default:> 'font' of the table. See table parameter 'font' for more details.
1376
1377 =item B<font_size> - Font size of this column
1378
1379 B<Value:> can be any positive number
1380 B<Default:> 'font_size' of the table.
1381
1382 =item B<font_color> - Font color of this column
1383
1384 B<Value:> Color specifier as 'name' or 'HEX'
1385 B<Default:> 'font_color' of the table.
1386
1387 =item B<font_underline> - Font underline of this cell
1388
1389 B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
1390 B<Default:> none
1391
1392 =item B<background_color> - Background color of this column
1393
1394 B<Value:> Color specifier as 'name' or 'HEX'
1395 B<Default:> undef
1396
1397 =item B<justify> - Alignment of text in this column
1398
1399 B<Value:> One of 'left', 'right', 'center'
1400 B<Default:> 'left'
1401
1402 Example:
1403
1404     my $col_props = [
1405         {},# This is an empty hash so the next one will hold the properties for the second column from left to right.
1406         {
1407             min_w => 100,       # Minimum column width of 100.
1408             max_w => 150,       # Maximum column width of 150 .
1409             justify => 'right', # Right text alignment
1410             font => $pdf->corefont("Helvetica", -encoding => "latin1"),
1411             font_size => 10,
1412             font_color=> 'blue',
1413             background_color => '#FFFF00',
1414         },
1415         # etc.
1416     ];
1417
1418 =back
1419
1420 NOTE: If 'min_w' and/or 'max_w' parameter is used in 'col_props', have in mind that it may be overridden by the calculated minimum/maximum cell witdh so that table can be created.
1421 When this happens a warning will be issued with some advises what can be done.
1422 In cases of a conflict between column formatting and odd/even row formatting, 'col_props' will override odd/even.
1423
1424 =head4 Cell Properties
1425
1426 If the 'cell_props' parameter is used, it should be an arrayref with arrays of hashrefs
1427 (of the same dimension as the data array) with one hashref for each cell of the table.
1428
1429 Each hashref can contain any of the keys shown below:
1430
1431 =over
1432
1433 =item B<font> - instance of PDF::API2::Resource::Font defining the fontf to be used in this cell
1434
1435 B<Value:> can be any PDF::API2::Resource::* type of font
1436 B<Default:> 'font' of the table. See table parameter 'font' for more details.
1437
1438 =item B<font_size> - Font size of this cell
1439
1440 B<Value:> can be any positive number
1441 B<Default:> 'font_size' of the table.
1442
1443 =item B<font_color> - Font color of this cell
1444
1445 B<Value:> Color specifier as 'name' or 'HEX'
1446 B<Default:> 'font_color' of the table.
1447
1448 =item B<font_underline> - Font underline of this cell
1449
1450 B<Value:> 'auto', integer of distance, or arrayref of distance & thickness (more than one pair will provide mlultiple underlines. Negative distance gives strike-through.
1451 B<Default:> none
1452
1453 =item B<background_color> - Background color of this cell
1454
1455 B<Value:> Color specifier as 'name' or 'HEX'
1456 B<Default:> undef
1457
1458 =item B<justify> - Alignment of text in this cell
1459
1460 B<Value:> One of 'left', 'right', 'center'
1461 B<Default:> 'left'
1462
1463 Example:
1464
1465     my $cell_props = [
1466         [ #This array is for the first row. If header_props is defined it will overwrite these settings.
1467             {    #Row 1 cell 1
1468                 background_color => '#AAAA00',
1469                 font_color       => 'yellow',
1470                 font_underline   => [ 2, 2 ],
1471             },
1472
1473             # etc.
1474         ],
1475         [#Row 2
1476             {    #Row 2 cell 1
1477                 background_color => '#CCCC00',
1478                 font_color       => 'blue',
1479             },
1480             {    #Row 2 cell 2
1481                 background_color => '#BBBB00',
1482                 font_color       => 'red',
1483             },
1484             # etc.
1485         ],
1486         # etc.
1487     ];
1488
1489     OR
1490
1491     my $cell_props = [];
1492     $cell_props->[1][0] = {
1493         #Row 2 cell 1
1494         background_color => '#CCCC00',
1495         font_color       => 'blue',
1496     };
1497
1498 =back
1499
1500 NOTE: In case of a conflict between column, odd/even and cell formatting, cell formatting will overwrite the other two.
1501 In case of a conflict between header row and cell formatting, header formatting will override cell.
1502
1503 =head2 text_block()
1504
1505     my ($width_of_last_line, $ypos_of_last_line, $left_over_text) = text_block( $txt, $data, %settings)
1506
1507 =over
1508
1509 =item Description
1510
1511 Utility method to create a block of text. The block may contain multiple paragraphs.
1512 It is mainly used internaly but you can use it from outside for placing formatted text anywhere on the sheet.
1513
1514 NOTE: This method will NOT add more pages to the pdf instance if the space is not enough to place the string inside the block.
1515 Leftover text will be returned and has to be handled by the caller - i.e. add a new page and a new block with the leftover.
1516
1517 =item Parameters
1518
1519     $txt  - a PDF::API2::Page::Text instance representing the text tool
1520     $data - a string that will be placed inside the block
1521     %settings - HASH with geometry and formatting parameters.
1522
1523 =item Reuturns
1524
1525 The return value is a 3 items list where
1526
1527     $width_of_last_line - Width of last line in the block
1528     $final_y - The Y coordinate of the block bottom so that additional content can be added after it
1529     $left_over_text - Text that was did not fit in the provided box geometry.
1530
1531 =item Example
1532
1533     # PDF::API2 objects
1534     my $page = $pdf->page;
1535     my $txt  = $page->text;
1536
1537     my %settings = (
1538         x => 10,
1539         y => 570,
1540         w => 220,
1541         h => 180
1542
1543         #OPTIONAL PARAMS
1544         lead     => $font_size | $distance_between_lines,
1545         align    => "left|right|center|justify|fulljustify",
1546         hang     => $optional_hanging_indent,
1547         Only one of the subsequent 3params can be given.
1548         They override each other.-parspace is the weightest
1549         parspace => $optional_vertical_space_before_first_paragraph,
1550         flindent => $optional_indent_of_first_line,
1551         fpindent => $optional_indent_of_first_paragraph,
1552         indent   => $optional_indent_of_text_to_every_non_first_line,
1553     );
1554
1555     my ( $width_of_last_line, $final_y, $left_over_text ) = $pdftable->text_block( $txt, $data, %settings );
1556
1557 =back
1558
1559 =head1 VERSION
1560
1561 0.9.7
1562
1563 =head1 AUTHOR
1564
1565 Daemmon Hughes
1566
1567 =head1 DEVELOPMENT
1568
1569 Further development since Ver: 0.02 - Desislav Kamenov
1570
1571 =head1 COPYRIGHT AND LICENSE
1572
1573 Copyright (C) 2006 by Daemmon Hughes, portions Copyright 2004 Stone
1574 Environmental Inc. (www.stone-env.com) All Rights Reserved.
1575
1576 This library is free software; you can redistribute it and/or modify
1577 it under the same terms as Perl itself, either Perl version 5.8.4 or,
1578 at your option, any later version of Perl 5 you may have available.
1579
1580 =head1 PLUGS
1581
1582 =over
1583
1584 =item by Daemmon Hughes
1585
1586 Much of the work on this module was sponsered by
1587 Stone Environmental Inc. (www.stone-env.com).
1588
1589 The text_block() method is a slightly modified copy of the one from
1590 Rick Measham's PDF::API2 tutorial at
1591 http://pdfapi2.sourceforge.net/cgi-bin/view/Main/YourFirstDocument
1592
1593 =item by Desislav Kamenov (@deskata on Twitter)
1594
1595 The development of this module was supported by SEEBURGER AG (www.seeburger.com) till year 2007
1596
1597 Thanks to my friends Krasimir Berov and Alex Kantchev for helpful tips and QA during development of versions 0.9.0 to 0.9.5
1598
1599 Thanks to all GitHub contributors!
1600
1601 =back
1602
1603 =head1 CONTRIBUTION
1604
1605 Hey PDF::Table is on GitHub. You are more than welcome to contribute!
1606
1607 https://github.com/kamenov/PDF-Table
1608
1609 =head1 SEE ALSO
1610
1611 L<PDF::API2>
1612
1613 =cut
1614