File indexing completed on 2024-04-28 11:20:57

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com>
0004     SPDX-FileCopyrightText: 2012 Martin Kuettler <martin.kuettler@gmail.com>
0005 */
0006 
0007 #include "latexentry.h"
0008 
0009 #include "worksheet.h"
0010 #include "lib/renderer.h"
0011 #include "lib/jupyterutils.h"
0012 #include "lib/defaulthighlighter.h"
0013 #include "lib/latexrenderer.h"
0014 #include "config-cantor.h"
0015 
0016 #include <QTextCursor>
0017 #include <QStandardPaths>
0018 #include <QDir>
0019 #include <QDebug>
0020 #include <QBuffer>
0021 #include <QUuid>
0022 #include <QJsonValue>
0023 #include <QJsonObject>
0024 #include <QJsonArray>
0025 
0026 #include <KZip>
0027 #include <KLocalizedString>
0028 
0029 LatexEntry::LatexEntry(Worksheet* worksheet) : WorksheetEntry(worksheet), m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction))
0030 {
0031     m_textItem->installEventFilter(this);
0032 
0033     connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &LatexEntry::moveToPreviousEntry);
0034     connect(m_textItem, &WorksheetTextItem::moveToNext, this, &LatexEntry::moveToNextEntry);
0035     connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
0036 }
0037 
0038 void LatexEntry::populateMenu(QMenu* menu, QPointF pos)
0039 {
0040     bool imageSelected = false;
0041     QTextCursor cursor = m_textItem->textCursor();
0042     const QChar repl = QChar::ObjectReplacementCharacter;
0043     if (cursor.hasSelection()) {
0044         QString selection = m_textItem->textCursor().selectedText();
0045         imageSelected = selection.contains(repl);
0046     } else {
0047         // we need to try both the current cursor and the one after the that
0048         cursor = m_textItem->cursorForPosition(pos);
0049         for (int i = 2; i; --i) {
0050             int p = cursor.position();
0051             if (m_textItem->document()->characterAt(p-1) == repl &&
0052                 cursor.charFormat().hasProperty(Cantor::Renderer::CantorFormula)) {
0053                 m_textItem->setTextCursor(cursor);
0054                 imageSelected = true;
0055                 break;
0056             }
0057             cursor.movePosition(QTextCursor::NextCharacter);
0058         }
0059     }
0060     if (imageSelected) {
0061         menu->addAction(i18n("Show LaTeX code"), this, SLOT(resolveImagesAtCursor()));
0062         menu->addSeparator();
0063     }
0064     WorksheetEntry::populateMenu(menu, pos);
0065 }
0066 
0067 int LatexEntry::type() const
0068 {
0069     return Type;
0070 }
0071 
0072 bool LatexEntry::isEmpty()
0073 {
0074     return m_textItem->document()->isEmpty();
0075 }
0076 
0077 bool LatexEntry::acceptRichText()
0078 {
0079     return false;
0080 }
0081 
0082 bool LatexEntry::focusEntry(int pos, qreal xCoord)
0083 {
0084     if (aboutToBeRemoved())
0085         return false;
0086     m_textItem->setFocusAt(pos, xCoord);
0087     return true;
0088 }
0089 
0090 void LatexEntry::setContent(const QString& content)
0091 {
0092     m_latex = content;
0093     m_textItem->setPlainText(m_latex);
0094 }
0095 
0096 void LatexEntry::setContent(const QDomElement& content, const KZip& file)
0097 {
0098     m_latex = content.text();
0099     qDebug() << m_latex;
0100 
0101     m_textItem->document()->clear();
0102     QTextCursor cursor = m_textItem->textCursor();
0103     cursor.movePosition(QTextCursor::Start);
0104 
0105     QString imagePath;
0106     bool useLatexCode = true;
0107 
0108     if(content.hasAttribute(QLatin1String("filename")))
0109     {
0110         const KArchiveEntry* imageEntry=file.directory()->entry(content.attribute(QLatin1String("filename")));
0111         if (imageEntry&&imageEntry->isFile())
0112         {
0113             const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(imageEntry);
0114             const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
0115             imageFile->copyTo(dir);
0116             imagePath = dir + QDir::separator() + imageFile->name();
0117 
0118 #ifdef LIBSPECTRE_FOUND
0119             QString uuid = Cantor::LatexRenderer::genUuid();
0120             m_renderedFormat = worksheet()->renderer()->render(m_textItem->document(), Cantor::Renderer::EPS, QUrl::fromLocalFile(imagePath), uuid);
0121             qDebug()<<"rendering successful? " << !m_renderedFormat.name().isEmpty();
0122 
0123             m_renderedFormat.setProperty(Cantor::Renderer::CantorFormula, Cantor::Renderer::LatexFormula);
0124             m_renderedFormat.setProperty(Cantor::Renderer::ImagePath, imagePath);
0125             m_renderedFormat.setProperty(Cantor::Renderer::Code, m_latex);
0126 
0127             cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
0128             useLatexCode = false;
0129             m_textItem->denyEditing();
0130 #endif
0131         }
0132     }
0133 
0134     if (useLatexCode && content.hasAttribute(QLatin1String("image")))
0135     {
0136         const QByteArray& ba = QByteArray::fromBase64(content.attribute(QLatin1String("image")).toLatin1());
0137         QImage image;
0138         if (image.loadFromData(ba))
0139         {
0140             // Create unique internal url for this loaded image
0141             QUrl internal;
0142             internal.setScheme(QLatin1String("internal"));
0143             internal.setPath(QUuid::createUuid().toString());
0144 
0145             m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
0146 
0147             m_renderedFormat.setName(internal.url());
0148             m_renderedFormat.setWidth(image.width());
0149             m_renderedFormat.setHeight(image.height());
0150 
0151             m_renderedFormat.setProperty(Cantor::Renderer::CantorFormula, Cantor::Renderer::LatexFormula);
0152             if (!imagePath.isEmpty())
0153                 m_renderedFormat.setProperty(Cantor::Renderer::ImagePath, imagePath);
0154             m_renderedFormat.setProperty(Cantor::Renderer::Code, m_latex);
0155 
0156             cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
0157             useLatexCode = false;
0158             m_textItem->denyEditing();
0159         }
0160     }
0161 
0162     if (useLatexCode)
0163         cursor.insertText(m_latex);
0164 }
0165 
0166 void LatexEntry::setContentFromJupyter(const QJsonObject& cell)
0167 {
0168     if (!Cantor::JupyterUtils::isCodeCell(cell))
0169         return;
0170 
0171     m_textItem->document()->clear();
0172     QTextCursor cursor = m_textItem->textCursor();
0173     cursor.movePosition(QTextCursor::Start);
0174 
0175     bool useLatexCode = true;
0176 
0177     QString source = Cantor::JupyterUtils::getSource(cell);
0178     m_latex = source.remove(QLatin1String("%%latex\n"));
0179 
0180     QJsonArray outputs = cell.value(Cantor::JupyterUtils::outputsKey).toArray();
0181     if (outputs.size() == 1 && Cantor::JupyterUtils::isJupyterDisplayOutput(outputs[0]))
0182     {
0183         const QJsonObject data = outputs[0].toObject().value(Cantor::JupyterUtils::dataKey).toObject();
0184         const QImage& image = Cantor::JupyterUtils::loadImage(data, Cantor::JupyterUtils::pngMime);
0185         if (!image.isNull())
0186         {
0187             QUrl internal;
0188             internal.setScheme(QLatin1String("internal"));
0189             internal.setPath(QUuid::createUuid().toString());
0190 
0191             m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
0192 
0193             m_renderedFormat.setName(internal.url());
0194             m_renderedFormat.setWidth(image.width());
0195             m_renderedFormat.setHeight(image.height());
0196 
0197             m_renderedFormat.setProperty(Cantor::Renderer::CantorFormula, Cantor::Renderer::LatexFormula);
0198             m_renderedFormat.setProperty(Cantor::Renderer::Code, m_latex);
0199 
0200             cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
0201             useLatexCode = false;
0202             m_textItem->denyEditing();
0203         }
0204     }
0205 
0206     if (useLatexCode)
0207     {
0208         cursor.insertText(m_latex);
0209         m_latex.clear(); // We don't render image, so clear latex code cache
0210     }
0211 }
0212 
0213 QJsonValue LatexEntry::toJupyterJson()
0214 {
0215     QJsonObject entry;
0216     entry.insert(Cantor::JupyterUtils::cellTypeKey, QLatin1String("code"));
0217     entry.insert(Cantor::JupyterUtils::executionCountKey, QJsonValue());
0218 
0219     QJsonObject metadata, cantorMetadata;
0220     cantorMetadata.insert(QLatin1String("latex_entry"), true);
0221     metadata.insert(Cantor::JupyterUtils::cantorMetadataKey, cantorMetadata);
0222     entry.insert(Cantor::JupyterUtils::metadataKey, metadata);
0223 
0224     QJsonArray outputs;
0225 
0226     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
0227     if (!cursor.isNull())
0228     {
0229         QTextImageFormat format=cursor.charFormat().toImageFormat();
0230 
0231         QUrl internal;
0232         internal.setUrl(format.name());
0233         const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, internal).value<QImage>();
0234         if (!image.isNull())
0235         {
0236             QByteArray ba;
0237             QBuffer buffer(&ba);
0238             buffer.open(QIODevice::WriteOnly);
0239             image.save(&buffer, "PNG");
0240 
0241             // Add image result with latex rendered image to this Jupyter code cell
0242             QJsonObject imageResult;
0243             imageResult.insert(Cantor::JupyterUtils::outputTypeKey, QLatin1String("display_data"));
0244 
0245             QJsonObject data;
0246             data.insert(Cantor::JupyterUtils::pngMime, Cantor::JupyterUtils::toJupyterMultiline(QString::fromLatin1(ba.toBase64())));
0247             imageResult.insert(QLatin1String("data"), data);
0248 
0249             imageResult.insert(Cantor::JupyterUtils::metadataKey, QJsonObject());
0250 
0251             outputs.append(imageResult);
0252         }
0253     }
0254     entry.insert(Cantor::JupyterUtils::outputsKey, outputs);
0255 
0256     const QString& latex = latexCode();
0257     Cantor::JupyterUtils::setSource(entry, QLatin1String("%%latex\n") + latex);
0258 
0259     return entry;
0260 }
0261 
0262 QDomElement LatexEntry::toXml(QDomDocument& doc, KZip* archive)
0263 {
0264     QDomElement el = doc.createElement(QLatin1String("Latex"));
0265     el.appendChild( doc.createTextNode( latexCode() ));
0266 
0267     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
0268     if (!cursor.isNull())
0269     {
0270         QTextImageFormat format=cursor.charFormat().toImageFormat();
0271         QString fileName = format.property(Cantor::Renderer::ImagePath).toString();
0272         // Check, if eps file exists, and if not true, rerender latex code
0273         bool isEpsFileExists = QFile::exists(fileName);
0274 
0275 #ifdef LIBSPECTRE_FOUND
0276         if (!isEpsFileExists && renderLatexCode())
0277         {
0278             cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
0279             format=cursor.charFormat().toImageFormat();
0280             fileName = format.property(Cantor::Renderer::ImagePath).toString();
0281             isEpsFileExists = QFile::exists(fileName);
0282         }
0283 #endif
0284 
0285         if (isEpsFileExists && archive)
0286         {
0287             const QUrl& url=QUrl::fromLocalFile(fileName);
0288             archive->addLocalFile(url.toLocalFile(), url.fileName());
0289             el.setAttribute(QLatin1String("filename"), url.fileName());
0290         }
0291 
0292         // Save also rendered QImage, if exist.
0293         QUrl internal;
0294         internal.setUrl(format.name());
0295 
0296         const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, internal).value<QImage>();
0297         if (!image.isNull())
0298         {
0299             QByteArray ba;
0300             QBuffer buffer(&ba);
0301             buffer.open(QIODevice::WriteOnly);
0302             image.save(&buffer, "PNG");
0303             el.setAttribute(QLatin1String("image"), QString::fromLatin1(ba.toBase64()));
0304         }
0305     }
0306 
0307     return el;
0308 }
0309 
0310 QString LatexEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
0311 {
0312     Q_UNUSED(commandSep);
0313 
0314     if (commentStartingSeq.isEmpty())
0315         return QString();
0316 
0317     QString text = latexCode();
0318     if (!commentEndingSeq.isEmpty())
0319         return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n");
0320     return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n");
0321 }
0322 
0323 bool LatexEntry::evaluate(EvaluationOption evalOp)
0324 {
0325     bool success = false;
0326 
0327     if (isOneImageOnly())
0328     {
0329         success = true;
0330     }
0331     else
0332     {
0333         if (m_latex == latexCode())
0334         {
0335             bool renderWasSuccessful = !m_renderedFormat.name().isEmpty();
0336             if (renderWasSuccessful)
0337             {
0338                 QTextCursor cursor = m_textItem->textCursor();
0339                 cursor.movePosition(QTextCursor::Start);
0340                 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0341                 cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
0342                 m_textItem->denyEditing();
0343             }
0344             else
0345             {
0346                 success = renderLatexCode();
0347             }
0348         }
0349         else
0350         {
0351             m_latex = latexCode();
0352             success = renderLatexCode();
0353         }
0354     }
0355 
0356     qDebug()<<"rendering successful? "<<success;
0357 
0358     evaluateNext(evalOp);
0359     return success;
0360 }
0361 
0362 void LatexEntry::updateEntry()
0363 {
0364     QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
0365     while (!cursor.isNull())
0366     {
0367         qDebug()<<"found a formula... rendering the eps...";
0368         QTextImageFormat format=cursor.charFormat().toImageFormat();
0369         const QUrl& url=QUrl::fromLocalFile(format.property(Cantor::Renderer::ImagePath).toString());
0370         QSizeF s = worksheet()->renderer()->renderToResource(m_textItem->document(), Cantor::Renderer::EPS, url, QUrl(format.name()));
0371         qDebug()<<"rendering successful? "<< s.isValid();
0372 
0373         cursor.movePosition(QTextCursor::NextCharacter);
0374 
0375         cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
0376     }
0377 }
0378 
0379 bool LatexEntry::eventFilter(QObject* object, QEvent* event)
0380 {
0381     if(object == m_textItem)
0382     {
0383         if (event->type() == QEvent::GraphicsSceneMouseDoubleClick)
0384         {
0385             // One image if we have rendered entry
0386             if (isOneImageOnly())
0387             {
0388                 QTextCursor cursor = m_textItem->textCursor();
0389                 if (!cursor.hasSelection())
0390                     cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
0391 
0392                 cursor.insertText(m_textItem->resolveImages(cursor));
0393                 m_textItem->allowEditing();
0394                 return true;
0395             }
0396         }
0397         else if (event->type() == QEvent::KeyPress)
0398         {
0399             auto* key_event = static_cast<QKeyEvent*>(event);
0400             if (key_event->matches(QKeySequence::Cancel))
0401             {
0402                 QTextCursor cursor = m_textItem->textCursor();
0403                 cursor.movePosition(QTextCursor::Start);
0404                 cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0405                 cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
0406                 m_textItem->denyEditing();
0407                 return true;
0408             }
0409         }
0410     }
0411     return false;
0412 }
0413 
0414 QString LatexEntry::latexCode()
0415 {
0416     QTextCursor cursor = m_textItem->textCursor();
0417     cursor.movePosition(QTextCursor::Start);
0418     cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0419 
0420     QString code = m_textItem->resolveImages(cursor);
0421     code.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); //Replace the U+2029 paragraph break by a Normal Newline
0422     code.replace(QChar::LineSeparator, QLatin1Char('\n')); //Replace the line break by a Normal Newline
0423     return code;
0424 }
0425 
0426 bool LatexEntry::isOneImageOnly()
0427 {
0428     QTextCursor cursor = m_textItem->textCursor();
0429     cursor.movePosition(QTextCursor::Start);
0430     cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0431 
0432     return (cursor.selectionEnd() == 1 && cursor.selectedText() == QString(QChar::ObjectReplacementCharacter));
0433 }
0434 
0435 int LatexEntry::searchText(const QString& text, const QString& pattern,
0436                           QTextDocument::FindFlags qt_flags)
0437 {
0438     Qt::CaseSensitivity caseSensitivity;
0439     if (qt_flags & QTextDocument::FindCaseSensitively)
0440         caseSensitivity = Qt::CaseSensitive;
0441     else
0442         caseSensitivity = Qt::CaseInsensitive;
0443 
0444     int position;
0445     if (qt_flags & QTextDocument::FindBackward)
0446         position = text.lastIndexOf(pattern, -1, caseSensitivity);
0447     else
0448         position = text.indexOf(pattern, 0, caseSensitivity);
0449 
0450     return position;
0451 }
0452 
0453 WorksheetCursor LatexEntry::search(const QString& pattern, unsigned flags,
0454                                    QTextDocument::FindFlags qt_flags,
0455                                    const WorksheetCursor& pos)
0456 {
0457     if (!(flags & WorksheetEntry::SearchLaTeX))
0458         return WorksheetCursor();
0459     if (pos.isValid() && (pos.entry() != this || pos.textItem() != m_textItem))
0460         return WorksheetCursor();
0461 
0462     QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos);
0463     int position = 0;
0464     QString latex;
0465     const QString repl = QString(QChar::ObjectReplacementCharacter);
0466     QTextCursor latexCursor = m_textItem->search(repl, qt_flags, pos);
0467 
0468     while (!latexCursor.isNull()) {
0469         latex = m_textItem->resolveImages(latexCursor);
0470         position = searchText(latex, pattern, qt_flags);
0471         if (position >= 0) {
0472             break;
0473         }
0474         WorksheetCursor c(this, m_textItem, latexCursor);
0475         latexCursor = m_textItem->search(repl, qt_flags, c);
0476     }
0477 
0478     if (latexCursor.isNull()) {
0479         if (textCursor.isNull())
0480             return WorksheetCursor();
0481         else
0482             return WorksheetCursor(this, m_textItem, textCursor);
0483     } else {
0484         if (textCursor.isNull() || latexCursor < textCursor) {
0485             int start = latexCursor.selectionStart();
0486             latexCursor.insertText(latex);
0487             QTextCursor c = m_textItem->textCursor();
0488             c.setPosition(start + position);
0489             c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
0490                            pattern.length());
0491             return WorksheetCursor(this, m_textItem, c);
0492         } else {
0493             return WorksheetCursor(this, m_textItem, textCursor);
0494         }
0495     }
0496 }
0497 
0498 void LatexEntry::layOutForWidth(qreal entry_zone_x, qreal w, bool force)
0499 {
0500     if (size().width() == w && m_textItem->pos().x() == entry_zone_x && !force)
0501         return;
0502 
0503     const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin;
0504 
0505     m_textItem->setGeometry(entry_zone_x, 0, w - margin - entry_zone_x);
0506     setSize(QSizeF(m_textItem->width() + margin + entry_zone_x, m_textItem->height() + VerticalMargin));
0507 }
0508 
0509 bool LatexEntry::wantToEvaluate()
0510 {
0511     return !isOneImageOnly();
0512 }
0513 
0514 bool LatexEntry::renderLatexCode()
0515 {
0516     bool success = false;
0517     QString latex = latexCode();
0518     m_renderedFormat = QTextImageFormat(); // clear rendered image
0519     Cantor::LatexRenderer* renderer = new Cantor::LatexRenderer(this);
0520     renderer->setLatexCode(latex);
0521     renderer->setEquationOnly(false);
0522     renderer->setMethod(Cantor::LatexRenderer::LatexMethod);
0523     renderer->renderBlocking();
0524 
0525     if (renderer->renderingSuccessful())
0526     {
0527         Cantor::Renderer* epsRend = worksheet()->renderer();
0528         m_renderedFormat = epsRend->render(m_textItem->document(), renderer);
0529         success = !m_renderedFormat.name().isEmpty();
0530     }
0531     else
0532         qWarning() << "Fail to render LatexEntry with error " << renderer->errorMessage();
0533 
0534     if(success)
0535     {
0536         QTextCursor cursor = m_textItem->textCursor();
0537         cursor.movePosition(QTextCursor::Start);
0538         cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0539         cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
0540         m_textItem->denyEditing();
0541     }
0542 
0543     delete renderer;
0544     return success;
0545 }
0546 
0547 bool LatexEntry::isConvertableToLatexEntry(const QJsonObject& cell)
0548 {
0549     if (!Cantor::JupyterUtils::isCodeCell(cell))
0550         return false;
0551 
0552     const QString& source = Cantor::JupyterUtils::getSource(cell);
0553 
0554     return source.startsWith(QLatin1String("%%latex\n"));
0555 }
0556 
0557 void LatexEntry::resolveImagesAtCursor()
0558 {
0559     QTextCursor cursor = m_textItem->textCursor();
0560     if (!cursor.hasSelection())
0561         cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
0562     cursor.insertText(m_textItem->resolveImages(cursor));
0563 }
0564 
0565 QString LatexEntry::plain() const
0566 {
0567     return m_textItem->toPlainText();
0568 }