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