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("&gt;"), QLatin1String(">"));
0066         formul.replace(QLatin1String("&lt;"), QLatin1String("<"));
0067         formul.replace(QLatin1String("&amp;"), QLatin1String("&"));
0068         formul.replace(QLatin1String("&quot;"), QLatin1String("\""));
0069         formul.replace(QLatin1String("&apos;"), 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("&quot;")); // 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 }