/* SPDX-FileCopyrightText: 2004 Duncan Mac-Vicar Prett SPDX-FileCopyrightText: 2004-2005 Olivier Goffart SPDX-FileCopyrightText: 2011 Niels Ole Salscheider SPDX-License-Identifier: GPL-2.0-or-later */ #include "latexrenderer.h" #include #include #include #include #include #include #include #include #include #include #include "debug_ui.h" namespace GuiUtils { LatexRenderer::LatexRenderer() { } LatexRenderer::~LatexRenderer() { for (const QString &file : qAsConst(m_fileList)) { QFile::remove(file); } } LatexRenderer::Error LatexRenderer::renderLatexInHtml(QString &html, const QColor &textColor, int fontSize, int resolution, QString &latexOutput) { if (!html.contains(QStringLiteral("$$"))) return NoError; // this searches for $$formula$$ QRegularExpression rg(QStringLiteral("\\$\\$.+?\\$\\$")); QRegularExpressionMatchIterator it = rg.globalMatch(html); QMap replaceMap; while (it.hasNext()) { QRegularExpressionMatch match = it.next(); const QString matchedString = match.captured(0); QString formul = matchedString; // first remove the $$ delimiters on start and end formul.remove(QStringLiteral("$$")); // then trim the result, so we can skip totally empty/whitespace-only formulas formul = formul.trimmed(); if (formul.isEmpty() || !securityCheck(formul)) { continue; } // unescape formula formul.replace(QLatin1String(">"), QLatin1String(">")); formul.replace(QLatin1String("<"), QLatin1String("<")); formul.replace(QLatin1String("&"), QLatin1String("&")); formul.replace(QLatin1String("""), QLatin1String("\"")); formul.replace(QLatin1String("'"), QLatin1String("\'")); formul.replace(QLatin1String("
"), QLatin1String(" ")); QString fileName; Error returnCode = handleLatex(fileName, formul, textColor, fontSize, resolution, latexOutput); if (returnCode != NoError) { return returnCode; } replaceMap[matchedString] = fileName; } if (replaceMap.isEmpty()) // we haven't found any LaTeX strings return NoError; int imagePxWidth, imagePxHeight; for (QMap::ConstIterator it = replaceMap.constBegin(); it != replaceMap.constEnd(); ++it) { QImage theImage(*it); if (theImage.isNull()) continue; imagePxWidth = theImage.width(); imagePxHeight = theImage.height(); 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 html.replace(it.key(), QStringLiteral(" \"") ")); } return NoError; } bool LatexRenderer::mightContainLatex(const QString &text) { if (!text.contains(QStringLiteral("$$"))) return false; // this searches for $$formula$$ QRegularExpression rg(QStringLiteral("\\$\\$.+?\\$\\$")); if (!rg.match(text).hasMatch()) return false; return true; } LatexRenderer::Error LatexRenderer::handleLatex(QString &fileName, const QString &latexFormula, const QColor &textColor, int fontSize, int resolution, QString &latexOutput) { KProcess latexProc; KProcess dvipngProc; QTemporaryFile *tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/okular_kdelatex-XXXXXX.tex")); tempFile->open(); QString tempFileName = tempFile->fileName(); QFileInfo *tempFileInfo = new QFileInfo(tempFileName); QString tempFileNameNS = tempFileInfo->absolutePath() + '/' + tempFileInfo->baseName(); QString tempFilePath = tempFileInfo->absolutePath(); delete tempFileInfo; QTextStream tempStream(tempFile); tempStream << "\ \\documentclass[" << fontSize << "pt]{article} \ \\usepackage{color} \ \\usepackage{amsmath,latexsym,amsfonts,amssymb,ulem} \ \\pagestyle{empty} \ \\begin{document} \ {\\color[rgb]{" << textColor.redF() << "," << textColor.greenF() << "," << textColor.blueF() << "} \ \\begin{eqnarray*} \ " << latexFormula << " \ \\end{eqnarray*}} \ \\end{document}"; tempFile->close(); QString latexExecutable = QStandardPaths::findExecutable(QStringLiteral("latex")); if (latexExecutable.isEmpty()) { qCDebug(OkularUiDebug) << "Could not find latex!"; delete tempFile; fileName = QString(); return LatexNotFound; } latexProc << latexExecutable << QStringLiteral("-interaction=nonstopmode") << QStringLiteral("-halt-on-error") << QStringLiteral("-output-directory=%1").arg(tempFilePath) << tempFile->fileName(); latexProc.setOutputChannelMode(KProcess::MergedChannels); latexProc.execute(); latexOutput = QString::fromLocal8Bit(latexProc.readAll()); tempFile->remove(); QFile::remove(tempFileNameNS + QStringLiteral(".log")); QFile::remove(tempFileNameNS + QStringLiteral(".aux")); delete tempFile; if (!QFile::exists(tempFileNameNS + QStringLiteral(".dvi"))) { fileName = QString(); return LatexFailed; } QString dvipngExecutable = QStandardPaths::findExecutable(QStringLiteral("dvipng")); if (dvipngExecutable.isEmpty()) { qCDebug(OkularUiDebug) << "Could not find dvipng!"; fileName = QString(); return DvipngNotFound; } dvipngProc << dvipngExecutable << QStringLiteral("-o%1").arg(tempFileNameNS + QStringLiteral(".png")) << QStringLiteral("-Ttight") << QStringLiteral("-bgTransparent") << QStringLiteral("-D %1").arg(resolution) << QStringLiteral("%1").arg(tempFileNameNS + QStringLiteral(".dvi")); dvipngProc.setOutputChannelMode(KProcess::MergedChannels); dvipngProc.execute(); QFile::remove(tempFileNameNS + QStringLiteral(".dvi")); if (!QFile::exists(tempFileNameNS + QStringLiteral(".png"))) { fileName = QString(); return DvipngFailed; } fileName = tempFileNameNS + QStringLiteral(".png"); m_fileList << fileName; return NoError; } bool LatexRenderer::securityCheck(const QString &latexFormula) { return !latexFormula.contains( QRegularExpression(QString::fromLatin1("\\\\(def|let|futurelet|newcommand|renewcommand|else|fi|write|input|include" "|chardef|catcode|makeatletter|noexpand|toksdef|every|errhelp|errorstopmode|scrollmode|nonstopmode|batchmode" "|read|csname|newhelp|relax|afterground|afterassignment|expandafter|noexpand|special|command|loop|repeat|toks" "|output|line|mathcode|name|item|section|mbox|DeclareRobustCommand)[^a-zA-Z]"))); } }