File indexing completed on 2024-04-28 15:14:04

0001 /***************************************************************************
0002     File                 : TeXRenderer.cc
0003     Project              : LabPlot
0004     Description          : TeX renderer class
0005     --------------------------------------------------------------------
0006     Copyright            : (C) 2008-2016 by Alexander Semke (alexander.semke@web.de)
0007     Copyright            : (C) 2012 by Stefan Gerlach (stefan.gerlach@uni-konstanz.de)
0008 
0009  ***************************************************************************/
0010 
0011 /***************************************************************************
0012  *                                                                         *
0013  *  This program is free software; you can redistribute it and/or modify   *
0014  *  it under the terms of the GNU General Public License as published by   *
0015  *  the Free Software Foundation; either version 2 of the License, or      *
0016  *  (at your option) any later version.                                    *
0017  *                                                                         *
0018  *  This program is distributed in the hope that it will be useful,        *
0019  *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
0020  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
0021  *  GNU General Public License for more details.                           *
0022  *                                                                         *
0023  *   You should have received a copy of the GNU General Public License     *
0024  *   along with this program; if not, write to the Free Software           *
0025  *   Foundation, Inc., 51 Franklin Street, Fifth Floor,                    *
0026  *   Boston, MA  02110-1301  USA                                           *
0027  *                                                                         *
0028  ***************************************************************************/
0029 #include "TeXRenderer.h"
0030 #include "backend/lib/macros.h"
0031 
0032 #include <KConfigGroup>
0033 #include <KSharedConfig>
0034 
0035 #include <QColor>
0036 #include <QDir>
0037 #include <QImage>
0038 #include <QProcess>
0039 #include <QStandardPaths>
0040 #include <QTemporaryFile>
0041 #include <QTextStream>
0042 
0043 /*!
0044     \class TeXRenderer
0045     \brief Implements rendering of latex code to a PNG image.
0046 
0047     Uses latex engine specified by the user (default xelatex) to render LaTeX text
0048 
0049     \ingroup tools
0050 */
0051 QImage TeXRenderer::renderImageLaTeX(const QString& teXString, bool* success, const TeXRenderer::Formatting& format) {
0052     const QColor& fontColor = format.fontColor;
0053     const QColor& backgroundColor = format.backgroundColor;
0054     const int fontSize = format.fontSize;
0055     const QString& fontFamily = format.fontFamily;
0056     const int dpi = format.dpi;
0057 
0058     //determine the temp directory where the produced files are going to be created
0059     QString tempPath;
0060 #ifdef Q_OS_LINUX
0061     //on linux try to use shared memory device first if available
0062     static bool useShm = QDir("/dev/shm/").exists();
0063     if (useShm)
0064         tempPath = "/dev/shm/";
0065     else
0066         tempPath = QDir::tempPath();
0067 #else
0068     tempPath = QDir::tempPath();
0069 #endif
0070 
0071     // make sure we have preview.sty available
0072     if (!tempPath.contains(QLatin1String("preview.sty"))) {
0073         QString file = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("latex/preview.sty"));
0074         if (file.isEmpty()) {
0075             WARN("Couldn't find preview.sty.");
0076             *success = false;
0077             return QImage();
0078         }
0079         else
0080             QFile::copy(file, tempPath + QLatin1String("/") + QLatin1String("preview.sty"));
0081     }
0082 
0083     //create a temporary file
0084     QTemporaryFile file(tempPath + QLatin1String("/") + "labplot_XXXXXX.tex");
0085     // FOR DEBUG: file.setAutoRemove(false);
0086     // DEBUG("temp file path = " << file.fileName().toUtf8().constData());
0087     if (file.open()) {
0088         QDir::setCurrent(tempPath);
0089     } else {
0090         WARN("Couldn't open the file " << STDSTRING(file.fileName()));
0091         *success = false;
0092         return QImage();
0093     }
0094 
0095     //determine latex engine to be used
0096     KConfigGroup group = KSharedConfig::openConfig()->group("Settings_Worksheet");
0097     QString engine = group.readEntry("LaTeXEngine", "pdflatex");
0098 
0099     // create latex code
0100     QTextStream out(&file);
0101     int headerIndex = teXString.indexOf("\\begin{document}");
0102     QString body;
0103     if (headerIndex != -1) {
0104         //user provided a complete latex document -> extract the document header and body
0105         QString header = teXString.left(headerIndex);
0106         int footerIndex = teXString.indexOf("\\end{document}");
0107         body = teXString.mid(headerIndex + 16, footerIndex - headerIndex - 16);
0108         out << header;
0109     } else {
0110         //user simply provided a document body (assume it's a math. expression) -> add a minimal header
0111         out << "\\documentclass{minimal}";
0112         if (teXString.indexOf('$') == -1)
0113             body = '$' + teXString + '$';
0114         else
0115             body = teXString;
0116 
0117         //replace line breaks with tex command for a line break '\\'
0118         body = body.replace(QLatin1String("\n"), QLatin1String("\\\\"));
0119     }
0120 
0121     if (engine == "xelatex" || engine == "lualatex") {
0122         out << "\\usepackage{xltxtra}";
0123         out << "\\defaultfontfeatures{Ligatures=TeX}";
0124         if (!fontFamily.isEmpty())
0125             out << "\\setmainfont[Mapping=tex-text]{" << fontFamily << "}";
0126     }
0127 
0128     out << "\\usepackage{color}";
0129     out << "\\usepackage[active,displaymath,textmath,tightpage]{preview}";
0130     // TODO: this fails with pdflatex
0131     //out << "\\usepackage{mathtools}";
0132     out << "\\begin{document}";
0133     out << "\\begin{preview}";
0134     out << "\\colorbox[rgb]{" << backgroundColor.redF() << ',' << backgroundColor.greenF() << ',' << backgroundColor.blueF() << "}{";
0135     out << "\\fontsize{" << QString::number(fontSize) << "}{" << QString::number(fontSize) << "}\\selectfont";
0136     out << "\\color[rgb]{" << fontColor.redF() << ',' << fontColor.greenF() << ',' << fontColor.blueF() << "}";
0137     out << body;
0138     out << "}";
0139     out << "\\end{preview}";
0140     out << "\\end{document}";
0141     out.flush();
0142     if (engine == "latex")
0143         return imageFromDVI(file, dpi, success);
0144     else
0145         return imageFromPDF(file, dpi, engine, success);
0146 }
0147 
0148 // TEX -> PDF -> PNG
0149 QImage TeXRenderer::imageFromPDF(const QTemporaryFile& file, const int dpi, const QString& engine, bool* success) {
0150     QFileInfo fi(file.fileName());
0151     const QString& baseName = fi.completeBaseName();
0152 
0153     // pdflatex: produce the PDF file
0154     QProcess latexProcess;
0155 #if defined(HAVE_WINDOWS)
0156     latexProcess.setNativeArguments("-interaction=batchmode " + file.fileName());
0157     latexProcess.start(engine, QStringList() << QString());
0158 #else
0159     latexProcess.start(engine, QStringList() << "-interaction=batchmode" << file.fileName());
0160 #endif
0161 
0162     if (!latexProcess.waitForFinished() || latexProcess.exitCode() != 0) {
0163         WARN("pdflatex process failed, exit code = " << latexProcess.exitCode());
0164         *success = false;
0165         QFile::remove(baseName + ".aux");
0166         QFile::remove(baseName + ".log");
0167         return QImage();
0168     }
0169 
0170     // convert: PDF -> PNG
0171     QProcess convertProcess;
0172 #if defined(HAVE_WINDOWS)
0173     // need to set path to magick coder modules (which are in the labplot2 directory)
0174     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0175     env.insert("MAGICK_CODER_MODULE_PATH", qPrintable(qgetenv("PROGRAMFILES") + QString("\\labplot2")));
0176     convertProcess.setProcessEnvironment(env);
0177 #endif
0178 
0179     const QStringList params{"-density", QString::number(dpi), baseName + ".pdf", baseName + ".png"};
0180     convertProcess.start("convert", params);
0181 
0182     if (!convertProcess.waitForFinished() || convertProcess.exitCode() != 0) {
0183         WARN("convert process failed, exit code = " << convertProcess.exitCode());
0184         *success = false;
0185         QFile::remove(baseName + ".aux");
0186         QFile::remove(baseName + ".log");
0187         QFile::remove(baseName + ".pdf");
0188         return QImage();
0189     }
0190 
0191     // read png file
0192     QImage image(baseName + ".png", "png");
0193 
0194     // final clean up
0195     QFile::remove(baseName + ".aux");
0196     QFile::remove(baseName + ".log");
0197     QFile::remove(baseName + ".pdf");
0198     QFile::remove(baseName + ".png");
0199 
0200     *success = true;
0201     return image;
0202 }
0203 
0204 // TEX -> DVI -> PS -> PNG
0205 QImage TeXRenderer::imageFromDVI(const QTemporaryFile& file, const int dpi, bool* success) {
0206     QFileInfo fi(file.fileName());
0207     const QString& baseName = fi.completeBaseName();
0208 
0209     //latex: produce the DVI file
0210     QProcess latexProcess;
0211     latexProcess.start("latex", QStringList() << "-interaction=batchmode" << file.fileName());
0212     if (!latexProcess.waitForFinished() || latexProcess.exitCode() != 0) {
0213         WARN("latex process failed, exit code = " << latexProcess.exitCode());
0214         *success = false;
0215         QFile::remove(baseName + ".aux");
0216         QFile::remove(baseName + ".log");
0217         return QImage();
0218     }
0219 
0220     // dvips: DVI -> PS
0221     QProcess dvipsProcess;
0222     dvipsProcess.start("dvips", QStringList() << "-E" << baseName);
0223     if (!dvipsProcess.waitForFinished() || dvipsProcess.exitCode() != 0) {
0224         WARN("dvips process failed, exit code = " << dvipsProcess.exitCode());
0225         *success = false;
0226         QFile::remove(baseName + ".aux");
0227         QFile::remove(baseName + ".log");
0228         QFile::remove(baseName + ".dvi");
0229         return QImage();
0230     }
0231 
0232     // convert: PS -> PNG
0233     QProcess convertProcess;
0234 #if defined(HAVE_WINDOWS)
0235     // need to set path to magick coder modules (which are in the labplot2 directory)
0236     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0237     env.insert("MAGICK_CODER_MODULE_PATH", qPrintable(qgetenv("PROGRAMFILES") + QString("\\labplot2")));
0238     convertProcess.setProcessEnvironment(env);
0239 #endif
0240 
0241     const QStringList params{"-density", QString::number(dpi), baseName + ".ps", baseName + ".png"};
0242     convertProcess.start("convert", params);
0243 
0244     if (!convertProcess.waitForFinished() || convertProcess.exitCode() != 0) {
0245         WARN("convert process failed, exit code = " << convertProcess.exitCode());
0246         *success = false;
0247         QFile::remove(baseName + ".aux");
0248         QFile::remove(baseName + ".log");
0249         QFile::remove(baseName + ".dvi");
0250         QFile::remove(baseName + ".ps");
0251         return QImage();
0252     }
0253 
0254     // read png file
0255     QImage image(baseName + ".png", "png");
0256 
0257     // final clean up
0258     QFile::remove(baseName + ".aux");
0259     QFile::remove(baseName + ".log");
0260     QFile::remove(baseName + ".dvi");
0261     QFile::remove(baseName + ".ps");
0262     QFile::remove(baseName + ".png");
0263 
0264     *success = true;
0265     return image;
0266 }
0267 
0268 bool TeXRenderer::enabled() {
0269     KConfigGroup group = KSharedConfig::openConfig()->group("Settings_Worksheet");
0270     QString engine = group.readEntry("LaTeXEngine", "");
0271     if (engine.isEmpty()) {
0272         //empty string was found in the settings (either the settings never saved or no tex engine was available during the last save)
0273         //->check whether the latex environment was installed in the meantime
0274         engine = QLatin1String("xelatex");
0275         if (!executableExists(engine)) {
0276             engine = QLatin1String("lualatex");
0277             if (!executableExists(engine)) {
0278                 engine = QLatin1String("pdflatex");
0279                 if (!executableExists(engine))
0280                     engine = QLatin1String("latex");
0281             }
0282         }
0283 
0284         if (!engine.isEmpty()) {
0285             //one of the tex engines was found -> automatically save it in the settings without any user action
0286             group.writeEntry(QLatin1String("LaTeXEngine"), engine);
0287             group.sync();
0288         }
0289     } else if (!executableExists(engine)) {
0290         WARN("LaTeX engine does not exist");
0291         return false;
0292     }
0293 
0294     //engine found, check the presence of other required tools (s.a. TeXRenderer.cpp):
0295     //to convert the generated PDF/PS files to PNG we need 'convert' from the ImageMagic package
0296     if (!executableExists(QLatin1String("convert"))) {
0297         WARN("program \"convert\" does not exist");
0298         return false;
0299     }
0300 
0301     //to convert the generated PS files to DVI we need 'dvips'
0302     if (engine == "latex") {
0303         if (!executableExists(QLatin1String("dvips"))) {
0304             WARN("program \"dvips\" does not exist");
0305             return false;
0306         }
0307     }
0308 
0309 #if defined(_WIN64)
0310     if (!executableExists(QLatin1String("gswin64c")) && !QDir(qgetenv("PROGRAMFILES") + QString("/gs")).exists()
0311         && !QDir(qgetenv("PROGRAMFILES(X86)") + QString("/gs")).exists()) {
0312         WARN("ghostscript (64bit) does not exist");
0313         return false;
0314     }
0315 #elif defined(HAVE_WINDOWS)
0316     if (!executableExists(QLatin1String("gswin32c")) && !QDir(qgetenv("PROGRAMFILES") + QString("/gs")).exists()) {
0317         WARN("ghostscript (32bit) does not exist");
0318         return false;
0319     }
0320 #endif
0321 
0322     return true;
0323 }
0324 
0325 bool TeXRenderer::executableExists(const QString& exe) {
0326     return !QStandardPaths::findExecutable(exe).isEmpty();
0327 }