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