diff --git a/src/markdownentry.cpp b/src/markdownentry.cpp index ad25e8a2..a766cff5 100644 --- a/src/markdownentry.cpp +++ b/src/markdownentry.cpp @@ -1,530 +1,594 @@ /* 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) 2018 Yifei Wu */ #include "markdownentry.h" #include #include #include #include #include #include #include #include +#include +#include #include "jupyterutils.h" #include "mathrender.h" #include #ifdef Discount_FOUND extern "C" { #include } #endif MarkdownEntry::MarkdownEntry(Worksheet* worksheet) : WorksheetEntry(worksheet), m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)), rendered(false) { m_textItem->enableRichText(false); m_textItem->setOpenExternalLinks(true); m_textItem->installEventFilter(this); connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &MarkdownEntry::moveToPreviousEntry); connect(m_textItem, &WorksheetTextItem::moveToNext, this, &MarkdownEntry::moveToNextEntry); connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate())); } void MarkdownEntry::populateMenu(QMenu* menu, QPointF pos) { bool imageSelected = false; QTextCursor cursor = m_textItem->textCursor(); const QChar repl = QChar::ObjectReplacementCharacter; if (cursor.hasSelection()) { QString selection = m_textItem->textCursor().selectedText(); imageSelected = selection.contains(repl); } else { // we need to try both the current cursor and the one after the that cursor = m_textItem->cursorForPosition(pos); for (int i = 2; i; --i) { int p = cursor.position(); if (m_textItem->document()->characterAt(p-1) == repl && cursor.charFormat().hasProperty(EpsRenderer::CantorFormula)) { m_textItem->setTextCursor(cursor); imageSelected = true; break; } cursor.movePosition(QTextCursor::NextCharacter); } } if (imageSelected) { menu->addAction(i18n("Show LaTeX code"), this, &MarkdownEntry::resolveImagesAtCursor); menu->addSeparator(); } WorksheetEntry::populateMenu(menu, pos); } bool MarkdownEntry::isEmpty() { return m_textItem->document()->isEmpty(); } int MarkdownEntry::type() const { return Type; } bool MarkdownEntry::acceptRichText() { return false; } bool MarkdownEntry::focusEntry(int pos, qreal xCoord) { if (aboutToBeRemoved()) return false; m_textItem->setFocusAt(pos, xCoord); return true; } void MarkdownEntry::setContent(const QString& content) { rendered = false; plain = content; setPlainText(plain); } void MarkdownEntry::setContent(const QDomElement& content, const KZip& file) { - Q_UNUSED(file); - rendered = content.attribute(QLatin1String("rendered"), QLatin1String("1")) == QLatin1String("1"); QDomElement htmlEl = content.firstChildElement(QLatin1String("HTML")); if(!htmlEl.isNull()) html = htmlEl.text(); else { html = QLatin1String(""); rendered = false; // No html provided. Assume that it hasn't been rendered. } QDomElement plainEl = content.firstChildElement(QLatin1String("Plain")); if(!plainEl.isNull()) plain = plainEl.text(); else { plain = QLatin1String(""); html = QLatin1String(""); // No plain text provided. The entry shouldn't render anything, or the user can't re-edit it. } const QDomNodeList& attachments = content.elementsByTagName(QLatin1String("Attachment")); for (int x = 0; x < attachments.count(); x++) { const QDomElement& attachment = attachments.at(x).toElement(); QUrl url(attachment.attribute(QLatin1String("url"))); const QString& base64 = attachment.text(); QImage image; image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), "PNG"); attachedImages.push_back(std::make_pair(url, QLatin1String("image/png"))); m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(image)); } if(rendered) setRenderedHtml(html); else setPlainText(plain); // Handle math after setting html const QDomNodeList& maths = content.elementsByTagName(QLatin1String("EmbeddedMath")); for (int i = 0; i < maths.count(); i++) { const QDomElement& math = maths.at(i).toElement(); bool mathRendered = math.attribute(QLatin1String("rendered")).toInt(); const QString mathCode = math.text(); foundMath.push_back(std::make_pair(mathCode, mathRendered)); if (rendered && mathRendered) - renderMathExpression(mathCode); + { + const KArchiveEntry* imageEntry=file.directory()->entry(math.attribute(QLatin1String("path"))); + if (imageEntry && imageEntry->isFile()) + { + const KArchiveFile* imageFile=static_cast(imageEntry); + const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation); + imageFile->copyTo(dir); + const QString& pdfPath = dir + QDir::separator() + imageFile->name(); + + QString latex; + Cantor::LatexRenderer::EquationType type; + std::tie(latex, type) = parseMathCode(mathCode); + + // Get uuid by removing 'cantor_' and '.pdf' extention + // len('cantor_') == 7, len('.pdf') == 4 + QString uuid = pdfPath; + uuid.remove(0, 7); + uuid.chop(4); + + bool success; + const auto& data = worksheet()->mathRenderer()->renderExpressionFromPdf(pdfPath, uuid, latex, type, &success); + if (success) + { + QUrl internal; + internal.setScheme(QLatin1String("internal")); + internal.setPath(uuid); + setRenderedMath(data.first, internal, data.second); + } + } + else + renderMathExpression(mathCode); + } } } void MarkdownEntry::setContentFromJupyter(const QJsonObject& cell) { if (!JupyterUtils::isMarkdownCell(cell)) return; // https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata // There isn't Jupyter metadata for markdown cells, which could be handled by Cantor const QJsonObject attachments = cell.value(QLatin1String("attachments")).toObject(); for (const QString& key : attachments.keys()) { const QJsonValue& attachment = attachments.value(key); const QString& mimeKey = JupyterUtils::firstImageKey(attachment); if (!mimeKey.isEmpty()) { const QImage& image = JupyterUtils::loadImage(attachment, mimeKey); QUrl resourceUrl; resourceUrl.setUrl(QLatin1String("attachment:")+key); attachedImages.push_back(std::make_pair(resourceUrl, mimeKey)); m_textItem->document()->addResource(QTextDocument::ImageResource, resourceUrl, QVariant(image)); } } setPlainText(JupyterUtils::getSource(cell)); } QDomElement MarkdownEntry::toXml(QDomDocument& doc, KZip* archive) { - Q_UNUSED(archive) - if(!rendered) plain = m_textItem->toPlainText(); QDomElement el = doc.createElement(QLatin1String("Markdown")); el.setAttribute(QLatin1String("rendered"), (int)rendered); QDomElement plainEl = doc.createElement(QLatin1String("Plain")); plainEl.appendChild(doc.createTextNode(plain)); el.appendChild(plainEl); QDomElement htmlEl = doc.createElement(QLatin1String("HTML")); htmlEl.appendChild(doc.createTextNode(html)); el.appendChild(htmlEl); QUrl url; QString key; for (const auto& data : attachedImages) { std::tie(url, key) = std::move(data); QDomElement attachmentEl = doc.createElement(QLatin1String("Attachment")); attachmentEl.setAttribute(QStringLiteral("url"), url.toString()); const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value(); QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); attachmentEl.appendChild(doc.createTextNode(QString::fromLatin1(ba.toBase64()))); el.appendChild(attachmentEl); - - /* - QString attachmentKey = url.toString().remove(QLatin1String("attachment:")); - attachments.insert(attachmentKey, JupyterUtils::packMimeBundle(image, key)); - */ } + // If math rendered, then append result .pdf to archive + QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter)); for (const auto& data : foundMath) { QDomElement mathEl = doc.createElement(QLatin1String("EmbeddedMath")); mathEl.setAttribute(QStringLiteral("rendered"), data.second); mathEl.appendChild(doc.createTextNode(data.first)); + if (data.second) + { + bool foundNeededImage = false; + while(!cursor.isNull() && !foundNeededImage) + { + QTextImageFormat format=cursor.charFormat().toImageFormat(); + if (format.hasProperty(EpsRenderer::CantorFormula)) + { + const QString& latex = format.property(EpsRenderer::Code).toString(); + const QString& delimiter = format.property(EpsRenderer::Delimiter).toString(); + const QString& code = delimiter + latex + delimiter; + if (code == data.first) + { + const QUrl& url = QUrl::fromLocalFile(format.property(EpsRenderer::ImagePath).toString()); + qDebug() << QFile::exists(url.toLocalFile()); + mathEl.setAttribute(QStringLiteral("path"), url.fileName()); + foundNeededImage = true; + } + } + cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); + } + } + el.appendChild(mathEl); } return el; } QJsonValue MarkdownEntry::toJupyterJson() { QJsonObject entry; entry.insert(QLatin1String("cell_type"), QLatin1String("markdown")); entry.insert(QLatin1String("metadata"), QJsonObject()); QJsonObject attachments; QUrl url; QString key; for (const auto& data : attachedImages) { std::tie(url, key) = std::move(data); const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value(); QString attachmentKey = url.toString().remove(QLatin1String("attachment:")); attachments.insert(attachmentKey, JupyterUtils::packMimeBundle(image, key)); } if (!attachments.isEmpty()) entry.insert(QLatin1String("attachments"), attachments); JupyterUtils::setSource(entry, plain); return entry; } QString MarkdownEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq) { Q_UNUSED(commandSep); if (commentStartingSeq.isEmpty()) return QString(); QString text(plain); if (!commentEndingSeq.isEmpty()) return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n"); return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n"); } void MarkdownEntry::interruptEvaluation() { } bool MarkdownEntry::evaluate(EvaluationOption evalOp) { if(!rendered) { if (m_textItem->toPlainText() == plain && !html.isEmpty()) { setRenderedHtml(html); rendered = true; } else { plain = m_textItem->toPlainText(); rendered = renderMarkdown(plain); } } if (worksheet()->embeddedMathEnabled()) renderMath(); evaluateNext(evalOp); return true; } bool MarkdownEntry::renderMarkdown(QString& plain) { #ifdef Discount_FOUND QByteArray mdCharArray = plain.toUtf8(); MMIOT* mdHandle = mkd_string(mdCharArray.data(), mdCharArray.size()+1, 0); if(!mkd_compile(mdHandle, MKD_LATEX | MKD_FENCEDCODE | MKD_GITHUBTAGS)) { qDebug()<<"Failed to compile the markdown document"; mkd_cleanup(mdHandle); return false; } char *htmlDocument; int htmlSize = mkd_document(mdHandle, &htmlDocument); html = QString::fromUtf8(htmlDocument, htmlSize); char *latexData; int latexDataSize = mkd_latextext(mdHandle, &latexData); QStringList latexUnits = QString::fromUtf8(latexData, latexDataSize).split(QLatin1Char(31), QString::SkipEmptyParts); foundMath.clear(); for (const QString& latex : latexUnits) { foundMath.push_back(std::make_pair(latex, false)); html.replace(latex, latex.toHtmlEscaped()); } qDebug() << "founded math:" << foundMath; mkd_cleanup(mdHandle); setRenderedHtml(html); return true; #else Q_UNUSED(plain); return false; #endif } void MarkdownEntry::updateEntry() { QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter)); while(!cursor.isNull()) { QTextImageFormat format=cursor.charFormat().toImageFormat(); if (format.hasProperty(EpsRenderer::CantorFormula)) worksheet()->mathRenderer()->rerender(m_textItem->document(), format); cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor); } } WorksheetCursor MarkdownEntry::search(const QString& pattern, unsigned flags, QTextDocument::FindFlags qt_flags, const WorksheetCursor& pos) { if (!(flags & WorksheetEntry::SearchText) || (pos.isValid() && pos.entry() != this)) return WorksheetCursor(); QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos); if (textCursor.isNull()) return WorksheetCursor(); else return WorksheetCursor(this, m_textItem, textCursor); } void MarkdownEntry::layOutForWidth(qreal w, bool force) { if (size().width() == w && !force) return; m_textItem->setGeometry(0, 0, w); setSize(QSizeF(m_textItem->width(), m_textItem->height() + VerticalMargin)); } bool MarkdownEntry::eventFilter(QObject* object, QEvent* event) { if(object == m_textItem) { if(event->type() == QEvent::GraphicsSceneMouseDoubleClick) { QGraphicsSceneMouseEvent* mouseEvent = dynamic_cast(event); if(!mouseEvent) return false; if(mouseEvent->button() == Qt::LeftButton) { if (rendered) { setPlainText(plain); m_textItem->setCursorPosition(mouseEvent->pos()); m_textItem->textCursor().clearSelection(); rendered = false; return true; } } } } return false; } bool MarkdownEntry::wantToEvaluate() { return !rendered; } void MarkdownEntry::setRenderedHtml(const QString& html) { m_textItem->setHtml(html); m_textItem->denyEditing(); } void MarkdownEntry::setPlainText(const QString& plain) { QTextDocument* doc = m_textItem->document(); doc->setPlainText(plain); m_textItem->setDocument(doc); m_textItem->allowEditing(); } void MarkdownEntry::resolveImagesAtCursor() { QTextCursor cursor = m_textItem->textCursor(); if (!cursor.hasSelection()) cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); const QString& mathCode = m_textItem->resolveImages(cursor); for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++) if (iter->first == mathCode) { iter->second = false; break; } cursor.insertText(mathCode); } void MarkdownEntry::renderMath() { QTextCursor cursor(m_textItem->document()); cursor.movePosition(QTextCursor::Start); for (std::pair pair : foundMath) { // Jupyter TODO: what about \( \) and \[ \]? Supports or not? QString searchText = pair.first; searchText.replace(QRegExp(QLatin1String("\\s+")), QLatin1String(" ")); QTextCursor found = m_textItem->document()->find(searchText, cursor); if (found.isNull()) continue; cursor = found; QString latexCode = cursor.selectedText(); latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); latexCode.replace(QChar::LineSeparator, QLatin1Char('\n')); qDebug()<<"found latex: " << latexCode; renderMathExpression(latexCode); } } void MarkdownEntry::handleMathRender(QSharedPointer result) { if (!result->successfull) { qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage; return; } - const QString& code = result->renderedMath.property(EpsRenderer::Code).toString(); - const QString& delimiter = result->renderedMath.property(EpsRenderer::Delimiter).toString(); - QString searchText = delimiter + code + delimiter; - searchText.replace(QRegExp(QLatin1String("\\s")), QLatin1String(" ")); - - QTextCursor cursor = m_textItem->document()->find(searchText); - if (!cursor.isNull()) - { - m_textItem->document()->addResource(QTextDocument::ImageResource, result->uniqueUrl, QVariant(result->image)); - cursor.insertText(QString(QChar::ObjectReplacementCharacter), result->renderedMath); - - // Set that the formulas is rendered - for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++) - { - const QString& formulas = delimiter + code + delimiter; - if (iter->first == formulas) - { - iter->second = true; - break; - } - } - } + setRenderedMath(result->renderedMath, result->uniqueUrl, result->image); } void MarkdownEntry::renderMathExpression(QString mathCode) +{ + QString latex; + Cantor::LatexRenderer::EquationType type; + std::tie(latex, type) = parseMathCode(mathCode); + if (!latex.isNull()) + worksheet()->mathRenderer()->renderExpression(latex, type, this, SLOT(handleMathRender(QSharedPointer))); +} + +std::pair MarkdownEntry::parseMathCode(QString mathCode) { static const QLatin1String inlineDelimiter("$"); static const QLatin1String displayedDelimiter("$$"); - MathRenderer* renderer = worksheet()->mathRenderer(); - if (mathCode.startsWith(displayedDelimiter) && mathCode.endsWith(displayedDelimiter)) + if (mathCode.startsWith(displayedDelimiter) && mathCode.endsWith(displayedDelimiter)) { mathCode.remove(0, 2); mathCode.chop(2); - renderer->renderExpression(mathCode, Cantor::LatexRenderer::FullEquation, this, SLOT(handleMathRender(QSharedPointer))); + return std::make_pair(mathCode, Cantor::LatexRenderer::FullEquation); } else if (mathCode.startsWith(inlineDelimiter) && mathCode.endsWith(inlineDelimiter)) { mathCode.remove(0, 1); mathCode.chop(1); - renderer->renderExpression(mathCode, Cantor::LatexRenderer::InlineEquation, this, SLOT(handleMathRender(QSharedPointer))); + return std::make_pair(mathCode, Cantor::LatexRenderer::InlineEquation); + } + else + return std::make_pair(QString(), Cantor::LatexRenderer::InlineEquation); +} + +void MarkdownEntry::setRenderedMath(const QTextImageFormat& format, const QUrl& internal, const QImage& image) +{ + const QString& code = format.property(EpsRenderer::Code).toString(); + const QString& delimiter = format.property(EpsRenderer::Delimiter).toString(); + QString searchText = delimiter + code + delimiter; + searchText.replace(QRegExp(QLatin1String("\\s")), QLatin1String(" ")); + + QTextCursor cursor = m_textItem->document()->find(searchText); + if (!cursor.isNull()) + { + m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image)); + cursor.insertText(QString(QChar::ObjectReplacementCharacter), format); + + // Set that the formulas is rendered + for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++) + { + const QString& formulas = delimiter + code + delimiter; + if (iter->first == formulas) + { + iter->second = true; + break; + } + } } } diff --git a/src/markdownentry.h b/src/markdownentry.h index 80d0dfc4..010b104d 100644 --- a/src/markdownentry.h +++ b/src/markdownentry.h @@ -1,94 +1,98 @@ /* 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) 2018 Yifei Wu */ #ifndef MARKDOWNENTRY_H #define MARKDOWNENTRY_H #include #include #include "worksheetentry.h" #include "worksheettextitem.h" #include "mathrendertask.h" +#include "lib/latexrenderer.h" class QJsonObject; class MarkdownEntry : public WorksheetEntry { Q_OBJECT public: explicit MarkdownEntry(Worksheet* worksheet); ~MarkdownEntry() override = default; enum {Type = UserType + 7}; int type() const override; bool isEmpty() override; bool acceptRichText() override; bool focusEntry(int pos = WorksheetTextItem::TopLeft, qreal xCoord=0) override; void setContent(const QString& content) override; void setContent(const QDomElement& content, const KZip& file) override; void setContentFromJupyter(const QJsonObject& cell) override; QDomElement toXml(QDomDocument& doc, KZip* archive) override; QJsonValue toJupyterJson() override; QString toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq) override; void interruptEvaluation() override; void layOutForWidth(qreal w, bool force = false) override; WorksheetCursor search(const QString& pattern, unsigned flags, QTextDocument::FindFlags qt_flags, const WorksheetCursor& pos = WorksheetCursor()) override; public Q_SLOTS: bool evaluate(WorksheetEntry::EvaluationOption evalOp = FocusNext) override; void updateEntry() override; void resolveImagesAtCursor(); void populateMenu(QMenu* menu, QPointF pos) override; protected: bool renderMarkdown(QString& plain); bool eventFilter(QObject* object, QEvent* event) override; bool wantToEvaluate() override; void setRenderedHtml(const QString& html); void setPlainText(const QString& plain); void renderMath(); void renderMathExpression(QString mathCode); + void setRenderedMath(const QTextImageFormat& format, const QUrl& internal, const QImage& image); + + static std::pair parseMathCode(QString mathCode); protected Q_SLOTS: void handleMathRender(QSharedPointer result); protected: WorksheetTextItem* m_textItem; QString plain; QString html; bool rendered; std::vector> attachedImages; std::vector> foundMath; }; #endif //MARKDOWNENTRY_H diff --git a/src/mathrender.cpp b/src/mathrender.cpp index e468c9a7..c2409786 100644 --- a/src/mathrender.cpp +++ b/src/mathrender.cpp @@ -1,87 +1,105 @@ /* 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 */ #include "mathrender.h" #include #include #include #include #include "mathrendertask.h" #include "epsrenderer.h" MathRenderer::MathRenderer(): m_scale(1.0), m_useHighRes(false) { qRegisterMetaType>(); } 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, &popplerMutex); 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, &popplerMutex); if (success) { QUrl internal(math.name()); document->addResource(QTextDocument::ImageResource, internal, QVariant(img)); } else { qDebug() << "Rerender embedded math failed with message: " << errorMessage; } } + +std::pair MathRenderer::renderExpressionFromPdf(const QString& filename, const QString& uuid, const QString& code, Cantor::LatexRenderer::EquationType type, bool* outSuccess) +{ + if (!QFile::exists(filename)) + { + if (outSuccess) + *outSuccess = false; + return std::make_pair(QTextImageFormat(), QImage()); + } + + bool success; QString errorMessage; + const auto& data = MathRenderTask::renderPdfToFormat(filename, code, uuid, type, m_scale, m_useHighRes, &success, &errorMessage, &popplerMutex); + if (success == false) + qDebug() << "Render embedded math from pdf failed with message: " << errorMessage; + if (outSuccess) + *outSuccess = success; + return data; +} diff --git a/src/mathrender.h b/src/mathrender.h index 5baeb233..08518b2d 100644 --- a/src/mathrender.h +++ b/src/mathrender.h @@ -1,76 +1,83 @@ /* 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 */ #ifndef MATHRENDER_H #define MATHRENDER_H #include #include #include #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); + /** + * Render math expression from existing .pdf + * Like MathRenderer::rerender is blocking + */ + std::pair renderExpressionFromPdf( + const QString& filename, const QString& uuid, const QString& code, Cantor::LatexRenderer::EquationType type, bool* success + ); private: double m_scale; bool m_useHighRes; // We need this, because poppler-qt5 not threadsafe before 0.73.0 and 0.73.0 is too new // and not common widespread in repositories QMutex popplerMutex; }; #endif /* MATHRENDER_H */ diff --git a/src/mathrendertask.cpp b/src/mathrendertask.cpp index 7dbf345a..936eff91 100644 --- a/src/mathrendertask.cpp +++ b/src/mathrendertask.cpp @@ -1,269 +1,300 @@ /* 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 */ #include "mathrendertask.h" #include #include #include #include #include #include #include #include #include #include "epsrenderer.h" // Jupyter TODO: pagecolor don't work with preview // For example there are question about it: // https://tex.stackexchange.com/questions/499712/pagecolor-ignored-when-preview-package-used static const QLatin1String mathTex("\\documentclass{minimal}"\ "\\usepackage{amsfonts,amssymb}"\ "\\usepackage{amsmath}"\ "\\usepackage[utf8]{inputenc}"\ "\\usepackage{color}"\ "\\usepackage[active,textmath,tightpage]{%1}"\ /* "\\setlength\\textwidth{5in}"\ "\\setlength{\\parindent}{0pt}"\ "\\pagestyle{empty}"\ */ "\\begin{document}"\ "\\begin{preview}"\ "\\pagecolor[rgb]{%2,%3,%4}"\ "\\color[rgb]{%5,%6,%7}"\ "%8"\ "\\end{preview}"\ "\\end{document}"); static const QLatin1String eqnHeader("$\\displaystyle %1$"); static const QLatin1String inlineEqnHeader("$%1$"); MathRenderTask::MathRenderTask( const QString& code, Cantor::LatexRenderer::EquationType type, double scale, bool highResolution, QMutex* mutex ): m_code(code), m_type(type), m_scale(scale), m_highResolution(highResolution), m_mutex(mutex) {} void MathRenderTask::setHandler(const QObject* receiver, const char* resultHandler) { connect(this, SIGNAL(finish(QSharedPointer)), receiver, resultHandler); } void MathRenderTask::run() { QSharedPointer 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(); // Search preview.sty QString file = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("latex/preview.sty")); if (file.isEmpty()) file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("cantor/latex/preview.sty")); if (file.isEmpty()) { result->successfull = false; result->errorMessage = QString::fromLatin1("needed for math render preview.sty file not found"); finalize(result); return; } QString expressionTex=mathTex; file.chop(4); //remove '.sty' extention expressionTex=expressionTex.arg(file); 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& uuid = genUuid(); 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); texFile.setAutoRemove(false); //Usefull for debug 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, m_mutex); result->successfull = success; result->errorMessage = errorMessage; if (success == false) { finalize(result); return; } - QTextImageFormat format; + const auto& data = renderPdfToFormat(pdfFileName, m_code, uuid, m_type, m_scale, m_highResolution, &success, &errorMessage, m_mutex); + result->successfull = success; + result->errorMessage = errorMessage; + if (success == false) + { + finalize(result); + return; + } + + result->renderedMath = data.first; + result->image = data.second; 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); - format.setVerticalAlignment(QTextCharFormat::AlignBaseline); - - 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 result) { emit finish(result); deleteLater(); } QImage MathRenderTask::renderPdf(const QString& filename, double scale, bool highResolution, bool* success, QSizeF* size, QString* errorReason, QMutex* mutex) { if (mutex) mutex->lock(); Poppler::Document* document = Poppler::Document::load(filename); if (mutex) mutex->unlock(); 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(); } 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); delete document; return QImage(); } QSize pageSize = pdfPage->pageSize(); double realSclae; qreal w, h; if(highResolution) { realSclae = 1.2 * 5 * 1.8; w = 1.2 * pageSize.width(); h = 1.2 * pageSize.height(); } else { realSclae = 2.4 * scale * 1.8; w = 2.4 * pageSize.width(); h = 2.4 * pageSize.height(); } QImage image = pdfPage->renderToImage(72.0*realSclae, 72.0*realSclae); delete pdfPage; if (mutex) mutex->lock(); delete document; if (mutex) mutex->unlock(); 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; } + +std::pair MathRenderTask::renderPdfToFormat(const QString& filename, const QString& code, const QString uuid, Cantor::LatexRenderer::EquationType type, double scale, bool highResulution, bool* success, QString* errorReason, QMutex* mutex) +{ + QSizeF size; + const QImage& image = renderPdf(filename, scale, highResulution, success, &size, errorReason, mutex); + + if (success && *success == false) + return std::make_pair(QTextImageFormat(), QImage()); + + 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, type); + format.setProperty(EpsRenderer::ImagePath, filename); + format.setProperty(EpsRenderer::Code, code); + format.setVerticalAlignment(QTextCharFormat::AlignBaseline); + + switch(type) + { + case Cantor::LatexRenderer::FullEquation: + format.setProperty(EpsRenderer::Delimiter, QLatin1String("$$")); + break; + + case Cantor::LatexRenderer::InlineEquation: + format.setProperty(EpsRenderer::Delimiter, QLatin1String("$")); + break; + } + + return std::make_pair(std::move(format), std::move(image)); +} + +QString MathRenderTask::genUuid() +{ + QString uuid = QUuid::createUuid().toString(); + uuid.remove(0, 1); + uuid.chop(1); + uuid.replace(QLatin1Char('-'), QLatin1Char('_')); + return uuid; +} diff --git a/src/mathrendertask.h b/src/mathrendertask.h index 89927a7f..bb120d69 100644 --- a/src/mathrendertask.h +++ b/src/mathrendertask.h @@ -1,78 +1,93 @@ /* 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 */ #ifndef MATHRENDERTASK_H #define MATHRENDERTASK_H #include #include #include #include #include #include #include #include "lib/latexrenderer.h" class QMutex; struct MathRenderResult { bool successfull; QString errorMessage; QTextImageFormat renderedMath; QUrl uniqueUrl; QImage image; }; Q_DECLARE_METATYPE(MathRenderResult) Q_DECLARE_METATYPE(QSharedPointer) class MathRenderTask : public QObject, public QRunnable { Q_OBJECT public: MathRenderTask( const QString& code, Cantor::LatexRenderer::EquationType type, double scale, bool highResolution, QMutex* mutex ); void setHandler(const QObject *receiver, const char *resultHandler); void run() override; - static QImage renderPdf(const QString& filename, double scale, bool highResulution, bool* success = nullptr, QSizeF* size = nullptr, QString* errorReason = nullptr, QMutex* mutex = nullptr); + static QImage renderPdf( + const QString& filename, double scale, bool highResulution, bool* success = nullptr, QSizeF* size = nullptr, QString* errorReason = nullptr, QMutex* mutex = nullptr + ); + static std::pair renderPdfToFormat( + const QString& filename, + const QString& code, + const QString uuid, + Cantor::LatexRenderer::EquationType type, + double scale, + bool highResulution, + bool* success = nullptr, + QString* errorReason = nullptr, + QMutex* mutex = nullptr + ); + + static QString genUuid(); Q_SIGNALS: void finish(QSharedPointer result); private: void finalize(QSharedPointer result); private: QString m_code; Cantor::LatexRenderer::EquationType m_type; double m_scale; bool m_highResolution; QMutex* m_mutex; }; #endif /* MATHRENDERTASK_H */