Commit 7f154553 authored by Nikita Sirgienko's avatar Nikita Sirgienko
Browse files

[Jupyter] Significant speed up embedded math rendering

  - Add new class - MathRenderer for rendering embedded math (the class use threadpool for async math render and poppler pdf library). On this moment, this way to render used only in MarkdownEntry, but it will be changed.
  - Disable embedded math, if pdflatex isn't installed
  - Other minor changes
parent baab526f
......@@ -42,6 +42,9 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED
XmlGui
I18n)
find_package(Poppler "0.62.0" REQUIRED COMPONENTS Qt5)
if(NOT WIN32)
find_package(KF5 ${KF5_MIN_VERSION} REQUIRED Pty)
endif()
......
......@@ -81,6 +81,8 @@ set(cantor_PART_SRCS
animation.cpp
epsrenderer.cpp
jupyterutils.cpp
mathrender.cpp
mathrendertask.cpp
)
ki18n_wrap_ui(cantor_PART_SRCS imagesettings.ui)
......@@ -101,7 +103,7 @@ set_target_properties(cantorpart PROPERTIES PREFIX "${CMAKE_SHARED_LIBRARY_PREFI
target_link_libraries(cantorpart KF5::Parts KF5::NewStuff
KF5::TextEditor ${Qt5XmlPatterns_LIBRARIES}
KF5::KIOCore KF5::KIOFileWidgets KF5::KIOWidgets
Qt5::PrintSupport cantorlibs cantor_config )
Qt5::PrintSupport Poppler::Qt5 cantorlibs cantor_config )
if(LIBSPECTRE_FOUND)
target_link_libraries(cantorpart ${LIBSPECTRE_LIBRARY})
endif(LIBSPECTRE_FOUND)
......
......@@ -31,7 +31,7 @@
<default>true</default>
</entry>
<entry name="EmbeddedMathDefault" type="Bool">
<label>Enable rendering math expressions inside $$..$$ in Text and Markdown entries by default</label>
<label>Enable rendering math expressions inside $$..$$ in Text and Markdown entries by default (needs pdflatex installed)</label>
<default>true</default>
</entry>
<entry name="AutoEval" type="Bool">
......
......@@ -256,10 +256,13 @@ CantorPart::CantorPart( QWidget *parentWidget, QObject *parent, const QVariantLi
collection->addAction(QLatin1String("enable_animations"), m_animateWorksheet);
connect(m_animateWorksheet, SIGNAL(toggled(bool)), m_worksheet, SLOT(enableAnimations(bool)));
m_embeddedMath= new KToggleAction(i18n("Embedded Math"), collection);
m_embeddedMath->setChecked(Settings::self()->embeddedMathDefault());
collection->addAction(QLatin1String("enable_embedded_math"), m_embeddedMath);
connect(m_embeddedMath, SIGNAL(toggled(bool)), m_worksheet, SLOT(enableEmbeddedMath(bool)));
if (m_worksheet->mathRenderer()->mathRenderAvailable())
{
m_embeddedMath= new KToggleAction(i18n("Embedded Math"), collection);
m_embeddedMath->setChecked(Settings::self()->embeddedMathDefault());
collection->addAction(QLatin1String("enable_embedded_math"), m_embeddedMath);
connect(m_embeddedMath, SIGNAL(toggled(bool)), m_worksheet, SLOT(enableEmbeddedMath(bool)));
}
m_restart = new QAction(i18n("Restart Backend"), collection);
collection->addAction(QLatin1String("restart_backend"), m_restart);
......
......@@ -26,8 +26,11 @@
#include <QImage>
#include <QImageReader>
#include <QBuffer>
#include <KLocalizedString>
#include <QDebug>
#include "jupyterutils.h"
#include "mathrender.h"
#include <config-cantor.h>
#ifdef Discount_FOUND
......@@ -36,8 +39,6 @@ extern "C" {
}
#endif
#include <KLocalizedString>
#include <QDebug>
MarkdownEntry::MarkdownEntry(Worksheet* worksheet) : WorksheetEntry(worksheet), m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)), rendered(false)
{
......@@ -289,53 +290,8 @@ bool MarkdownEntry::evaluate(EvaluationOption evalOp)
}
}
if (rendered && worksheet()->embeddedMathEnabled())
{
// Render math in $$...$$ via Latex
QTextCursor cursor = findLatexCode();
while (!cursor.isNull())
{
QString latexCode = cursor.selectedText();
qDebug()<<"found latex: " << latexCode;
latexCode.remove(0, 2);
latexCode.remove(latexCode.length() - 2, 2);
latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n'));
latexCode.replace(QChar::LineSeparator, QLatin1Char('\n'));
Cantor::LatexRenderer* renderer=new Cantor::LatexRenderer(this);
renderer->setLatexCode(latexCode);
renderer->setEquationOnly(true);
renderer->setEquationType(Cantor::LatexRenderer::InlineEquation);
renderer->setMethod(Cantor::LatexRenderer::LatexMethod);
renderer->renderBlocking();
bool success;
QTextImageFormat formulaFormat;
if (renderer->renderingSuccessful()) {
EpsRenderer* epsRend = worksheet()->epsRenderer();
formulaFormat = epsRend->render(m_textItem->document(), renderer);
success = !formulaFormat.name().isEmpty();
} else {
success = false;
}
qDebug()<<"rendering successful? "<<success;
if (!success) {
cursor = findLatexCode(cursor);
continue;
}
formulaFormat.setProperty(EpsRenderer::Delimiter, QLatin1String("$$"));
cursor.insertText(QString(QChar::ObjectReplacementCharacter), formulaFormat);
delete renderer;
cursor = findLatexCode(cursor);
}
}
if (worksheet()->embeddedMathEnabled())
renderMath();
evaluateNext(evalOp);
return true;
......@@ -373,10 +329,7 @@ void MarkdownEntry::updateEntry()
{
QTextImageFormat format=cursor.charFormat().toImageFormat();
if (format.hasProperty(EpsRenderer::CantorFormula))
{
const QUrl& url=QUrl::fromLocalFile(format.property(EpsRenderer::ImagePath).toString());
worksheet()->epsRenderer()->renderToResource(m_textItem->document(), url, QUrl(format.name()));
}
worksheet()->mathRenderer()->rerender(m_textItem->document(), format);
cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
}
......@@ -474,3 +427,44 @@ void MarkdownEntry::resolveImagesAtCursor()
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
cursor.insertText(m_textItem->resolveImages(cursor));
}
void MarkdownEntry::renderMath()
{
// Render math in $$...$$ via Latex
QTextCursor cursor = findLatexCode();
while (!cursor.isNull())
{
QString latexCode = cursor.selectedText();
qDebug()<<"found latex: " << latexCode;
latexCode.remove(0, 2);
latexCode.remove(latexCode.length() - 2, 2);
latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n'));
latexCode.replace(QChar::LineSeparator, QLatin1Char('\n'));
MathRenderer* renderer = worksheet()->mathRenderer();
renderer->renderExpression(latexCode, Cantor::LatexRenderer::InlineEquation, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>)));
cursor = findLatexCode(cursor);
}
}
void MarkdownEntry::handleMathRender(QSharedPointer<MathRenderResult> result)
{
if (!result->successfull)
{
qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage;
return;
}
const QString& code = result->renderedMath.property(EpsRenderer::Code).toString();
// Jupyter TODO: add support for $...$ math and starts use
// result->renderedMath.property(EpsRenderer::Delimiter).toString();
const QString& delimiter = QLatin1String("$$");
QTextCursor cursor = m_textItem->document()->find(delimiter + code + delimiter);
if (!cursor.isNull())
{
m_textItem->document()->addResource(QTextDocument::ImageResource, result->uniqueUrl, QVariant(result->image));
cursor.insertText(QString(QChar::ObjectReplacementCharacter), result->renderedMath);
}
}
......@@ -23,9 +23,13 @@
#include <vector>
#include <QSharedPointer>
#include "worksheetentry.h"
#include "worksheettextitem.h"
#include "mathrendertask.h"
class QJsonObject;
class MarkdownEntry : public WorksheetEntry
......@@ -73,6 +77,10 @@ class MarkdownEntry : public WorksheetEntry
void setRenderedHtml(const QString& html);
void setPlainText(const QString& plain);
QTextCursor findLatexCode(const QTextCursor& cursor = QTextCursor()) const;
void renderMath();
protected Q_SLOTS:
void handleMathRender(QSharedPointer<MathRenderResult> result);
protected:
WorksheetTextItem* m_textItem;
......
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2019 Sirgienko Nikita <warquark@gmail.com>
*/
#include "mathrender.h"
#include <QThreadPool>
#include <QDebug>
#include <QStandardPaths>
#include <QFile>
#include "mathrendertask.h"
#include "epsrenderer.h"
MathRenderer::MathRenderer(): m_scale(1.0), m_useHighRes(false)
{
qRegisterMetaType<QSharedPointer<MathRenderResult>>();
}
MathRenderer::~MathRenderer()
{
}
bool MathRenderer::mathRenderAvailable()
{
return QStandardPaths::findExecutable(QLatin1String("pdflatex")).isEmpty() == false;
}
qreal MathRenderer::scale()
{
return m_scale;
}
void MathRenderer::setScale(qreal scale)
{
m_scale = scale;
}
void MathRenderer::useHighResolution(bool b)
{
m_useHighRes = b;
}
void MathRenderer::renderExpression(const QString& mathExpression, Cantor::LatexRenderer::EquationType type, const QObject* receiver, const char* resultHandler)
{
MathRenderTask* task = new MathRenderTask(mathExpression, type, m_scale, m_useHighRes);
task->setHandler(receiver, resultHandler);
task->setAutoDelete(false);
QThreadPool::globalInstance()->start(task);
}
void MathRenderer::rerender(QTextDocument* document, const QTextImageFormat& math)
{
const QString& filename = math.property(EpsRenderer::ImagePath).toString();
if (!QFile::exists(filename))
return;
bool success; QString errorMessage;
QImage img = MathRenderTask::renderPdf(filename, m_scale, m_useHighRes, &success, nullptr, &errorMessage);
if (success)
{
QUrl internal(math.name());
document->addResource(QTextDocument::ImageResource, internal, QVariant(img));
}
else
{
qDebug() << "Rerender embedded math failed with message: " << errorMessage;
}
}
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2019 Sirgienko Nikita <warquark@gmail.com>
*/
#ifndef MATHRENDER_H
#define MATHRENDER_H
#include <QObject>
#include <QTextImageFormat>
#include "lib/latexrenderer.h"
/**
* Special class for renderning embeded math in MarkdownEntry and TextEntry
* Instead of LatexRenderer+EpsRenderer provide all needed functianality in one class
* Even if we add some speed optimization in future, API of the class probably won't change
*/
class MathRenderer : public QObject {
Q_OBJECT
public:
MathRenderer();
~MathRenderer();
bool mathRenderAvailable();
// Resulution contol
void setScale(qreal scale);
qreal scale();
void useHighResolution(bool b);
/**
* This function will run render task in Qt thread pool and
* call resultHandler SLOT with MathRenderResult* argument on finish
* receiver will be managed about pointer, task only create it
*/
void renderExpression(
const QString& mathExpression,
Cantor::LatexRenderer::EquationType type,
const QObject *receiver,
const char *resultHandler);
/**
* Rerender renderer math expression in document
* Unlike MathRender::renderExpression this method isn't async, because
* rerender already rendered math is not long operation
*/
void rerender(QTextDocument* document, const QTextImageFormat& math);
private:
double m_scale;
bool m_useHighRes;
};
#endif /* MATHRENDER_H */
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2019 Sirgienko Nikita <warquark@gmail.com>
*/
#include "mathrendertask.h"
#include <QTemporaryFile>
#include <QStandardPaths>
#include <QUuid>
#include <QDir>
#include <KColorScheme>
#include <KProcess>
#include <QScopedPointer>
#include <poppler-qt5.h>
#include "epsrenderer.h"
static const QLatin1String mathTex("\\documentclass{minimal}"\
"\\usepackage{latexsym,amsfonts,amssymb,ulem}"\
"\\usepackage{amsmath}"\
"\\usepackage[dvips]{graphicx}"\
"\\usepackage[utf8]{inputenc}"\
"\\usepackage{xcolor}"\
"\\usepackage[active,displaymath,textmath,tightpage]{preview}"\
"\\setlength\\textwidth{5in}"\
"\\setlength{\\parindent}{0pt}"\
"\\pagecolor[rgb]{%1,%2,%3}"\
"\\pagestyle{empty}"\
"\\begin{document}"\
"\\begin{preview}"\
"\\color[rgb]{%4,%5,%6}"\
"%7"\
"\\end{preview}"\
"\\end{document}");
static const QLatin1String eqnHeader("\\begin{eqnarray*}%1\\end{eqnarray*}");
static const QLatin1String inlineEqnHeader("$%1$");
MathRenderTask::MathRenderTask(
const QString& code,
Cantor::LatexRenderer::EquationType type,
double scale,
bool highResolution
): m_code(code), m_type(type), m_scale(scale), m_highResolution(highResolution)
{}
void MathRenderTask::setHandler(const QObject* receiver, const char* resultHandler)
{
connect(this, SIGNAL(finish(QSharedPointer<MathRenderResult>)), receiver, resultHandler);
}
void MathRenderTask::run()
{
QSharedPointer<MathRenderResult> result(new MathRenderResult());
const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QTemporaryFile texFile(dir + QDir::separator() + QLatin1String("cantor_tex-XXXXXX.tex"));
texFile.open();
KColorScheme scheme(QPalette::Active);
const QColor &backgroundColor=scheme.background().color();
const QColor &foregroundColor=scheme.foreground().color();
QString expressionTex=mathTex;
expressionTex=expressionTex
.arg(backgroundColor.redF()).arg(backgroundColor.greenF()).arg(backgroundColor.blueF())
.arg(foregroundColor.redF()).arg(foregroundColor.greenF()).arg(foregroundColor.blueF());
switch(m_type)
{
case Cantor::LatexRenderer::FullEquation: expressionTex=expressionTex.arg(eqnHeader); break;
case Cantor::LatexRenderer::InlineEquation: expressionTex=expressionTex.arg(inlineEqnHeader); break;
}
expressionTex=expressionTex.arg(m_code);
texFile.write(expressionTex.toUtf8());
texFile.flush();
KProcess p;
p.setWorkingDirectory(dir);
// Create unique uuid for this job
// It will be used as pdf filename, for preventing names collisions
// And as internal url path too
QString uuid = QUuid::createUuid().toString();
uuid.remove(0, 1);
uuid.chop(1);
uuid.replace(QLatin1Char('-'), QLatin1Char('_'));
const QString& pdflatex = QStandardPaths::findExecutable(QLatin1String("pdflatex"));
p << pdflatex << QStringLiteral("-interaction=batchmode") << QStringLiteral("-jobname=cantor_") + uuid << QStringLiteral("-halt-on-error") << texFile.fileName();
p.start();
p.waitForFinished();
if (p.exitCode() != 0)
{
// pdflatex render failed and we haven't pdf file
result->successfull = false;
result->errorMessage = QString::fromLatin1("pdflatex failed to render pdf and exit with code %1").arg(p.exitCode());
finalize(result);
return;
}
//Clean up .aux and .log files
QString pathWithoutExtention = dir + QDir::separator() + QLatin1String("cantor_")+uuid;
QFile::remove(pathWithoutExtention + QLatin1String(".log"));
QFile::remove(pathWithoutExtention + QLatin1String(".aux"));
const QString& pdfFileName = pathWithoutExtention + QLatin1String(".pdf");
bool success; QString errorMessage; QSizeF size;
result->image = renderPdf(pdfFileName, m_scale, m_highResolution, &success, &size, &errorMessage);
result->successfull = success;
result->errorMessage = errorMessage;
if (success == false)
{
finalize(result);
return;
}
QTextImageFormat format;
QUrl internal;
internal.setScheme(QLatin1String("internal"));
internal.setPath(uuid);
format.setName(internal.url());
format.setWidth(size.width());
format.setHeight(size.height());
format.setProperty(EpsRenderer::CantorFormula, m_type);
format.setProperty(EpsRenderer::ImagePath, pdfFileName);
format.setProperty(EpsRenderer::Code, m_code);
switch(m_type)
{
case Cantor::LatexRenderer::FullEquation:
format.setProperty(EpsRenderer::Delimiter, QLatin1String("$$"));
break;
case Cantor::LatexRenderer::InlineEquation:
format.setProperty(EpsRenderer::Delimiter, QLatin1String("$"));
break;
}
result->renderedMath = format;
result->uniqueUrl = internal;
finalize(result);
}
void MathRenderTask::finalize(QSharedPointer<MathRenderResult> result)
{
emit finish(result);
deleteLater();
}
QImage MathRenderTask::renderPdf(const QString& filename, double scale, bool highResolution, bool* success, QSizeF* size, QString* errorReason)
{
QScopedPointer<Poppler::Document> document(Poppler::Document::load(filename));
if (document == nullptr)
{
if (success)
*success = false;
if (errorReason)
*errorReason = QString::fromLatin1("Poppler library have failed to open file % as pdf").arg(filename);
return QImage();
}
QScopedPointer<Poppler::Page> pdfPage(document->page(0));
if (pdfPage == nullptr) {
if (success)
*success = false;
if (errorReason)
*errorReason = QString::fromLatin1("Poppler library failed to access first page of %1 document").arg(filename);
return QImage();
}
QSize pageSize = pdfPage->pageSize();
double pscale;
qreal w, h;
if(highResolution) {
pscale=1.2 * 4.8 * 1.8;
w = 1.2 * pageSize.width();
h = 1.2 * pageSize.height();
} else {
pscale = 2.3 * scale * 1.8;
w = 2.3 * pageSize.width();
h = 2.3 * pageSize.height();
}
QImage image = pdfPage->renderToImage(72.0*pscale, 72.0*pscale, 0, 0, pageSize.width()*pscale, pageSize.height()*pscale);
if (image.isNull())
{
if (success)
*success = false;
if (errorReason)
*errorReason = QString::fromLatin1("Poppler library failed to render pdf %1 to image").arg(filename);
return image;
}
// Resize with smooth transformation for more beautiful result
image = image.convertToFormat(QImage::Format_ARGB32).scaled(image.size()/1.8, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
if (success)
*success = true;
if (size)
*size = QSizeF(w, h);
return image;
}
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the