Unterstützung für zellenübergreifende Überschriften im PDF-Export des ReportGenerator...
[kivitendo-erp.git] / SL / ReportGenerator.pm
1 package SL::ReportGenerator;
2
3 use IO::Wrap;
4 use List::Util qw(max);
5 use Text::CSV_XS;
6 use Text::Iconv;
7 use PDF::Table;
8
9 use SL::Form;
10
11 # Cause locales.pl to parse these files:
12 # parse_html_template('report_generator/html_report')
13 # parse_html_template('report_generator/pdf_report')
14
15 sub new {
16   my $type = shift;
17
18   my $self = { };
19
20   $self->{myconfig} = shift;
21   $self->{form}     = shift;
22
23   $self->{data}     = [];
24   $self->{options}  = {
25     'std_column_visibility' => 0,
26     'output_format'         => 'HTML',
27     'allow_pdf_export'      => 1,
28     'allow_csv_export'      => 1,
29     'html_template'         => 'report_generator/html_report',
30     'pdf_template'          => 'report_generator/pdf_report',
31     'pdf_export'            => {
32       'paper_size'          => 'a4',
33       'orientation'         => 'landscape',
34       'font_name'           => 'Verdana',
35       'font_size'           => '7',
36       'margin_top'          => 1.5,
37       'margin_left'         => 1.5,
38       'margin_bottom'       => 1.5,
39       'margin_right'        => 1.5,
40       'number'              => 1,
41       'print'               => 0,
42       'printer_id'          => 0,
43       'copies'              => 1,
44     },
45     'csv_export'            => {
46       'quote_char'          => '"',
47       'sep_char'            => ';',
48       'escape_char'         => '"',
49       'eol_style'           => 'Unix',
50       'headers'             => 1,
51     },
52   };
53   $self->{export}   = {
54     'nextsub'       => '',
55     'variable_list' => [],
56   };
57
58   $self->{data_present} = 0;
59
60   bless $self, $type;
61
62   $self->set_options(@_) if (@_);
63
64   $self->_init_escaped_strings_map();
65
66   return $self;
67 }
68
69 sub _init_escaped_strings_map {
70   my $self = shift;
71
72   $self->{escaped_strings_map} = {
73     'ä'  => 'ä',
74     'ö'  => 'ö',
75     'ü'  => 'ü',
76     'Ä'  => 'Ä',
77     'Ö'  => 'Ö',
78     'Ü'  => 'Ü',
79     'ß' => 'ß',
80     '>'    => '>',
81      '&lt;'    => '<',
82     '&quot;'  => '"',
83   };
84
85   my $iconv = $main::locale->{iconv_iso8859};
86
87   if ($iconv) {
88     map { $self->{escaped_strings_map}->{$_} = $iconv->convert($self->{escaped_strings_map}->{$_}) } keys %{ $self->{escaped_strings_map} };
89   }
90 }
91
92 sub set_columns {
93   my $self    = shift;
94   my %columns = @_;
95
96   $self->{columns} = \%columns;
97
98   foreach my $column (values %{ $self->{columns} }) {
99     $column->{visible} = $self->{options}->{std_column_visibility} unless defined $column->{visible};
100   }
101
102   $self->set_column_order(sort keys %{ $self->{columns} });
103 }
104
105 sub set_column_order {
106   my $self    = shift;
107
108   my $order   = 0;
109   my %columns = map { $order++; ($_, $order) } @_;
110
111   foreach my $column (sort keys %{ $self->{columns} }) {
112     next if $columns{$column};
113
114     $order++;
115     $columns{$column} = $order;
116   }
117
118   $self->{column_order} = [ sort { $columns{$a} <=> $columns{$b} } keys %columns ];
119 }
120
121 sub set_sort_indicator {
122   my $self = shift;
123
124   $self->{options}->{sort_indicator_column}    = shift;
125   $self->{options}->{sort_indicator_direction} = shift;
126 }
127
128 sub add_data {
129   my $self = shift;
130
131   my $last_row_set;
132
133   while (my $arg = shift) {
134     my $row_set;
135
136     if ('ARRAY' eq ref $arg) {
137       $row_set = $arg;
138
139     } elsif ('HASH' eq ref $arg) {
140       $row_set = [ $arg ];
141
142     } else {
143       $self->{form}->error('Incorrect usage -- expecting hash or array ref');
144     }
145
146     my @columns_with_default_alignment = grep { defined $self->{columns}->{$_}->{align} } keys %{ $self->{columns} };
147
148     foreach my $row (@{ $row_set }) {
149       foreach my $column (@columns_with_default_alignment) {
150         $row->{$column}          ||= { };
151         $row->{$column}->{align}   = $self->{columns}->{$column}->{align} unless (defined $row->{$column}->{align});
152       }
153
154       foreach my $field (qw(data link)) {
155         map { $row->{$_}->{$field} = [ $row->{$_}->{$field} ] if (ref $row->{$_}->{$field} ne 'ARRAY') } keys %{ $row };
156       }
157     }
158
159     push @{ $self->{data} }, $row_set;
160     $last_row_set = $row_set;
161
162     $self->{data_present} = 1;
163   }
164
165   return $last_row_set;
166 }
167
168 sub add_separator {
169   my $self = shift;
170
171   push @{ $self->{data} }, { 'type' => 'separator' };
172 }
173
174 sub add_control {
175   my $self = shift;
176   my $data = shift;
177
178   push @{ $self->{data} }, $data;
179 }
180
181 sub clear_data {
182   my $self = shift;
183
184   $self->{data}         = [];
185   $self->{data_present} = 0;
186 }
187
188 sub set_options {
189   my $self    = shift;
190   my %options = @_;
191
192   while (my ($key, $value) = each %options) {
193     if ($key eq 'pdf_export') {
194       map { $self->{options}->{pdf_export}->{$_} = $value->{$_} } keys %{ $value };
195     } else {
196       $self->{options}->{$key} = $value;
197     }
198   }
199 }
200
201 sub set_options_from_form {
202   my $self     = shift;
203
204   my $form     = $self->{form};
205   my $myconfig = $self->{myconfig};
206
207   foreach my $key (qw(output_format)) {
208     my $full_key = "report_generator_${key}";
209     $self->{options}->{$key} = $form->{$full_key} if (defined $form->{$full_key});
210   }
211
212   foreach my $format (qw(pdf csv)) {
213     my $opts = $self->{options}->{"${format}_export"};
214     foreach my $key (keys %{ $opts }) {
215       my $full_key = "report_generator_${format}_options_${key}";
216       $opts->{$key} = $key =~ /^margin/ ? $form->parse_amount($myconfig, $form->{$full_key}) : $form->{$full_key};
217     }
218   }
219 }
220
221 sub set_export_options {
222   my $self        = shift;
223
224   $self->{export} = {
225     'nextsub'       => shift,
226     'variable_list' => [ @_ ],
227   };
228 }
229
230 sub get_attachment_basename {
231   my $self     = shift;
232   my $filename =  $self->{options}->{attachment_basename} || 'report';
233   $filename    =~ s|.*\\||;
234   $filename    =~ s|.*/||;
235
236   return $filename;
237 }
238
239 sub generate_with_headers {
240   my $self   = shift;
241   my $format = lc $self->{options}->{output_format};
242   my $form   = $self->{form};
243
244   if (!$self->{columns}) {
245     $form->error('Incorrect usage -- no columns specified');
246   }
247
248   if ($format eq 'html') {
249     my $title      = $form->{title};
250     $form->{title} = $self->{title} if ($self->{title});
251     $form->header();
252     $form->{title} = $title;
253
254     print $self->generate_html_content();
255
256   } elsif ($format eq 'csv') {
257     my $filename = $self->get_attachment_basename();
258     print qq|content-type: text/csv\n|;
259     print qq|content-disposition: attachment; filename=${filename}.csv\n\n|;
260     $self->generate_csv_content();
261
262   } elsif ($format eq 'pdf') {
263     $self->generate_pdf_content();
264
265   } else {
266     $form->error('Incorrect usage -- unknown format (supported are HTML, CSV, PDF)');
267   }
268 }
269
270 sub get_visible_columns {
271   my $self   = shift;
272   my $format = shift;
273
274   return grep { my $c = $self->{columns}->{$_}; $c && $c->{visible} && (($c->{visible} == 1) || ($c->{visible} =~ /\Q${format}\E/i)) } @{ $self->{column_order} };
275 }
276
277 sub html_format {
278   my $self  = shift;
279   my $value = shift;
280
281   $value =  $self->{form}->quote_html($value);
282   $value =~ s/\r//g;
283   $value =~ s/\n/<br>/g;
284
285   return $value;
286 }
287
288 sub prepare_html_content {
289   my $self = shift;
290
291   my ($column, $name, @column_headers);
292
293   my $opts            = $self->{options};
294   my @visible_columns = $self->get_visible_columns('HTML');
295
296   foreach $name (@visible_columns) {
297     $column = $self->{columns}->{$name};
298
299     my $header = {
300       'name'                     => $name,
301       'link'                     => $column->{link},
302       'text'                     => $column->{text},
303       'show_sort_indicator'      => $name eq $opts->{sort_indicator_column},
304       'sort_indicator_direction' => $opts->{sort_indicator_direction},
305     };
306
307     push @column_headers, $header;
308   }
309
310   my ($outer_idx, $inner_idx) = (0, 0);
311   my $next_border_top;
312   my @rows;
313
314   foreach my $row_set (@{ $self->{data} }) {
315     if ('HASH' eq ref $row_set) {
316       if ($row_set->{type} eq 'separator') {
317         if (! scalar @rows) {
318           $next_border_top = 1;
319         } else {
320           $rows[-1]->{BORDER_BOTTOM} = 1;
321         }
322
323         next;
324       }
325
326       my $row_data = {
327         'IS_CONTROL'      => 1,
328         'IS_COLSPAN_DATA' => $row_set->{type} eq 'colspan_data',
329         'NUM_COLUMNS'     => scalar @visible_columns,
330         'BORDER_TOP'      => $next_border_top,
331         'data'            => $row_set->{data},
332       };
333
334       push @rows, $row_data;
335
336       $next_border_top = 0;
337
338       next;
339     }
340
341     $outer_idx++;
342
343     foreach my $row (@{ $row_set }) {
344       $inner_idx++;
345
346       foreach my $col_name (@visible_columns) {
347         my $col = $row->{$col_name};
348         $col->{CELL_ROWS} = [ ];
349         foreach my $i (0 .. scalar(@{ $col->{data} }) - 1) {
350           push @{ $col->{CELL_ROWS} }, {
351             'data' => $self->html_format($col->{data}->[$i]),
352             'link' => $col->{link}->[$i],
353           };
354         }
355
356         # Force at least a &nbsp; to be displayed so that browsers
357         # will format the table cell (e.g. borders etc).
358         if (!scalar @{ $col->{CELL_ROWS} }) {
359           push @{ $col->{CELL_ROWS} }, { 'data' => '&nbsp;' };
360         } elsif ((1 == scalar @{ $col->{CELL_ROWS} }) && (!defined $col->{CELL_ROWS}->[0]->{data} || ($col->{CELL_ROWS}->[0]->{data} eq ''))) {
361           $col->{CELL_ROWS}->[0]->{data} = '&nbsp;';
362         }
363       }
364
365       my $row_data = {
366         'COLUMNS'       => [ map { $row->{$_} } @visible_columns ],
367         'outer_idx'     => $outer_idx,
368         'outer_idx_odd' => $outer_idx % 2,
369         'inner_idx'     => $inner_idx,
370         'BORDER_TOP'    => $next_border_top,
371       };
372
373       push @rows, $row_data;
374
375       $next_border_top = 0;
376     }
377   }
378
379   my @export_variables = $self->{form}->flatten_variables(@{ $self->{export}->{variable_list} });
380
381   my $allow_pdf_export = $opts->{allow_pdf_export} && (-x $main::html2ps_bin) && (-x $main::ghostscript_bin);
382
383   eval { require PDF::API2; require PDF::Table; };
384   $allow_pdf_export |= 1 if (! $@);
385
386   my $variables = {
387     'TITLE'                => $opts->{title},
388     'TOP_INFO_TEXT'        => $self->html_format($opts->{top_info_text}),
389     'RAW_TOP_INFO_TEXT'    => $opts->{raw_top_info_text},
390     'BOTTOM_INFO_TEXT'     => $self->html_format($opts->{bottom_info_text}),
391     'RAW_BOTTOM_INFO_TEXT' => $opts->{raw_bottom_info_text},
392     'ALLOW_PDF_EXPORT'     => $allow_pdf_export,
393     'ALLOW_CSV_EXPORT'     => $opts->{allow_csv_export},
394     'SHOW_EXPORT_BUTTONS'  => ($allow_pdf_export || $opts->{allow_csv_export}) && $self->{data_present},
395     'COLUMN_HEADERS'       => \@column_headers,
396     'NUM_COLUMNS'          => scalar @column_headers,
397     'ROWS'                 => \@rows,
398     'EXPORT_VARIABLES'     => \@export_variables,
399     'EXPORT_VARIABLE_LIST' => join(' ', @{ $self->{export}->{variable_list} }),
400     'EXPORT_NEXTSUB'       => $self->{export}->{nextsub},
401     'DATA_PRESENT'         => $self->{data_present},
402   };
403
404   return $variables;
405 }
406
407 sub generate_html_content {
408   my $self      = shift;
409   my $variables = $self->prepare_html_content();
410
411   return $self->{form}->parse_html_template($self->{options}->{html_template}, $variables);
412 }
413
414 sub _cm2bp {
415   # 1 bp = 1/72 in
416   # 1 in = 2.54 cm
417   return $_[0] * 72 / 2.54;
418 }
419
420 sub render_pdf_with_pdf_api2 {
421   my $self       = shift;
422   my $variables  = $self->prepare_html_content();
423   my $form       = $self->{form};
424   my $myconfig   = $self->{myconfig};
425
426   my $opts       = $self->{options};
427   my $pdfopts    = $opts->{pdf_export};
428
429   my (@data, @column_props, @cell_props);
430
431   my $data_row        = [];
432   my $cell_props_row  = [];
433   my @visible_columns = $self->get_visible_columns('HTML');
434
435   foreach $name (@visible_columns) {
436     $column = $self->{columns}->{$name};
437
438     push @{ $data_row },       $column->{text};
439     push @{ $cell_props_row }, {};
440     push @column_props,        { 'justify' => $column->{align} eq 'right' ? 'right' : 'left' };
441   }
442
443   push @data,       $data_row;
444   push @cell_props, $cell_props_row;
445
446   my $num_columns = scalar @column_props;
447
448   foreach my $row_set (@{ $self->{data} }) {
449     if ('HASH' eq ref $row_set) {
450       if ($row_set->{type} eq 'colspan_data') {
451         push @data, [ $row_set->{data} ];
452
453         $cell_props_row = [];
454         push @cell_props, $cell_props_row;
455
456         foreach (0 .. $num_columns - 1) {
457           push @{ $cell_props_row }, { 'background_color' => '#666666',
458                                        'font_color'       => '#ffffff',
459                                        'colspan'          => $_ == 0 ? -1 : undef, };
460         }
461       }
462       next;
463     }
464
465     foreach my $row (@{ $row_set }) {
466       $data_row = [];
467       push @data, $data_row;
468
469       my $col_idx = 0;
470       foreach my $col_name (@visible_columns) {
471         my $col = $row->{$col_name};
472         push @{ $data_row }, join("\n", @{ $col->{data} });
473
474         $column_props[$col_idx]->{justify} = 'right' if ($col->{align} eq 'right');
475
476         $col_idx++;
477       }
478
479       $cell_props_row = [];
480       push @cell_props, $cell_props_row;
481
482       foreach (0 .. $num_columns - 1) {
483         push @{ $cell_props_row }, { };
484       }
485     }
486   }
487
488   foreach my $i (0 .. scalar(@data) - 1) {
489     my $aref             = $data[$i];
490     my $num_columns_here = scalar @{ $aref };
491
492     if ($num_columns_here < $num_columns) {
493       push @{ $aref }, ('') x ($num_columns - $num_columns_here);
494     } elsif ($num_columns_here > $num_columns) {
495       splice @{ $aref }, $num_columns;
496     }
497   }
498
499   my $papersizes = {
500     'a3'         => [ 842, 1190 ],
501     'a4'         => [ 595,  842 ],
502     'a5'         => [ 420,  595 ],
503     'letter'     => [ 612,  792 ],
504     'legal'      => [ 612, 1008 ],
505   };
506
507   my %supported_fonts = map { $_ => 1 } qw(courier georgia helvetica times verdana);
508
509   my $paper_size  = defined $pdfopts->{paper_size} && defined $papersizes->{lc $pdfopts->{paper_size}} ? lc $pdfopts->{paper_size} : 'a4';
510   my ($paper_width, $paper_height);
511
512   if (lc $pdfopts->{orientation} eq 'landscape') {
513     ($paper_width, $paper_height) = @{$papersizes->{$paper_size}}[1, 0];
514   } else {
515     ($paper_width, $paper_height) = @{$papersizes->{$paper_size}}[0, 1];
516   }
517
518   my $margin_top        = _cm2bp($pdfopts->{margin_top}    || 1.5);
519   my $margin_bottom     = _cm2bp($pdfopts->{margin_bottom} || 1.5);
520   my $margin_left       = _cm2bp($pdfopts->{margin_left}   || 1.5);
521   my $margin_right      = _cm2bp($pdfopts->{margin_right}  || 1.5);
522
523   my $table             = PDF::Table->new();
524   my $pdf               = PDF::API2->new();
525   my $page              = $pdf->page();
526
527   $pdf->mediabox($paper_width, $paper_height);
528
529   my $font              = $pdf->corefont(defined $pdfopts->{font_name} && $supported_fonts{lc $pdfopts->{font_name}} ? ucfirst $pdfopts->{font_name} : 'Verdana',
530                                          '-encoding' => $main::dbcharset || 'ISO-8859-15');
531   my $font_size         = $pdfopts->{font_size} || 7;
532   my $title_font_size   = $font_size + 1;
533   my $padding           = 1;
534   my $font_height       = $font_size + 2 * $padding;
535   my $title_font_height = $font_size + 2 * $padding;
536
537   my $header_height     = 2 * $title_font_height if ($opts->{title});
538   my $footer_height     = 2 * $font_height       if ($pdfopts->{number});
539
540   my $top_text_height   = 0;
541
542   if ($self->{options}->{top_info_text}) {
543     my $top_text     =  $self->{options}->{top_info_text};
544     $top_text        =~ s/\r//g;
545     $top_text        =~ s/\n+$//;
546
547     my @lines        =  split m/\n/, $top_text;
548     $top_text_height =  $font_height * scalar @lines;
549
550     foreach my $line_no (0 .. scalar(@lines) - 1) {
551       my $y_pos    = $paper_height - $margin_top - $header_height - $line_no * $font_height;
552       my $text_obj = $page->text();
553
554       $text_obj->font($font, $font_size);
555       $text_obj->translate($margin_left, $y_pos);
556       $text_obj->text($lines[$line_no]);
557     }
558   }
559
560   $table->table($pdf,
561                 $page,
562                 \@data,
563                 'x'                     => $margin_left,
564                 'w'                     => $paper_width - $margin_left - $margin_right,
565                 'start_y'               => $paper_height - $margin_top                  - $header_height                  - $top_text_height,
566                 'next_y'                => $paper_height - $margin_top                  - $header_height,
567                 'start_h'               => $paper_height - $margin_top - $margin_bottom - $header_height - $footer_height - $top_text_height,
568                 'next_h'                => $paper_height - $margin_top - $margin_bottom - $header_height - $footer_height,
569                 'padding'               => 1,
570                 'background_color_odd'  => '#ffffff',
571                 'background_color_even' => '#eeeeee',
572                 'font'                  => $font,
573                 'font_size'             => $font_size,
574                 'font_color'            => '#000000',
575                 'header_props'          => {
576                   'bg_color'            => '#ffffff',
577                   'repeat'              => 1,
578                   'font_color'          => '#000000',
579                 },
580                 'column_props'          => \@column_props,
581                 'cell_props'            => \@cell_props,
582     );
583
584   foreach my $page_num (1..$pdf->pages()) {
585     my $curpage  = $pdf->openpage($page_num);
586
587     if ($pdfopts->{number}) {
588       my $label    = $main::locale->text("Page #1/#2", $page_num, $pdf->pages());
589       my $text_obj = $curpage->text();
590
591       $text_obj->font($font, $font_size);
592       $text_obj->translate(($paper_width - $margin_left - $margin_right) / 2 + $margin_left - $text_obj->advancewidth($label) / 2, $margin_bottom);
593       $text_obj->text($label);
594     }
595
596     if ($opts->{title}) {
597       my $text_obj = $curpage->text();
598
599       $text_obj->font($font, $title_font_size);
600       $text_obj->translate(($paper_width - $margin_left - $margin_right) / 2 + $margin_left - $text_obj->advancewidth($opts->{title}) / 2,
601                            $paper_height - $margin_top);
602       $text_obj->text($opts->{title}, '-underline' => 1);
603     }
604   }
605
606   my $content = $pdf->stringify();
607
608   my $printer_command;
609   if ($pdfopts->{print} && $pdfopts->{printer_id}) {
610     $form->{printer_id} = $pdfopts->{printer_id};
611     $form->get_printer_code($myconfig);
612     $printer_command = $form->{printer_command};
613   }
614
615   if ($printer_command) {
616     $self->_print_content('printer_command' => $printer_command,
617                           'content'         => $content,
618                           'copies'          => $pdfopts->{copies});
619     $form->{report_generator_printed} = 1;
620
621   } else {
622     my $filename = $self->get_attachment_basename();
623
624     print qq|content-type: application/pdf\n|;
625     print qq|content-disposition: attachment; filename=${filename}.pdf\n\n|;
626
627     print $content;
628   }
629 }
630
631 sub verify_paper_size {
632   my $self                 = shift;
633   my $requested_paper_size = lc shift;
634   my $default_paper_size   = shift;
635
636   my %allowed_paper_sizes  = map { $_ => 1 } qw(a3 a4 a5 letter legal);
637
638   return $allowed_paper_sizes{$requested_paper_size} ? $requested_paper_size : $default_paper_size;
639 }
640
641 sub render_pdf_with_html2ps {
642   my $self      = shift;
643   my $variables = $self->prepare_html_content();
644   my $form      = $self->{form};
645   my $myconfig  = $self->{myconfig};
646   my $opt       = $self->{options}->{pdf_export};
647
648   my $opt_number     = $opt->{number}                     ? 'number : 1'    : '';
649   my $opt_landscape  = $opt->{orientation} eq 'landscape' ? 'landscape : 1' : '';
650
651   my $opt_paper_size = $self->verify_paper_size($opt->{paper_size}, 'a4');
652
653   my $html2ps_config = <<"END"
654 \@html2ps {
655   option {
656     titlepage: 0;
657     hyphenate: 0;
658     colour: 1;
659     ${opt_landscape};
660     ${opt_number};
661   }
662   paper {
663     type: ${opt_paper_size};
664   }
665   break-table: 1;
666 }
667
668 \@page {
669   margin-top:    $opt->{margin_top}cm;
670   margin-left:   $opt->{margin_left}cm;
671   margin-bottom: $opt->{margin_bottom}cm;
672   margin-right:  $opt->{margin_right}cm;
673 }
674
675 BODY {
676   font-family: Helvetica;
677   font-size:   $opt->{font_size}pt;
678 }
679
680 END
681   ;
682
683   my $printer_command;
684   if ($opt->{print} && $opt->{printer_id}) {
685     $form->{printer_id} = $opt->{printer_id};
686     $form->get_printer_code($myconfig);
687     $printer_command = $form->{printer_command};
688   }
689
690   my $cfg_file_name = Common::tmpname() . '-html2ps-config';
691   my $cfg_file      = IO::File->new($cfg_file_name, 'w') || $form->error($locale->text('Could not write the html2ps config file.'));
692
693   $cfg_file->print($html2ps_config);
694   $cfg_file->close();
695
696   my $html_file_name = Common::tmpname() . '.html';
697   my $html_file      = IO::File->new($html_file_name, 'w');
698
699   if (!$html_file) {
700     unlink $cfg_file_name;
701     $form->error($locale->text('Could not write the temporary HTML file.'));
702   }
703
704   $html_file->print($form->parse_html_template($self->{options}->{pdf_template}, $variables));
705   $html_file->close();
706
707   my $cmdline =
708     "\"${main::html2ps_bin}\" -f \"${cfg_file_name}\" \"${html_file_name}\" | " .
709     "\"${main::ghostscript_bin}\" -q -dSAFER -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sPAPERSIZE=${opt_paper_size} -sOutputFile=- -c .setpdfwrite -";
710
711   my $gs = IO::File->new("${cmdline} |");
712   if ($gs) {
713     my $content;
714
715     if (!$printer_command) {
716       my $filename = $self->get_attachment_basename();
717       print qq|content-type: application/pdf\n|;
718       print qq|content-disposition: attachment; filename=${filename}.pdf\n\n|;
719
720       while (my $line = <$gs>) {
721         print $line;
722       }
723
724     } else {
725       while (my $line = <$gs>) {
726         $content .= $line;
727       }
728     }
729
730     $gs->close();
731     unlink $cfg_file_name, $html_file_name;
732
733     if ($printer_command && $content) {
734       $self->_print_content('printer_command' => $printer_command,
735                             'content'         => $content,
736                             'copies'          => $opt->{copies});
737       $form->{report_generator_printed} = 1;
738     }
739
740   } else {
741     unlink $cfg_file_name, $html_file_name;
742     $form->error($locale->text('Could not spawn html2ps or GhostScript.'));
743   }
744 }
745
746 sub _print_content {
747   my $self   = shift;
748   my %params = @_;
749
750   foreach my $i (1 .. max $params{copies}, 1) {
751     my $printer = IO::File->new("| $params{printer_command}");
752     $main::form->error($main::locale->text('Could not spawn the printer command.')) if (!$printer);
753     $printer->print($params{content});
754     $printer->close();
755   }
756 }
757
758 sub generate_pdf_content {
759   my $self = shift;
760
761   eval { require PDF::API2; require PDF::Table; };
762
763   if ($@) {
764     return $self->render_pdf_with_html2ps(@_);
765   } else {
766     return $self->render_pdf_with_pdf_api2(@_);
767   }
768 }
769
770 sub unescape_string {
771   my $self = shift;
772   my $text = shift;
773
774   foreach my $key (keys %{ $self->{escaped_strings_map} }) {
775     $text =~ s/\Q$key\E/$self->{escaped_strings_map}->{$key}/g;
776   }
777
778   $text =~ s/\Q&amp;\E/&/g;
779
780   return $text;
781 }
782
783 sub generate_csv_content {
784   my $self = shift;
785
786   my %valid_sep_chars    = (';' => ';', ',' => ',', ':' => ':', 'TAB' => "\t");
787   my %valid_escape_chars = ('"' => 1, "'" => 1);
788   my %valid_quote_chars  = ('"' => 1, "'" => 1);
789
790   my $opts        = $self->{options}->{csv_export};
791   my $eol         = $opts->{eol_style} eq 'DOS'               ? "\r\n"                              : "\n";
792   my $sep_char    = $valid_sep_chars{$opts->{sep_char}}       ? $valid_sep_chars{$opts->{sep_char}} : ';';
793   my $escape_char = $valid_escape_chars{$opts->{escape_char}} ? $opts->{escape_char}                : '"';
794   my $quote_char  = $valid_quote_chars{$opts->{quote_char}}   ? $opts->{quote_char}                 : '"';
795
796   $escape_char    = $quote_char if ($opts->{escape_char} eq 'QUOTE_CHAR');
797
798   my $csv = Text::CSV_XS->new({ 'binary'      => 1,
799                                 'sep_char'    => $sep_char,
800                                 'escape_char' => $escape_char,
801                                 'quote_char'  => $quote_char,
802                                 'eol'         => $eol, });
803
804   my $stdout          = wraphandle(\*STDOUT);
805   my @visible_columns = $self->get_visible_columns('CSV');
806
807   if ($opts->{headers}) {
808     $csv->print($stdout, [ map { $self->unescape_string($self->{columns}->{$_}->{text}) } @visible_columns ]);
809   }
810
811   foreach my $row_set (@{ $self->{data} }) {
812     next if ('ARRAY' ne ref $row_set);
813     foreach my $row (@{ $row_set }) {
814       my @data;
815       foreach my $col (@visible_columns) {
816         push @data, join($eol, map { s/\r?\n/$eol/g; $_ } @{ $row->{$col}->{data} });
817       }
818       $csv->print($stdout, \@data);
819     }
820   }
821 }
822
823 1;