d99afa43ccc1a37cb9aa0fcd6116af612b46d4c6
[kivitendo-erp.git] / SL / Template / LaTeX.pm
1 package SL::Template::LaTeX;
2
3 use parent qw(SL::Template::Simple);
4
5 use strict;
6
7 use Carp;
8 use Cwd;
9 use English qw(-no_match_vars);
10 use File::Basename;
11 use File::Temp;
12 use List::MoreUtils qw(any);
13 use Unicode::Normalize qw();
14
15 use SL::DB::Default;
16
17 sub new {
18   my $type = shift;
19
20   my $self = $type->SUPER::new(@_);
21
22   return $self;
23 }
24
25 sub format_string {
26   my ($self, $variable) = @_;
27
28   $variable = $main::locale->quote_special_chars('Template/LaTeX', $variable);
29
30   # Allow some HTML markup to be converted into the output format's
31   # corresponding markup code, e.g. bold or italic.
32   my %markup_replace = ('b' => 'textbf',
33                         'i' => 'textit',
34                         'u' => 'underline');
35
36   foreach my $key (keys(%markup_replace)) {
37     my $new = $markup_replace{$key};
38     $variable =~ s/\$\<\$${key}\$\>\$(.*?)\$<\$\/${key}\$>\$/\\${new}\{$1\}/gi;
39   }
40
41   $variable =~ s/[\x00-\x1f]//g;
42
43   return $variable;
44 }
45
46 sub parse_foreach {
47   my ($self, $var, $text, $start_tag, $end_tag, @indices) = @_;
48
49   my ($form, $new_contents) = ($self->{"form"}, "");
50
51   my $ary = $self->_get_loop_variable($var, 1, @indices);
52
53   my $sum                          = 0;
54   my $current_page                 = 1;
55   my ($current_line, $corrent_row) = (0, 1);
56   my $description_array            = $self->_get_loop_variable("description",     1);
57   my $longdescription_array        = $self->_get_loop_variable("longdescription", 1);
58   my $linetotal_array              = $self->_get_loop_variable("linetotal",       1);
59
60   $form->{TEMPLATE_ARRAYS}->{cumulatelinetotal} = [];
61
62   # forech block hasn't given us an array. ignore
63   return $new_contents unless ref $ary eq 'ARRAY';
64
65   for (my $i = 0; $i < scalar(@{$ary}); $i++) {
66     # do magic markers
67     $form->{"__first__"}   = $i == 0;
68     $form->{"__last__"}    = ($i + 1) == scalar(@{$ary});
69     $form->{"__odd__"}     = (($i + 1) % 2) == 1;
70     $form->{"__counter__"} = $i + 1;
71
72   #everything from here to the next marker should be removed after the release of 2.7.0
73     if (   ref $description_array       eq 'ARRAY'
74         && scalar @{$description_array} == scalar @{$ary}
75         && $self->{"chars_per_line"}    != 0)
76     {
77       my $lines = int(length($description_array->[$i]) / $self->{"chars_per_line"});
78       my $lpp;
79
80       $description_array->[$i] =~ s/(\\newline\s?)*$//;
81       $lines++ while ($description_array->[$i] =~ m/\\newline/g);
82       $lines++;
83
84       if ($current_page == 1) {
85         $lpp = $self->{"lines_on_first_page"};
86       } else {
87         $lpp = $self->{"lines_on_second_page"};
88       }
89
90       # Yes we need a manual page break -- or the user has forced one
91       if (   (($current_line + $lines) > $lpp)
92           || ($description_array->[$i]     =~ /<pagebreak>/)
93           || (   ref $longdescription_array eq 'ARRAY'
94               && $longdescription_array->[$i] =~ /<pagebreak>/)) {
95         my $pb = $self->{"pagebreak_block"};
96
97         # replace the special variables <%sumcarriedforward%>
98         # and <%lastpage%>
99
100         my $psum = $form->format_amount($self->{"myconfig"}, $sum, 2);
101         $pb =~ s/$self->{tag_start_qm}sumcarriedforward$self->{tag_end_qm}/$psum/g;
102         $pb =~ s/$self->{tag_start_qm}lastpage$self->{tag_end_qm}/$current_page/g;
103
104         my $new_text = $self->parse_block($pb, (@indices, $i));
105         return undef unless (defined($new_text));
106         $new_contents .= $new_text;
107
108         $current_page++;
109         $current_line = 0;
110       }
111       $current_line += $lines;
112     }
113   #stop removing code here.
114
115     if (   ref $linetotal_array eq 'ARRAY'
116         && $i < scalar(@{$linetotal_array})) {
117       $sum += $form->parse_amount($self->{"myconfig"}, $linetotal_array->[$i]);
118     }
119
120     $form->{TEMPLATE_ARRAYS}->{cumulatelinetotal}->[$i] = $form->format_amount($self->{"myconfig"}, $sum, 2);
121
122     my $new_text = $self->parse_block($text, (@indices, $i));
123     return undef unless (defined($new_text));
124     $new_contents .= $start_tag . $new_text . $end_tag;
125   }
126   map({ delete($form->{"__${_}__"}); } qw(first last odd counter));
127
128   return $new_contents;
129 }
130
131 sub find_end {
132   my ($self, $text, $pos, $var, $not) = @_;
133
134   my $tag_start_len = length $self->{tag_start};
135
136   my $depth = 1;
137   $pos = 0 unless ($pos);
138
139   while ($pos < length($text)) {
140     $pos++;
141
142     next if (substr($text, $pos - 1, length($self->{tag_start})) ne $self->{tag_start});
143
144     my $keyword_pos = $pos - 1 + $tag_start_len;
145
146     if ((substr($text, $keyword_pos, 2) eq 'if') || (substr($text, $keyword_pos, 7) eq 'foreach')) {
147       $depth++;
148
149     } elsif ((substr($text, $keyword_pos, 4) eq 'else') && (1 == $depth)) {
150       if (!$var) {
151         $self->{"error"} =
152             "$self->{tag_start}else$self->{tag_end} outside of "
153           . "$self->{tag_start}if$self->{tag_end} / "
154           . "$self->{tag_start}ifnot$self->{tag_end}.";
155         return undef;
156       }
157
158       my $block = substr($text, 0, $pos - 1);
159       substr($text, 0, $pos - 1) = "";
160       $text =~ s!^$self->{tag_start_qm}.+?$self->{tag_end_qm}!!;
161       $text =  $self->{tag_start} . 'if' . ($not ?  " " : "not ") . $var . $self->{tag_end} . $text;
162
163       return ($block, $text);
164
165     } elsif (substr($text, $keyword_pos, 3) eq 'end') {
166       $depth--;
167       if ($depth == 0) {
168         my $block = substr($text, 0, $pos - 1);
169         substr($text, 0, $pos - 1) = "";
170         $text =~ s!^$self->{tag_start_qm}.+?$self->{tag_end_qm}!!;
171
172         return ($block, $text);
173       }
174     }
175   }
176
177   return undef;
178 }
179
180 sub parse_block {
181   $main::lxdebug->enter_sub();
182
183   my ($self, $contents, @indices) = @_;
184
185   my $new_contents = "";
186
187   while ($contents ne "") {
188     my $pos_if      = index($contents, $self->{tag_start} . 'if');
189     my $pos_foreach = index($contents, $self->{tag_start} . 'foreach');
190
191     if ((-1 == $pos_if) && (-1 == $pos_foreach)) {
192       $new_contents .= $self->substitute_vars($contents, @indices);
193       last;
194     }
195
196     if ((-1 == $pos_if) || ((-1 != $pos_foreach) && ($pos_if > $pos_foreach))) {
197       $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices);
198       substr($contents, 0, $pos_foreach) = "";
199
200       if ($contents !~ m|^$self->{tag_start_qm}foreach (.+?)$self->{tag_end_qm}|) {
201         $self->{"error"} = "Malformed $self->{tag_start}foreach$self->{tag_end}.";
202         $main::lxdebug->leave_sub();
203         return undef;
204       }
205
206       my $var = $1;
207
208       substr($contents, 0, length($&)) = "";
209
210       my $block;
211       ($block, $contents) = $self->find_end($contents);
212       if (!$block) {
213         $self->{"error"} = "Unclosed $self->{tag_start}foreach$self->{tag_end}." unless ($self->{"error"});
214         $main::lxdebug->leave_sub();
215         return undef;
216       }
217
218       my $new_text = $self->parse_foreach($var, $block, "", "", @indices);
219       if (!defined($new_text)) {
220         $main::lxdebug->leave_sub();
221         return undef;
222       }
223       $new_contents .= $new_text;
224
225     } else {
226       if (!$self->_parse_block_if(\$contents, \$new_contents, $pos_if, @indices)) {
227         $main::lxdebug->leave_sub();
228         return undef;
229       }
230     }
231   }
232
233   $main::lxdebug->leave_sub();
234
235   return $new_contents;
236 }
237
238 sub parse_first_line {
239   my $self = shift;
240   my $line = shift || "";
241
242   if ($line =~ m/([^\s]+)set-tag-style([^\s]+)/) {
243     if ($1 eq $2) {
244       $self->{error} = "The tag start and end markers must not be equal.";
245       return 0;
246     }
247
248     $self->set_tag_style($1, $2);
249   }
250
251   return 1;
252 }
253
254 sub _parse_config_option {
255   my $self = shift;
256   my $line = shift;
257
258   $line =~ s/^\s*//;
259   $line =~ s/\s*$//;
260
261   my ($key, $value) = split m/\s*=\s*/, $line, 2;
262
263   if ($key eq 'tag-style') {
264     $self->set_tag_style(split(m/\s+/, $value, 2));
265   }
266   if ($key eq 'use-template-toolkit') {
267     $self->set_use_template_toolkit($value);
268   }
269 }
270
271 sub _parse_config_lines {
272   my $self  = shift;
273   my $lines = shift;
274
275   my ($comment_start, $comment_end) = ("", "");
276
277   if (ref $self eq 'SL::Template::LaTeX') {
278     $comment_start = '\s*%';
279   } elsif (ref $self eq 'SL::Template::HTML') {
280     $comment_start = '\s*<!--';
281     $comment_end   = '(?:--)?>\s*';
282   } else {
283     $comment_start = '\s*\#';
284   }
285
286   my $num_lines = scalar @{ $lines };
287   my $i         = 0;
288
289   while ($i < $num_lines) {
290     my $line = $lines->[$i];
291
292     if ($line !~ m/^${comment_start}\s*config\s*:(.*?)${comment_end}$/i) {
293       $i++;
294       next;
295     }
296
297     $self->_parse_config_option($1);
298     splice @{ $lines }, $i, 1;
299     $num_lines--;
300   }
301 }
302
303 sub _force_mandatory_packages {
304   my $self  = shift;
305   my $lines = shift;
306
307   my (%used_packages, $document_start_line, $last_usepackage_line);
308
309   foreach my $i (0 .. scalar @{ $lines } - 1) {
310     if ($lines->[$i] =~ m/\\usepackage[^\{]*{(.*?)}/) {
311       $used_packages{$1} = 1;
312       $last_usepackage_line = $i;
313
314     } elsif ($lines->[$i] =~ m/\\begin\{document\}/) {
315       $document_start_line = $i;
316       last;
317
318     }
319   }
320
321   my $insertion_point = defined($document_start_line)  ? $document_start_line
322                       : defined($last_usepackage_line) ? $last_usepackage_line
323                       :                                  scalar @{ $lines } - 1;
324
325   foreach my $package (qw(textcomp)) {
326     next if $used_packages{$package};
327     splice @{ $lines }, $insertion_point, 0, "\\usepackage{${package}}\n";
328     $insertion_point++;
329   }
330 }
331
332 sub parse {
333   my $self = $_[0];
334   local *OUT = $_[1];
335   my $form = $self->{"form"};
336
337   if (!open(IN, "$form->{templates}/$form->{IN}")) {
338     $self->{"error"} = "$form->{templates}/$form->{IN}: $!";
339     return 0;
340   }
341   binmode IN, ":utf8" if $::locale->is_utf8;
342   my @lines = <IN>;
343   close(IN);
344
345   $self->_parse_config_lines(\@lines);
346   $self->_force_mandatory_packages(\@lines) if (ref $self eq 'SL::Template::LaTeX');
347
348   my $contents = join("", @lines);
349
350   # detect pagebreak block and its parameters
351   if ($contents =~ /$self->{tag_start_qm}pagebreak\s+(\d+)\s+(\d+)\s+(\d+)\s*$self->{tag_end_qm}(.*?)$self->{tag_start_qm}end(\s*pagebreak)?$self->{tag_end_qm}/s) {
352     $self->{"chars_per_line"} = $1;
353     $self->{"lines_on_first_page"} = $2;
354     $self->{"lines_on_second_page"} = $3;
355     $self->{"pagebreak_block"} = $4;
356
357     substr($contents, length($`), length($&)) = "";
358   }
359
360   $self->{"forced_pagebreaks"} = [];
361
362   my $new_contents;
363   if ($self->{use_template_toolkit}) {
364     if ($self->{custom_tag_style}) {
365       $contents = "[% TAGS $self->{tag_start} $self->{tag_end} %]\n" . $contents;
366     }
367
368     $::form->init_template->process(\$contents, $form, \$new_contents) || die $::form->template->error;
369   } else {
370     $new_contents = $self->parse_block($contents);
371   }
372   if (!defined($new_contents)) {
373     $main::lxdebug->leave_sub();
374     return 0;
375   }
376
377   if ($::locale->is_utf8) {
378     binmode OUT, ":utf8";
379     print OUT Unicode::Normalize::normalize('C', $new_contents);
380
381   } else {
382     print OUT $new_contents;
383   }
384
385   if ($form->{"format"} =~ /postscript/i) {
386     return $self->convert_to_postscript();
387   } elsif ($form->{"format"} =~ /pdf/i) {
388     return $self->convert_to_pdf();
389   } else {
390     return 1;
391   }
392 }
393
394 sub convert_to_postscript {
395   my ($self) = @_;
396   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
397
398   # Convert the tex file to postscript
399   local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS};
400
401   if (!chdir("$userspath")) {
402     $self->{"error"} = "chdir : $!";
403     $self->cleanup();
404     return 0;
405   }
406
407   $form->{tmpfile} =~ s/\Q$userspath\E\///g;
408
409   my $latex = $self->_get_latex_path();
410   my $old_home = $ENV{HOME};
411   my $old_openin_any = $ENV{openin_any};
412   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
413   $ENV{openin_any} = "p";
414
415   for (my $run = 1; $run <= 2; $run++) {
416     system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
417            "> $form->{tmpfile}.err");
418     if ($?) {
419       $ENV{HOME} = $old_home;
420       $ENV{openin_any} = $old_openin_any;
421       $self->{"error"} = $form->cleanup($latex);
422       return 0;
423     }
424   }
425
426   $form->{tmpfile} =~ s/tex$/dvi/;
427
428   system("dvips $form->{tmpfile} -o -q > /dev/null");
429   $ENV{HOME} = $old_home;
430   $ENV{openin_any} = $old_openin_any;
431
432   if ($?) {
433     $self->{"error"} = "dvips : $!";
434     $self->cleanup('dvips');
435     return 0;
436   }
437   $form->{tmpfile} =~ s/dvi$/ps/;
438
439   $self->cleanup();
440
441   return 1;
442 }
443
444 sub convert_to_pdf {
445   my ($self) = @_;
446   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
447
448   # Convert the tex file to PDF
449   local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS};
450
451   if (!chdir("$userspath")) {
452     $self->{"error"} = "chdir : $!";
453     $self->cleanup();
454     return 0;
455   }
456
457   $form->{tmpfile} =~ s/\Q$userspath\E\///g;
458
459   my $latex = $self->_get_latex_path();
460   my $old_home = $ENV{HOME};
461   my $old_openin_any = $ENV{openin_any};
462   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
463   $ENV{openin_any} = "p";
464
465   for (my $run = 1; $run <= 2; $run++) {
466     system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
467            "> $form->{tmpfile}.err");
468     if ($?) {
469       $ENV{HOME}     = $old_home;
470       $ENV{openin_any} = $old_openin_any;
471       $self->{error} = $form->cleanup($latex);
472       return 0;
473     }
474   }
475
476   $ENV{HOME} = $old_home;
477   $ENV{openin_any} = $old_openin_any;
478   $form->{tmpfile} =~ s/tex$/pdf/;
479
480   $self->cleanup();
481
482   return 1;
483 }
484
485 sub _get_latex_path {
486   return $::lx_office_conf{applications}->{latex} || 'pdflatex';
487 }
488
489 sub get_mime_type() {
490   my ($self) = @_;
491
492   if ($self->{"form"}->{"format"} =~ /postscript/i) {
493     return "application/postscript";
494   } else {
495     return "application/pdf";
496   }
497 }
498
499 sub uses_temp_file {
500   return 1;
501 }
502
503 sub parse_and_create_pdf {
504   my ($class, $template_file_name, %params) = @_;
505
506   my $keep_temp                = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
507   my ($tex_fh, $tex_file_name) = File::Temp::tempfile(
508     'kivitendo-printXXXXXX',
509     SUFFIX => '.tex',
510     DIR    => $::lx_office_conf{paths}->{userspath},
511     UNLINK => $keep_temp ? 0 : 1,,
512   );
513
514   my $old_wd               = getcwd();
515
516   my $local_form           = Form->new('');
517   $local_form->{cwd}       = $old_wd;
518   $local_form->{IN}        = $template_file_name;
519   $local_form->{tmpdir}    = $::lx_office_conf{paths}->{userspath};
520   $local_form->{tmpfile}   = $tex_file_name;
521   $local_form->{templates} = SL::DB::Default->get->templates;
522
523   foreach (keys %params) {
524     croak "The parameter '$_' must not be used." if exists $local_form->{$_};
525     $local_form->{$_} = $params{$_};
526   }
527
528   my $error;
529   eval {
530     my $template = SL::Template::LaTeX->new($template_file_name, $local_form, \%::myconfig, $::lx_office_conf{paths}->{userspath});
531     my $result   = $template->parse($tex_fh) && $template->convert_to_pdf;
532
533     die $template->{error} unless $result;
534
535     1;
536   } or do { $error = $EVAL_ERROR; };
537
538   chdir $old_wd;
539   close $tex_fh;
540
541   if ($keep_temp) {
542     chmod(((stat $tex_file_name)[2] & 07777) | 0660, $tex_file_name);
543   } else {
544     my $tmpfile =  $tex_file_name;
545     $tmpfile    =~ s/\.\w+$//;
546     unlink(grep { !m/\.pdf$/ } <$tmpfile.*>);
547   }
548
549   return (error     => $error) if $error;
550   return (file_name => do { $tex_file_name =~ s/tex$/pdf/; $tex_file_name });
551 }
552
553 1;