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 }