File indexing completed on 2024-04-28 15:51:45
0001 /* 0002 SPDX-FileCopyrightText: 2004 Duncan Mac-Vicar Prett <duncan@kde.org> 0003 SPDX-FileCopyrightText: 2004-2005 Olivier Goffart <ogoffart@kde.org> 0004 SPDX-FileCopyrightText: 2011 Niels Ole Salscheider 0005 <niels_ole@salscheider-online.de> 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 0010 #include "latexrenderer.h" 0011 0012 #include <QDebug> 0013 0014 #include <KProcess> 0015 0016 #include <QColor> 0017 #include <QDir> 0018 #include <QFileInfo> 0019 #include <QImage> 0020 #include <QRegularExpression> 0021 #include <QStandardPaths> 0022 #include <QTemporaryFile> 0023 #include <QTextStream> 0024 0025 #include "gui/debug_ui.h" 0026 0027 namespace GuiUtils 0028 { 0029 LatexRenderer::LatexRenderer() 0030 { 0031 } 0032 0033 LatexRenderer::~LatexRenderer() 0034 { 0035 for (const QString &file : std::as_const(m_fileList)) { 0036 QFile::remove(file); 0037 } 0038 } 0039 0040 LatexRenderer::Error LatexRenderer::renderLatexInHtml(QString &html, const QColor &textColor, int fontSize, int resolution, QString &latexOutput) 0041 { 0042 if (!html.contains(QStringLiteral("$$"))) { 0043 return NoError; 0044 } 0045 0046 // this searches for $$formula$$ 0047 static const QRegularExpression rg(QStringLiteral("\\$\\$.+?\\$\\$")); 0048 QRegularExpressionMatchIterator matchIt = rg.globalMatch(html); 0049 0050 QMap<QString, QString> replaceMap; 0051 while (matchIt.hasNext()) { 0052 QRegularExpressionMatch match = matchIt.next(); 0053 const QString matchedString = match.captured(0); 0054 0055 QString formul = matchedString; 0056 // first remove the $$ delimiters on start and end 0057 formul.remove(QStringLiteral("$$")); 0058 // then trim the result, so we can skip totally empty/whitespace-only formulas 0059 formul = formul.trimmed(); 0060 if (formul.isEmpty() || !securityCheck(formul)) { 0061 continue; 0062 } 0063 0064 // unescape formula 0065 formul.replace(QLatin1String(">"), QLatin1String(">")); 0066 formul.replace(QLatin1String("<"), QLatin1String("<")); 0067 formul.replace(QLatin1String("&"), QLatin1String("&")); 0068 formul.replace(QLatin1String("""), QLatin1String("\"")); 0069 formul.replace(QLatin1String("'"), QLatin1String("\'")); 0070 formul.replace(QLatin1String("<br>"), QLatin1String(" ")); 0071 0072 QString fileName; 0073 Error returnCode = handleLatex(fileName, formul, textColor, fontSize, resolution, latexOutput); 0074 if (returnCode != NoError) { 0075 return returnCode; 0076 } 0077 0078 replaceMap[matchedString] = fileName; 0079 } 0080 0081 if (replaceMap.isEmpty()) { // we haven't found any LaTeX strings 0082 return NoError; 0083 } 0084 0085 int imagePxWidth, imagePxHeight; 0086 for (QMap<QString, QString>::ConstIterator it = replaceMap.constBegin(); it != replaceMap.constEnd(); ++it) { 0087 QImage theImage(*it); 0088 if (theImage.isNull()) { 0089 continue; 0090 } 0091 imagePxWidth = theImage.width(); 0092 imagePxHeight = theImage.height(); 0093 QString escapedLATEX = it.key().toHtmlEscaped().replace(QLatin1Char('"'), QLatin1String(""")); // we need the escape quotes because that string will be in a title="" argument, but not the \n 0094 html.replace(it.key(), 0095 QStringLiteral(" <img width=\"") + QString::number(imagePxWidth) + QStringLiteral("\" height=\"") + QString::number(imagePxHeight) + QStringLiteral("\" align=\"middle\" src=\"") + (*it) + QStringLiteral("\" alt=\"") + 0096 escapedLATEX + QStringLiteral("\" title=\"") + escapedLATEX + QStringLiteral("\" /> ")); 0097 } 0098 return NoError; 0099 } 0100 0101 bool LatexRenderer::mightContainLatex(const QString &text) 0102 { 0103 if (!text.contains(QStringLiteral("$$"))) { 0104 return false; 0105 } 0106 0107 // this searches for $$formula$$ 0108 static const QRegularExpression rg(QStringLiteral("\\$\\$.+?\\$\\$")); 0109 if (!rg.match(text).hasMatch()) { 0110 return false; 0111 } 0112 0113 return true; 0114 } 0115 0116 LatexRenderer::Error LatexRenderer::handleLatex(QString &fileName, const QString &latexFormula, const QColor &textColor, int fontSize, int resolution, QString &latexOutput) 0117 { 0118 KProcess latexProc; 0119 KProcess dvipngProc; 0120 0121 QTemporaryFile *tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/okular_kdelatex-XXXXXX.tex")); 0122 tempFile->open(); 0123 QString tempFileName = tempFile->fileName(); 0124 QFileInfo *tempFileInfo = new QFileInfo(tempFileName); 0125 QString tempFileNameNS = tempFileInfo->absolutePath() + QLatin1Char('/') + tempFileInfo->baseName(); 0126 QString tempFilePath = tempFileInfo->absolutePath(); 0127 delete tempFileInfo; 0128 QTextStream tempStream(tempFile); 0129 0130 tempStream << "\ 0131 \\documentclass[" 0132 << fontSize << "pt]{article} \ 0133 \\usepackage{color} \ 0134 \\usepackage{amsmath,latexsym,amsfonts,amssymb,ulem} \ 0135 \\pagestyle{empty} \ 0136 \\begin{document} \ 0137 {\\color[rgb]{" << textColor.redF() 0138 << "," << textColor.greenF() << "," << textColor.blueF() << "} \ 0139 \\begin{eqnarray*} \ 0140 " << latexFormula 0141 << " \ 0142 \\end{eqnarray*}} \ 0143 \\end{document}"; 0144 0145 tempFile->close(); 0146 QString latexExecutable = QStandardPaths::findExecutable(QStringLiteral("latex")); 0147 if (latexExecutable.isEmpty()) { 0148 qCDebug(OkularUiDebug) << "Could not find latex!"; 0149 delete tempFile; 0150 fileName = QString(); 0151 return LatexNotFound; 0152 } 0153 latexProc << latexExecutable << QStringLiteral("-interaction=nonstopmode") << QStringLiteral("-halt-on-error") << QStringLiteral("-output-directory=%1").arg(tempFilePath) << tempFile->fileName(); 0154 latexProc.setOutputChannelMode(KProcess::MergedChannels); 0155 latexProc.execute(); 0156 latexOutput = QString::fromLocal8Bit(latexProc.readAll()); 0157 tempFile->remove(); 0158 0159 QFile::remove(tempFileNameNS + QStringLiteral(".log")); 0160 QFile::remove(tempFileNameNS + QStringLiteral(".aux")); 0161 delete tempFile; 0162 0163 if (!QFile::exists(tempFileNameNS + QStringLiteral(".dvi"))) { 0164 fileName = QString(); 0165 return LatexFailed; 0166 } 0167 0168 QString dvipngExecutable = QStandardPaths::findExecutable(QStringLiteral("dvipng")); 0169 if (dvipngExecutable.isEmpty()) { 0170 qCDebug(OkularUiDebug) << "Could not find dvipng!"; 0171 fileName = QString(); 0172 return DvipngNotFound; 0173 } 0174 0175 dvipngProc << dvipngExecutable << QStringLiteral("-o%1").arg(tempFileNameNS + QStringLiteral(".png")) << QStringLiteral("-Ttight") << QStringLiteral("-bgTransparent") << QStringLiteral("-D %1").arg(resolution) 0176 << QStringLiteral("%1").arg(tempFileNameNS + QStringLiteral(".dvi")); 0177 dvipngProc.setOutputChannelMode(KProcess::MergedChannels); 0178 dvipngProc.execute(); 0179 0180 QFile::remove(tempFileNameNS + QStringLiteral(".dvi")); 0181 0182 if (!QFile::exists(tempFileNameNS + QStringLiteral(".png"))) { 0183 fileName = QString(); 0184 return DvipngFailed; 0185 } 0186 0187 fileName = tempFileNameNS + QStringLiteral(".png"); 0188 m_fileList << fileName; 0189 return NoError; 0190 } 0191 0192 bool LatexRenderer::securityCheck(const QString &latexFormula) 0193 { 0194 static const auto formulaRegex = 0195 QRegularExpression(QString::fromLatin1("\\\\(def|let|futurelet|newcommand|renewcommand|else|fi|write|input|include" 0196 "|chardef|catcode|makeatletter|noexpand|toksdef|every|errhelp|errorstopmode|scrollmode|nonstopmode|batchmode" 0197 "|read|csname|newhelp|relax|afterground|afterassignment|expandafter|noexpand|special|command|loop|repeat|toks" 0198 "|output|line|mathcode|name|item|section|mbox|DeclareRobustCommand)[^a-zA-Z]")); 0199 return !latexFormula.contains(formulaRegex); 0200 } 0201 0202 }