2f99f7e180a8b8c89849a52011b040fa3c26990a
[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";
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   binmode OUT, ":utf8";
378   print OUT Unicode::Normalize::normalize('C', $new_contents);
379
380   if ($form->{"format"} =~ /postscript/i) {
381     return $self->convert_to_postscript();
382   } elsif ($form->{"format"} =~ /pdf/i) {
383     return $self->convert_to_pdf();
384   } else {
385     return 1;
386   }
387 }
388
389 sub convert_to_postscript {
390   my ($self) = @_;
391   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
392
393   # Convert the tex file to postscript
394   local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS};
395
396   if (!chdir("$userspath")) {
397     $self->{"error"} = "chdir : $!";
398     $self->cleanup();
399     return 0;
400   }
401
402   $form->{tmpfile} =~ s/\Q$userspath\E\///g;
403
404   my $latex = $self->_get_latex_path();
405   my $old_home = $ENV{HOME};
406   my $old_openin_any = $ENV{openin_any};
407   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
408   $ENV{openin_any} = "p";
409
410   for (my $run = 1; $run <= 2; $run++) {
411     system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
412            "> $form->{tmpfile}.err");
413     if ($?) {
414       $ENV{HOME} = $old_home;
415       $ENV{openin_any} = $old_openin_any;
416       $self->{"error"} = $form->cleanup($latex);
417       return 0;
418     }
419   }
420
421   $form->{tmpfile} =~ s/tex$/dvi/;
422
423   system("dvips $form->{tmpfile} -o -q > /dev/null");
424   $ENV{HOME} = $old_home;
425   $ENV{openin_any} = $old_openin_any;
426
427   if ($?) {
428     $self->{"error"} = "dvips : $!";
429     $self->cleanup('dvips');
430     return 0;
431   }
432   $form->{tmpfile} =~ s/dvi$/ps/;
433
434   $self->cleanup();
435
436   return 1;
437 }
438
439 sub convert_to_pdf {
440   my ($self) = @_;
441   my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"});
442
443   # Convert the tex file to PDF
444   local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS};
445
446   if (!chdir("$userspath")) {
447     $self->{"error"} = "chdir : $!";
448     $self->cleanup();
449     return 0;
450   }
451
452   $form->{tmpfile} =~ s/\Q$userspath\E\///g;
453
454   my $latex = $self->_get_latex_path();
455   my $old_home = $ENV{HOME};
456   my $old_openin_any = $ENV{openin_any};
457   $ENV{HOME}   = $userspath =~ m|^/| ? $userspath : getcwd();
458   $ENV{openin_any} = "p";
459
460   for (my $run = 1; $run <= 2; $run++) {
461     system("${latex} --interaction=nonstopmode $form->{tmpfile} " .
462            "> $form->{tmpfile}.err");
463     if ($?) {
464       $ENV{HOME}     = $old_home;
465       $ENV{openin_any} = $old_openin_any;
466       $self->{error} = $form->cleanup($latex);
467       return 0;
468     }
469   }
470
471   $ENV{HOME} = $old_home;
472   $ENV{openin_any} = $old_openin_any;
473   $form->{tmpfile} =~ s/tex$/pdf/;
474
475   $self->cleanup();
476
477   return 1;
478 }
479
480 sub _get_latex_path {
481   return $::lx_office_conf{applications}->{latex} || 'pdflatex';
482 }
483
484 sub get_mime_type() {
485   my ($self) = @_;
486
487   if ($self->{"form"}->{"format"} =~ /postscript/i) {
488     return "application/postscript";
489   } else {
490     return "application/pdf";
491   }
492 }
493
494 sub uses_temp_file {
495   return 1;
496 }
497
498 sub parse_and_create_pdf {
499   my ($class, $template_file_name, %params) = @_;
500
501   my $keep_temp                = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
502   my ($tex_fh, $tex_file_name) = File::Temp::tempfile(
503     'kivitendo-printXXXXXX',
504     SUFFIX => '.tex',
505     DIR    => $::lx_office_conf{paths}->{userspath},
506     UNLINK => $keep_temp ? 0 : 1,,
507   );
508
509   my $old_wd               = getcwd();
510
511   my $local_form           = Form->new('');
512   $local_form->{cwd}       = $old_wd;
513   $local_form->{IN}        = $template_file_name;
514   $local_form->{tmpdir}    = $::lx_office_conf{paths}->{userspath};
515   $local_form->{tmpfile}   = $tex_file_name;
516   $local_form->{templates} = SL::DB::Default->get->templates;
517
518   foreach (keys %params) {
519     croak "The parameter '$_' must not be used." if exists $local_form->{$_};
520     $local_form->{$_} = $params{$_};
521   }
522
523   my $error;
524   eval {
525     my $template = SL::Template::LaTeX->new($template_file_name, $local_form, \%::myconfig, $::lx_office_conf{paths}->{userspath});
526     my $result   = $template->parse($tex_fh) && $template->convert_to_pdf;
527
528     die $template->{error} unless $result;
529
530     1;
531   } or do { $error = $EVAL_ERROR; };
532
533   chdir $old_wd;
534   close $tex_fh;
535
536   if ($keep_temp) {
537     chmod(((stat $tex_file_name)[2] & 07777) | 0660, $tex_file_name);
538   } else {
539     my $tmpfile =  $tex_file_name;
540     $tmpfile    =~ s/\.\w+$//;
541     unlink(grep { !m/\.pdf$/ } <$tmpfile.*>);
542   }
543
544   return (error     => $error) if $error;
545   return (file_name => do { $tex_file_name =~ s/tex$/pdf/; $tex_file_name });
546 }
547
548 1;