File indexing completed on 2024-05-05 03:48:22

0001 /*
0002     File                 : TeXRenderer.cc
0003     Project              : LabPlot
0004     Description          : TeX renderer class
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2008-2021 Alexander Semke <alexander.semke@web.de>
0007     SPDX-FileCopyrightText: 2012-2021 Stefan Gerlach <stefan.gerlach@uni-konstanz.de>
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "TeXRenderer.h"
0012 #include "backend/core/Settings.h"
0013 #include "backend/lib/macros.h"
0014 #include "kdefrontend/GuiTools.h"
0015 
0016 #include <KConfigGroup>
0017 #include <KLocalizedString>
0018 
0019 #include <QColor>
0020 #include <QDir>
0021 #include <QImage>
0022 #include <QProcess>
0023 #include <QRegularExpression>
0024 #include <QStandardPaths>
0025 #include <QTemporaryFile>
0026 #include <QTextStream>
0027 
0028 #ifdef HAVE_POPPLER
0029 #include <poppler-qt5.h>
0030 #endif
0031 
0032 /*!
0033     \class TeXRenderer
0034     \brief Implements rendering of latex code to a PNG image.
0035 
0036     Uses latex engine specified by the user (default xelatex) to render LaTeX text
0037 
0038     \ingroup tools
0039 */
0040 QByteArray TeXRenderer::renderImageLaTeX(const QString& teXString, Result* res, const TeXRenderer::Formatting& format) {
0041     const QColor& fontColor = format.fontColor;
0042     const QColor& backgroundColor = format.backgroundColor;
0043     const int fontSize = format.fontSize;
0044     const QString& fontFamily = format.fontFamily;
0045     const int dpi = format.dpi;
0046 
0047     // determine the temp directory where the produced files are going to be created
0048     QString tempPath;
0049 #ifdef Q_OS_LINUX
0050     // on linux try to use shared memory device first if available
0051     static bool useShm = QDir(QStringLiteral("/dev/shm/")).exists();
0052     if (useShm)
0053         tempPath = QStringLiteral("/dev/shm/");
0054     else
0055         tempPath = QDir::tempPath();
0056 #else
0057     tempPath = QDir::tempPath();
0058 #endif
0059 
0060     // make sure we have preview.sty available
0061     if (!tempPath.contains(QLatin1String("preview.sty"))) {
0062         QString file = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("latex/preview.sty"));
0063         if (file.isEmpty()) {
0064             QString err = i18n("Couldn't find preview.sty.");
0065             WARN(err.toStdString());
0066             res->successful = false;
0067             res->errorMessage = err;
0068             return {};
0069         } else
0070             QFile::copy(file, tempPath + QLatin1String("/") + QLatin1String("preview.sty"));
0071     }
0072 
0073     // create a temporary file
0074     QTemporaryFile file(tempPath + QStringLiteral("/") + QStringLiteral("labplot_XXXXXX.tex"));
0075     // FOR DEBUG: file.setAutoRemove(false);
0076     // DEBUG("temp file path = " << file.fileName().toUtf8().constData());
0077     if (file.open()) {
0078         QDir::setCurrent(tempPath);
0079     } else {
0080         QString err = i18n("Couldn't open the file") + QStringLiteral(" ") + file.fileName();
0081         WARN(err.toStdString());
0082         res->successful = false;
0083         res->errorMessage = err;
0084         return {};
0085     }
0086 
0087     // determine latex engine to be used
0088     const auto& group = Settings::group(QStringLiteral("Settings_Worksheet"));
0089     const auto& engine = group.readEntry("LaTeXEngine", "pdflatex");
0090 
0091     // create latex code
0092     QTextStream out(&file);
0093     int headerIndex = teXString.indexOf(QLatin1String("\\begin{document}"));
0094     QString body;
0095     if (headerIndex != -1) {
0096         // user provided a complete latex document -> extract the document header and body
0097         QString header = teXString.left(headerIndex);
0098         int footerIndex = teXString.indexOf(QLatin1String("\\end{document}"));
0099         body = teXString.mid(headerIndex + 16, footerIndex - headerIndex - 16);
0100         out << header;
0101     } else {
0102         // user simply provided a document body (assume it's a math. expression) -> add a minimal header
0103         out << "\\documentclass{minimal}";
0104         if (teXString.indexOf(QLatin1Char('$')) == -1)
0105             body = QLatin1Char('$') + teXString + QLatin1Char('$');
0106         else
0107             body = teXString;
0108 
0109         // replace line breaks with tex command for a line break '\\'
0110         body = body.replace(QLatin1String("\n"), QLatin1String("\\\\"));
0111     }
0112 
0113     if (engine == QLatin1String("xelatex") || engine == QLatin1String("lualatex")) {
0114         out << "\\usepackage{fontspec}";
0115         out << "\\defaultfontfeatures{Ligatures=TeX}";
0116         if (!fontFamily.isEmpty())
0117             out << "\\setmainfont[Mapping=tex-text]{" << fontFamily << "}";
0118     }
0119 
0120     out << "\\usepackage{color}";
0121     out << "\\usepackage[active,displaymath,textmath,tightpage]{preview}";
0122     out << "\\setlength\\PreviewBorder{0pt}";
0123     // TODO: this fails with pdflatex
0124     // out << "\\usepackage{mathtools}";
0125     out << "\\begin{document}";
0126     out << "\\begin{preview}";
0127     out << "\\setlength{\\fboxsep}{1.0pt}";
0128     out << "\\colorbox[rgb]{" << backgroundColor.redF() << ',' << backgroundColor.greenF() << ',' << backgroundColor.blueF() << "}{";
0129     out << "\\fontsize{" << QString::number(fontSize) << "}{" << QString::number(fontSize) << "}\\selectfont";
0130     out << "\\color[rgb]{" << fontColor.redF() << ',' << fontColor.greenF() << ',' << fontColor.blueF() << "}";
0131     out << body;
0132     out << "}";
0133     out << "\\end{preview}";
0134     out << "\\end{document}";
0135     out.flush();
0136 
0137     if (engine == QLatin1String("latex"))
0138         return imageFromDVI(file, dpi, res);
0139     else
0140         return imageFromPDF(file, engine, res);
0141 }
0142 
0143 bool TeXRenderer::executeLatexProcess(const QString engine,
0144                                       const QString& baseName,
0145                                       const QTemporaryFile& file,
0146                                       const QString& resultFileExtension,
0147                                       Result* res) {
0148     // latex: produce the DVI file
0149     const QString engineFullPath = QStandardPaths::findExecutable(engine);
0150     if (engineFullPath.isEmpty()) {
0151         res->successful = false;
0152         res->errorMessage = i18n("%1 not found").arg(engine);
0153         WARN(QStringLiteral("%1 not found").arg(engine).toStdString());
0154         return {};
0155     }
0156 
0157     WARN(QStringLiteral("Engine fullpath: %1").arg(engineFullPath).toStdString());
0158 
0159     QProcess latexProcess;
0160     latexProcess.start(engineFullPath, QStringList() << QStringLiteral("-interaction=batchmode") << file.fileName());
0161 
0162     WARN(QStringLiteral("Workdir: %1").arg(QDir::currentPath()).toStdString());
0163 
0164     bool finished = latexProcess.waitForFinished();
0165     if (!finished || latexProcess.exitCode() != 0) {
0166         QFile logFile(baseName + QStringLiteral(".log"));
0167         QString errorLogs;
0168         WARN(QStringLiteral("executeLatexProcess: logfile: %1").arg(QFileInfo(logFile).absoluteFilePath()).toStdString());
0169         if (logFile.open(QIODevice::ReadOnly)) {
0170             // really slow, but texrenderer is running asynchronous so it is not a problem
0171             while (!logFile.atEnd()) {
0172                 const auto line = logFile.readLine();
0173                 if (line.count() > 0 && line.at(0) == '!') {
0174                     errorLogs += QLatin1String(line);
0175                     break; // only first error message is enough
0176                 }
0177             }
0178             logFile.close();
0179         } else
0180             WARN(QStringLiteral("Unable to open logfile").toStdString());
0181 
0182         WARN(latexProcess.readAllStandardOutput().toStdString());
0183         WARN(latexProcess.readAllStandardError().toStdString());
0184 
0185         QString err;
0186         if (errorLogs.isEmpty()) {
0187             if (!finished) {
0188                 err = i18n("Timeout: Unable to generate latex file");
0189                 WARN(QStringLiteral("Timeout: Unable to generate latex file").toStdString());
0190             } else {
0191                 err = QStringLiteral("latex ") + i18n("process failed, exit code =") + QStringLiteral(" ") + QString::number(latexProcess.exitCode())
0192                     + QStringLiteral("\n");
0193                 WARN(QStringLiteral("latex process failed, exit code = %1").arg(latexProcess.exitCode()).toStdString());
0194             }
0195         } else {
0196             err = errorLogs;
0197             WARN(err.toStdString());
0198         }
0199 
0200         res->successful = false;
0201         res->errorMessage = err;
0202         QFile::remove(baseName + QStringLiteral(".aux"));
0203         QFile::remove(logFile.fileName());
0204         QFile::remove(baseName + QStringLiteral(".%1").arg(resultFileExtension)); // in some cases the file was also created
0205         return false;
0206     }
0207     res->successful = true;
0208     res->errorMessage = QStringLiteral("");
0209     return true;
0210 }
0211 
0212 // TEX -> PDF -> QImage
0213 QByteArray TeXRenderer::imageFromPDF(const QTemporaryFile& file, const QString& engine, Result* res) {
0214     // DEBUG(Q_FUNC_INFO << ", tmp file = " << STDSTRING(file.fileName()) << ", engine = " << STDSTRING(engine))
0215     QFileInfo fi(file.fileName());
0216     const QString& baseName = fi.completeBaseName();
0217 
0218     if (!executeLatexProcess(engine, baseName, file, QStringLiteral("pdf"), res))
0219         return {};
0220 
0221     // Can we move this into executeLatexProcess?
0222     QFile::remove(baseName + QStringLiteral(".aux"));
0223     QFile::remove(baseName + QStringLiteral(".log"));
0224 
0225     // read PDF file
0226     QFile pdfFile(baseName + QStringLiteral(".pdf"));
0227     if (!pdfFile.open(QIODevice::ReadOnly)) {
0228         QFile::remove(baseName + QStringLiteral(".pdf"));
0229         return {};
0230     }
0231 
0232     QByteArray ba = pdfFile.readAll();
0233     pdfFile.close();
0234     QFile::remove(baseName + QStringLiteral(".pdf"));
0235     res->successful = true;
0236     res->errorMessage = QString();
0237 
0238     return ba;
0239 }
0240 
0241 // TEX -> DVI -> PS -> PNG
0242 QByteArray TeXRenderer::imageFromDVI(const QTemporaryFile& file, const int dpi, Result* res) {
0243     QFileInfo fi(file.fileName());
0244     const QString& baseName = fi.completeBaseName();
0245 
0246     if (!executeLatexProcess(QLatin1String("latex"), baseName, file, QStringLiteral("dvi"), res))
0247         return {};
0248 
0249     // dvips: DVI -> PS
0250     const QString dvipsFullPath = QStandardPaths::findExecutable(QLatin1String("dvips"));
0251     if (dvipsFullPath.isEmpty()) {
0252         res->successful = false;
0253         res->errorMessage = i18n("dvips not found");
0254         WARN("dvips not found");
0255         return {};
0256     }
0257     QProcess dvipsProcess;
0258     dvipsProcess.start(dvipsFullPath, QStringList() << QStringLiteral("-E") << baseName);
0259     if (!dvipsProcess.waitForFinished() || dvipsProcess.exitCode() != 0) {
0260         QString err = i18n("dvips process failed, exit code =") + QStringLiteral(" ") + QString::number(dvipsProcess.exitCode());
0261         WARN(err.toStdString());
0262         res->successful = false;
0263         res->errorMessage = err;
0264         QFile::remove(baseName + QStringLiteral(".aux"));
0265         QFile::remove(baseName + QStringLiteral(".log"));
0266         QFile::remove(baseName + QStringLiteral(".dvi"));
0267         return {};
0268     }
0269 
0270     // convert: PS -> PNG
0271     QProcess convertProcess;
0272 #if defined(HAVE_WINDOWS)
0273     // need to set path to magick coder modules (which are in the labplot2 directory)
0274     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0275     env.insert(QStringLiteral("MAGICK_CODER_MODULE_PATH"), QString::fromLocal8Bit(qgetenv("PROGRAMFILES")) + QStringLiteral("\\labplot2"));
0276     convertProcess.setProcessEnvironment(env);
0277 #endif
0278     const QString convertFullPath = QStandardPaths::findExecutable(QLatin1String("convert"));
0279     if (convertFullPath.isEmpty()) {
0280         WARN("convert not found");
0281         res->successful = false;
0282         res->errorMessage = i18n("convert not found");
0283         return {};
0284     }
0285 
0286     const QStringList params{QStringLiteral("-density"), QString::number(dpi), baseName + QStringLiteral(".ps"), baseName + QStringLiteral(".pdf")};
0287     convertProcess.start(convertFullPath, params);
0288 
0289     if (!convertProcess.waitForFinished() || convertProcess.exitCode() != 0) {
0290         QString err = i18n("convert process failed, exit code =") + QStringLiteral(" ") + QString::number(convertProcess.exitCode());
0291         WARN(err.toStdString());
0292         res->successful = false;
0293         res->errorMessage = err;
0294         QFile::remove(baseName + QStringLiteral(".aux"));
0295         QFile::remove(baseName + QStringLiteral(".log"));
0296         QFile::remove(baseName + QStringLiteral(".dvi"));
0297         QFile::remove(baseName + QStringLiteral(".ps"));
0298         return {};
0299     }
0300 
0301     // final clean up
0302     QFile::remove(baseName + QStringLiteral(".aux"));
0303     QFile::remove(baseName + QStringLiteral(".log"));
0304     QFile::remove(baseName + QStringLiteral(".dvi"));
0305     QFile::remove(baseName + QStringLiteral(".ps"));
0306 
0307     // read PDF file
0308     QFile pdfFile(baseName + QLatin1String(".pdf"));
0309     if (!pdfFile.open(QIODevice::ReadOnly)) {
0310         QFile::remove(baseName + QStringLiteral(".pdf"));
0311         res->successful = false;
0312         res->errorMessage = i18n("Unable to open file:") + pdfFile.fileName();
0313         return {};
0314     }
0315 
0316     QByteArray ba = pdfFile.readAll();
0317     QFile::remove(baseName + QStringLiteral(".pdf"));
0318     res->successful = true;
0319     res->errorMessage = QString();
0320 
0321     return ba;
0322 }
0323 
0324 bool TeXRenderer::enabled() {
0325     KConfigGroup group = Settings::group(QStringLiteral("Settings_Worksheet"));
0326     QString engine = group.readEntry("LaTeXEngine", "");
0327     if (engine.isEmpty()) {
0328         // empty string was found in the settings (either the settings never saved or no tex engine was available during the last save)
0329         //->check whether the latex environment was installed in the meantime
0330         engine = QLatin1String("xelatex");
0331         if (!executableExists(engine)) {
0332             engine = QLatin1String("lualatex");
0333             if (!executableExists(engine)) {
0334                 engine = QLatin1String("pdflatex");
0335                 if (!executableExists(engine))
0336                     engine = QLatin1String("latex");
0337             }
0338         }
0339 
0340         if (!engine.isEmpty()) {
0341             // one of the tex engines was found -> automatically save it in the settings without any user action
0342             group.writeEntry(QLatin1String("LaTeXEngine"), engine);
0343             group.sync();
0344         }
0345     } else if (!executableExists(engine)) {
0346         WARN("LaTeX engine does not exist");
0347         return false;
0348     }
0349 
0350     // Tools needed to convert generated  DVI files to PS and PDF
0351     if (engine == QLatin1String("latex")) {
0352         if (!executableExists(QLatin1String("convert"))) {
0353             WARN("program \"convert\" does not exist");
0354             return false;
0355         }
0356         if (!executableExists(QLatin1String("dvips"))) {
0357             WARN("program \"dvips\" does not exist");
0358             return false;
0359         }
0360 
0361 #if defined(_WIN64)
0362         if (!executableExists(QLatin1String("gswin64c")) && !QDir(QString::fromLocal8Bit(qgetenv("PROGRAMFILES")) + QStringLiteral("/gs")).exists()
0363             && !QDir(QString::fromLocal8Bit(qgetenv("PROGRAMFILES(X86)")) + QStringLiteral("/gs")).exists()) {
0364             WARN("ghostscript (64bit) does not exist");
0365             return false;
0366         }
0367 #elif defined(HAVE_WINDOWS)
0368         if (!executableExists(QLatin1String("gswin32c")) && !QDir(QString::fromLocal8Bit(qgetenv("PROGRAMFILES")) + QStringLiteral("/gs")).exists()) {
0369             WARN("ghostscript (32bit) does not exist");
0370             return false;
0371         }
0372 #endif
0373     }
0374 
0375     return true;
0376 }
0377 
0378 bool TeXRenderer::executableExists(const QString& exe) {
0379     return !QStandardPaths::findExecutable(exe).isEmpty();
0380 }