X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FTemplate%2FLaTeX.pm;h=293db2e18c5bbb58cb495563026aa3c18134867d;hb=53593baa211863fbf66540cf1bcc36c8fb37257f;hp=c76b82a6274067af448d9360c54bd30a6140628a;hpb=cbf105b2340879d2381b81148f2cbfe16dfdffbf;p=kivitendo-erp.git diff --git a/SL/Template/LaTeX.pm b/SL/Template/LaTeX.pm index c76b82a62..293db2e18 100644 --- a/SL/Template/LaTeX.pm +++ b/SL/Template/LaTeX.pm @@ -11,9 +11,11 @@ use File::Basename; use File::Temp; use HTML::Entities (); use List::MoreUtils qw(any); +use Scalar::Util qw(blessed); use Unicode::Normalize qw(); use SL::DB::Default; +use SL::System::Process; my %text_markup_replace = ( b => 'textbf', @@ -66,6 +68,13 @@ my %html_replace = ( '
' => "\\newline ", ); +sub _lb_to_space { + my ($to_replace) = @_; + + my $vspace = '\vspace*{0.5cm}'; + return $vspace x (length($to_replace) / length($html_replace{'
'})); +} + sub _format_html { my ($self, $content, %params) = @_; @@ -74,6 +83,8 @@ sub _format_html { $content =~ s{ (?:\ |\s)+ }{ }gx; $content =~ s{ (?:\ |\s)+$ }{}gx; $content =~ s{ (?:
)+$ }{}gx; + $content =~ s{ |
    \s*
}{}igx; + $content =~ s{ (?:

\s*

\s* )+ \Z }{}imgx; my @parts = grep { $_ } map { if (substr($_, 0, 1) eq '<') { @@ -86,7 +97,13 @@ sub _format_html { } split(m{(<.*?>)}x, $content); $content = join '', @parts; - $content =~ s{ (?: [\n\s] | \\newline )+$ }{}gx; + $content =~ s{ (?: [\n\s] | \\newline )+ $ }{}gx; # remove line breaks at the end of the text + $content =~ s{ ^ \s+ }{}gx; # remove white space at the start of the text + $content =~ s{ ^ ( \\newline \ )+ }{ _lb_to_space($1) }gxe; # convert line breaks at the start of the text to vertical space + $content =~ s{ ( \n\n+ ) ( \\newline \ )+ }{ $1 . _lb_to_space($2) }gxe; # convert line breaks at the start of a paragraph to vertical space + $content =~ s{ ( \\end\{ [^\}]+ \} \h* ) ( \\newline \ )+ }{ $1 . _lb_to_space($2) }gxe; # convert line breaks after LaTeX environments like lists to vertical space + $content =~ s{ ^ \h+ \\newline }{\\newline}gmx; + $content =~ s{ \n\n \h* \\newline \h* }{\n\n}gmx; return $content; } @@ -112,6 +129,9 @@ sub format_string { // $formatters{ $self->{default_content_type} } // $formatters{ text }; + $content =~ s{[^\p{Print}\n]|\p{Cf}}{}g; + $variable =~ s{[^\p{Print}\n]|\p{Cf}}{}g; + return $formatter->($self, $content, variable => $variable); } @@ -269,15 +289,15 @@ sub parse_block { $new_contents .= $self->substitute_vars(substr($contents, 0, $pos_foreach), @indices); substr($contents, 0, $pos_foreach) = ""; - if ($contents !~ m|^$self->{tag_start_qm}foreach (.+?)$self->{tag_end_qm}|) { + if ($contents !~ m|^($self->{tag_start_qm}foreach (.+?)$self->{tag_end_qm})|) { $self->{"error"} = "Malformed $self->{tag_start}foreach$self->{tag_end}."; $main::lxdebug->leave_sub(); return undef; } - my $var = $1; + my $var = $2; - substr($contents, 0, length($&)) = ""; + substr($contents, 0, length($1)) = ""; my $block; ($block, $contents) = $self->find_end($contents); @@ -372,33 +392,76 @@ sub _parse_config_lines { } } +sub _embed_file_directive { + my ($self, $file) = @_; + + # { source => $xmlfile, + # name => 'factur-x.xml', + # description => $::locale->text('Factur-X/ZUGFeRD invoice'), } + + my $file_name = blessed($file->{source}) && $file->{source}->can('filename') ? $file->{source}->filename : "" . $file->{source}->filename; + my $embed_name = $file->{name} // $file_name; + $embed_name =~ s{.*/}{}; + + my $embed_name_ascii = $::locale->quote_special_chars('filenames', $embed_name); + $embed_name_ascii =~ s{[^a-z0-9!@#$%^&*(){}[\],.+'"=_-]+}{}gi; + + my @options; + + my $add_opt = sub { + my ($name, $value) = @_; + return if ($value // '') eq ''; + push @options, sprintf('%s={%s}', $name, $value); # TODO: escaping + }; + + $add_opt->('filespec', $embed_name_ascii); + $add_opt->('ucfilespec', $embed_name); + $add_opt->('desc', $file->{description}); + $add_opt->('afrelationship', $file->{relationship}); + $add_opt->('mimetype', $file->{mime_type}); + + return sprintf('\embedfile[%s]{%s}', join(',', @options), $file_name); +} + sub _force_mandatory_packages { - my $self = shift; - my $lines = shift; + my ($self, @lines) = @_; + my @new_lines; - my (%used_packages, $document_start_line, $last_usepackage_line); + my %used_packages; + my @required_packages = qw(textcomp ulem); + push @required_packages, 'embedfile' if $self->{pdf_a}; - foreach my $i (0 .. scalar @{ $lines } - 1) { - if ($lines->[$i] =~ m/\\usepackage[^\{]*{(.*?)}/) { + foreach my $line (@lines) { + if ($line =~ m/\\usepackage[^\{]*{(.*?)}/) { $used_packages{$1} = 1; - $last_usepackage_line = $i; - } elsif ($lines->[$i] =~ m/\\begin\{document\}/) { - $document_start_line = $i; - last; + } elsif ($line =~ m/\\begin\{document\}/) { + if ($self->{pdf_a} && $self->{pdf_a}->{xmp}) { + my $version = $self->{pdf_a}->{version} // '3a'; + my $xmp_file_name = $self->{userspath} . "/pdfa.xmp"; + my $out = IO::File->new($xmp_file_name, ">:encoding(utf-8)") || croak "Error creating ${xmp_file_name}: $!"; + $out->print(Encode::encode('utf-8', $self->{pdf_a}->{xmp})); + $out->close; + + push @new_lines, ( + "\\usepackage[a-${version},mathxmp]{pdfx}[2018/12/22]\n", + "\\usepackage[genericmode]{tagpdf}\n", + "\\tagpdfsetup{activate-all}\n", + "\\hypersetup{pdfstartview=}\n", + ); + } - } - } + push @new_lines, map { "\\usepackage{$_}\n" } grep { !$used_packages{$_} } @required_packages; + push @new_lines, $line; + push @new_lines, map { $self->_embed_file_directive($_) } @{ $self->{pdf_attachments} // [] }; - my $insertion_point = defined($document_start_line) ? $document_start_line - : defined($last_usepackage_line) ? $last_usepackage_line - : scalar @{ $lines } - 1; + next; + } - foreach my $package (qw(textcomp ulem)) { - next if $used_packages{$package}; - splice @{ $lines }, $insertion_point, 0, "\\usepackage{${package}}\n"; - $insertion_point++; + push @new_lines, $line; } + + return @new_lines; } sub parse { @@ -415,18 +478,18 @@ sub parse { close(IN); $self->_parse_config_lines(\@lines); - $self->_force_mandatory_packages(\@lines) if (ref $self eq 'SL::Template::LaTeX'); + @lines = $self->_force_mandatory_packages(@lines) if (ref $self eq 'SL::Template::LaTeX'); my $contents = join("", @lines); # detect pagebreak block and its parameters - 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) { - $self->{"chars_per_line"} = $1; - $self->{"lines_on_first_page"} = $2; - $self->{"lines_on_second_page"} = $3; - $self->{"pagebreak_block"} = $4; + 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) { + $self->{"chars_per_line"} = $3; + $self->{"lines_on_first_page"} = $4; + $self->{"lines_on_second_page"} = $5; + $self->{"pagebreak_block"} = $6; - substr($contents, length($`), length($&)) = ""; + substr($contents, length($1), length($2)) = ""; } $self->{"forced_pagebreaks"} = []; @@ -437,7 +500,9 @@ sub parse { $contents = "[% TAGS $self->{tag_start} $self->{tag_end} %]\n" . $contents; } - $::form->init_template->process(\$contents, $form, \$new_contents) || die $::form->template->error; + my $globals = global_vars(); + + $::form->template->process(\$contents, { %$form, %$globals }, \$new_contents) || die $::form->template->error; } else { $new_contents = $self->parse_block($contents); } @@ -458,12 +523,21 @@ sub parse { } } +sub _texinputs_path { + my ($self, $templates_path) = @_; + + my $exe_dir = SL::System::Process::exe_dir(); + $templates_path = $exe_dir . '/' . $templates_path unless $templates_path =~ m{^/}; + + return join(':', grep({ $_ } ('.', $exe_dir . '/texmf', $exe_dir . '/users', $templates_path, $ENV{TEXINPUTS})), ''); +} + sub convert_to_postscript { my ($self) = @_; my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"}); # Convert the tex file to postscript - local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS}; + local $ENV{TEXINPUTS} = $self->_texinputs_path($form->{templates}); if (!chdir("$userspath")) { $self->{"error"} = "chdir : $!"; @@ -477,11 +551,13 @@ sub convert_to_postscript { my $old_home = $ENV{HOME}; my $old_openin_any = $ENV{openin_any}; $ENV{HOME} = $userspath =~ m|^/| ? $userspath : getcwd(); - $ENV{openin_any} = "p"; + $ENV{openin_any} = "r"; for (my $run = 1; $run <= 2; $run++) { - system("${latex} --interaction=nonstopmode $form->{tmpfile} " . - "> $form->{tmpfile}.err"); + if (system("${latex} --interaction=nonstopmode $form->{tmpfile} " . + "> $form->{tmpfile}.err") == -1) { + die "system call to $latex failed: $!"; + } if ($?) { $ENV{HOME} = $old_home; $ENV{openin_any} = $old_openin_any; @@ -492,12 +568,14 @@ sub convert_to_postscript { $form->{tmpfile} =~ s/tex$/dvi/; - system("dvips $form->{tmpfile} -o -q > /dev/null"); + if (system("dvips $form->{tmpfile} -o -q > /dev/null") == -1) { + die "system call to dvips failed: $!"; + } $ENV{HOME} = $old_home; $ENV{openin_any} = $old_openin_any; if ($?) { - $self->{"error"} = "dvips : $!"; + $self->{"error"} = "dvips : $?"; $self->cleanup('dvips'); return 0; } @@ -513,7 +591,7 @@ sub convert_to_pdf { my ($form, $userspath) = ($self->{"form"}, $self->{"userspath"}); # Convert the tex file to PDF - local $ENV{TEXINPUTS} = ".:" . $form->{cwd} . "/" . $form->{templates} . ":" . $ENV{TEXINPUTS}; + local $ENV{TEXINPUTS} = $self->_texinputs_path($form->{templates}); if (!chdir("$userspath")) { $self->{"error"} = "chdir : $!"; @@ -527,11 +605,14 @@ sub convert_to_pdf { my $old_home = $ENV{HOME}; my $old_openin_any = $ENV{openin_any}; $ENV{HOME} = $userspath =~ m|^/| ? $userspath : getcwd(); - $ENV{openin_any} = "p"; + $ENV{openin_any} = "r"; for (my $run = 1; $run <= 2; $run++) { - system("${latex} --interaction=nonstopmode $form->{tmpfile} " . - "> $form->{tmpfile}.err"); + if (system("${latex} --interaction=nonstopmode $form->{tmpfile} " . + "> $form->{tmpfile}.err") == -1) { + die "system call to $latex failed: $!"; + } + if ($?) { $ENV{HOME} = $old_home; $ENV{openin_any} = $old_openin_any; @@ -570,11 +651,12 @@ sub uses_temp_file { sub parse_and_create_pdf { my ($class, $template_file_name, %params) = @_; + my $userspath = delete($params{userspath}) || $::lx_office_conf{paths}->{userspath}; my $keep_temp = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files}; my ($tex_fh, $tex_file_name) = File::Temp::tempfile( 'kivitendo-printXXXXXX', SUFFIX => '.tex', - DIR => $::lx_office_conf{paths}->{userspath}, + DIR => $userspath, UNLINK => $keep_temp ? 0 : 1,, ); @@ -583,7 +665,7 @@ sub parse_and_create_pdf { my $local_form = Form->new(''); $local_form->{cwd} = $old_wd; $local_form->{IN} = $template_file_name; - $local_form->{tmpdir} = $::lx_office_conf{paths}->{userspath}; + $local_form->{tmpdir} = $userspath; $local_form->{tmpfile} = $tex_file_name; $local_form->{templates} = SL::DB::Default->get->templates; @@ -594,7 +676,7 @@ sub parse_and_create_pdf { my $error; eval { - my $template = SL::Template::LaTeX->new(file_name => $template_file_name, form => $local_form); + my $template = SL::Template::LaTeX->new(file_name => $template_file_name, form => $local_form, userspath => $userspath); my $result = $template->parse($tex_fh) && $template->convert_to_pdf; die $template->{error} unless $result; @@ -617,4 +699,15 @@ sub parse_and_create_pdf { return (file_name => do { $tex_file_name =~ s/tex$/pdf/; $tex_file_name }); } +sub global_vars { + { + AUTH => $::auth, + INSTANCE_CONF => $::instance_conf, + LOCALE => $::locale, + LXCONFIG => $::lx_office_conf, + LXDEBUG => $::lxdebug, + MYCONFIG => \%::myconfig, + }; +} + 1;