1 /*
2     SPDX-License-Identifier: GPL-2.0-or-later
3     SPDX-FileCopyrightText: 2009 Alexander Rieder <alexanderrieder@gmail.com>
4 */
5 
6 #include "loadedexpression.h"
7 
8 #include "lib/jupyterutils.h"
9 #include "lib/imageresult.h"
10 #include "lib/epsresult.h"
11 #include "lib/textresult.h"
12 #include "lib/latexresult.h"
13 #include "lib/animationresult.h"
14 #include "lib/latexrenderer.h"
15 #include "lib/mimeresult.h"
16 #include "lib/htmlresult.h"
17 
18 #include <QDir>
19 #include <QStandardPaths>
20 #include <QJsonArray>
21 #include <QJsonObject>
22 #include <QJsonDocument>
23 #include <QDebug>
24 #include <QPixmap>
25 #include <QRegularExpression>
26 #include <QTemporaryFile>
27 
LoadedExpression(Cantor::Session * session)28 LoadedExpression::LoadedExpression( Cantor::Session* session ) : Cantor::Expression( session, false, -1)
29 {
30 
31 }
32 
interrupt()33 void LoadedExpression::interrupt()
34 {
35     //Do nothing
36 }
37 
evaluate()38 void LoadedExpression::evaluate()
39 {
40     //Do nothing
41 }
42 
loadFromXml(const QDomElement & xml,const KZip & file)43 void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file)
44 {
45     setCommand(xml.firstChildElement(QLatin1String("Command")).text());
46 
47     const QDomNodeList& results = xml.elementsByTagName(QLatin1String("Result"));
48     for (int i = 0; i < results.size(); i++)
49     {
50         const QDomElement& resultElement = results.at(i).toElement();
51         const QString& type = resultElement.attribute(QLatin1String("type"));
52         qDebug() << "type" << type;
53         if ( type == QLatin1String("text"))
54         {
55             const QString& format = resultElement.attribute(QLatin1String("format"));
56             bool isStderr = resultElement.attribute(QLatin1String("stderr")).toInt();
57             Cantor::TextResult* result = new Cantor::TextResult(resultElement.text());
58             if (format == QLatin1String("latex"))
59                 result->setFormat(Cantor::TextResult::LatexFormat);
60             result->setStdErr(isStderr);
61             addResult(result);
62         }
63         else if (type == QLatin1String("mime"))
64         {
65             const QDomElement& resultElement = results.at(i).toElement();
66 
67             QJsonObject mimeBundle;
68             const QDomNodeList& contents = resultElement.elementsByTagName(QLatin1String("Content"));
69             for (int x = 0; x < contents.count(); x++)
70             {
71                 const QDomElement& content = contents.at(x).toElement();
72 
73                 const QString& mimeType = content.attribute(QLatin1String("key"));
74                 QJsonDocument jsonDoc = QJsonDocument::fromJson(content.text().toUtf8());;
75                 const QJsonValue& value = jsonDoc.object().value(QLatin1String("content"));
76                 mimeBundle.insert(mimeType, value);
77             }
78 
79             addResult(new Cantor::MimeResult(mimeBundle));
80         }
81         else if (type == QLatin1String("html"))
82         {
83             const QString& formatString = resultElement.attribute(QLatin1String("showCode"));
84             Cantor::HtmlResult::Format format = Cantor::HtmlResult::Html;
85             if (formatString == QLatin1String("htmlSource"))
86                 format = Cantor::HtmlResult::HtmlSource;
87             else if (formatString == QLatin1String("plain"))
88                 format = Cantor::HtmlResult::PlainAlternative;
89 
90             const QString& plain = resultElement.firstChildElement(QLatin1String("Plain")).text();
91             const QString& html = resultElement.firstChildElement(QLatin1String("Html")).text();
92 
93             std::map<QString, QJsonValue> alternatives;
94             const QDomNodeList& alternativeElms = resultElement.elementsByTagName(QLatin1String("Alternative"));
95             for (int x = 0; x < alternativeElms.count(); x++)
96             {
97                 const QDomElement& content = alternativeElms.at(x).toElement();
98 
99                 const QString& mimeType = content.attribute(QLatin1String("key"));
100                 QJsonDocument jsonDoc = QJsonDocument::fromJson(content.text().toUtf8());;
101                 const QJsonValue& value = jsonDoc.object().value(QLatin1String("root"));
102                 alternatives[mimeType] = value;
103             }
104 
105             Cantor::HtmlResult* result = new Cantor::HtmlResult(html, plain, alternatives);
106             result->setFormat(format);
107 
108             addResult(result);
109         }
110         else if (type == QLatin1String("image") || type == QLatin1String("latex") || type == QLatin1String("animation") || type == QLatin1String("epsimage"))
111         {
112             const KArchiveEntry* imageEntry=file.directory()->entry(resultElement.attribute(QLatin1String("filename")));
113             if (imageEntry&&imageEntry->isFile())
114             {
115                 const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(imageEntry);
116                 QString dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
117                 imageFile->copyTo(dir);
118                 QUrl imageUrl = QUrl::fromLocalFile(QDir(dir).absoluteFilePath(imageFile->name()));
119                 if(type==QLatin1String("latex"))
120                 {
121                     const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
122                     QImage image;
123                     image.loadFromData(ba);
124                     addResult(new Cantor::LatexResult(resultElement.text(), imageUrl, QString(), image));
125                 }
126                 else if(type==QLatin1String("animation"))
127                 {
128                     addResult(new Cantor::AnimationResult(imageUrl));
129                 }
130                 else if(type==QLatin1String("epsimage"))
131                 {
132                     const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
133                     QImage image;
134                     image.loadFromData(ba);
135                     addResult(new Cantor::EpsResult(imageUrl, image));
136                 }
137                 else if(imageFile->name().endsWith(QLatin1String(".eps")))
138                 {
139                     const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
140                     QImage image;
141                     image.loadFromData(ba);
142                     addResult(new Cantor::EpsResult(imageUrl, image));
143                 }
144                 else
145                 {
146                     addResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
147                 }
148             }
149         }
150     }
151 
152     const QDomElement& errElem = xml.firstChildElement(QLatin1String("Error"));
153     if (!errElem.isNull())
154     {
155         setErrorMessage(errElem.text());
156         setStatus(Error);
157     }
158     else
159         setStatus(Done);
160 }
161 
loadFromJupyter(const QJsonObject & cell)162 void LoadedExpression::loadFromJupyter(const QJsonObject& cell)
163 {
164     setCommand(Cantor::JupyterUtils::getSource(cell));
165 
166     const QJsonValue idObject = cell.value(QLatin1String("execution_count"));
167     if (!idObject.isUndefined() && !idObject.isNull())
168         setId(idObject.toInt());
169 
170     const QJsonArray& outputs = cell.value(QLatin1String("outputs")).toArray();
171     for (QJsonArray::const_iterator iter = outputs.begin(); iter != outputs.end(); ++iter)
172     {
173         if (!Cantor::JupyterUtils::isJupyterOutput(*iter))
174             continue;
175 
176         const QJsonObject& output = iter->toObject();
177         const QString& outputType = Cantor::JupyterUtils::getOutputType(output);
178         if (Cantor::JupyterUtils::isJupyterTextOutput(output))
179         {
180             const QString& text = Cantor::JupyterUtils::fromJupyterMultiline(output.value(QLatin1String("text")));
181             bool isStderr = output.value(QLatin1String("name")).toString() == QLatin1String("stderr");
182             Cantor::TextResult* result = new Cantor::TextResult(text);
183             result->setStdErr(isStderr);
184             addResult(result);
185         }
186         else if (Cantor::JupyterUtils::isJupyterErrorOutput(output))
187         {
188             const QJsonArray& tracebackLineArray = output.value(QLatin1String("traceback")).toArray();
189             QString traceback;
190 
191             // Looks like the traceback in Jupyter joined with '\n', no ''
192             // So, manually add it
193             for (const QJsonValue& line : tracebackLineArray)
194                 traceback += line.toString() + QLatin1Char('\n');
195             traceback.chop(1);
196 
197             // IPython returns error with terminal colors, we handle it here, but should we?
198             static const QChar ESC(0x1b);
199             traceback.remove(QRegularExpression(QString(ESC)+QLatin1String("\\[[0-9;]*m")));
200 
201             setErrorMessage(traceback);
202         }
203         else if (Cantor::JupyterUtils::isJupyterDisplayOutput(output) || Cantor::JupyterUtils::isJupyterExecutionResult(output))
204         {
205             const QJsonObject& data = output.value(QLatin1String("data")).toObject();
206 
207             QJsonObject metadata = Cantor::JupyterUtils::getMetadata(output);
208             const QString& text = Cantor::JupyterUtils::fromJupyterMultiline(data.value(Cantor::JupyterUtils::textMime));
209             const QString& mainKey = Cantor::JupyterUtils::mainBundleKey(data);
210 
211             Cantor::Result* result = nullptr;
212             if (mainKey == Cantor::JupyterUtils::gifMime)
213             {
214                 const QByteArray& bytes = QByteArray::fromBase64(data.value(mainKey).toString().toLatin1());
215 
216                 QTemporaryFile file;
217                 file.setAutoRemove(false);
218                 file.open();
219                 file.write(bytes);
220                 file.close();
221 
222                 result = new Cantor::AnimationResult(QUrl::fromLocalFile(file.fileName()), text);
223             }
224             else if (mainKey == Cantor::JupyterUtils::textMime)
225             {
226                 result = new Cantor::TextResult(text);
227             }
228             else if (mainKey == Cantor::JupyterUtils::htmlMime)
229             {
230                 const QString& html = Cantor::JupyterUtils::fromJupyterMultiline(data.value(Cantor::JupyterUtils::htmlMime));
231                 // Some backends places gif animation in hmlt (img tag), for example, Sage
232                 if (Cantor::JupyterUtils::isGifHtml(html))
233                 {
234                     result = new Cantor::AnimationResult(Cantor::JupyterUtils::loadGifHtml(html), text);
235                 }
236                 else
237                 {
238                     // Load alternative content types too
239                     std::map<QString, QJsonValue> alternatives;
240                     for (const QString& key : data.keys())
241                         if (key != Cantor::JupyterUtils::htmlMime && key != Cantor::JupyterUtils::textMime)
242                             alternatives[key] = data[key];
243 
244                     result = new Cantor::HtmlResult(html, text, alternatives);
245                 }
246             }
247             else if (mainKey == Cantor::JupyterUtils::latexMime)
248             {
249                 // Some latex results contains already rendered images, so use them, if presents
250                 const QImage& image = Cantor::JupyterUtils::loadImage(data, Cantor::JupyterUtils::pngMime);
251 
252                 QString latex = Cantor::JupyterUtils::fromJupyterMultiline(data.value(mainKey));
253                 QScopedPointer<Cantor::LatexRenderer> renderer(new Cantor::LatexRenderer(this));
254                 renderer->setLatexCode(latex);
255                 renderer->setEquationOnly(false);
256                 renderer->setMethod(Cantor::LatexRenderer::LatexMethod);
257                 renderer->renderBlocking();
258 
259                 result = new Cantor::LatexResult(latex, QUrl::fromLocalFile(renderer->imagePath()), text, image);
260 
261                 // If we have failed to render LaTeX i think Cantor should show the latex code at least
262                 if (!renderer->renderingSuccessful())
263                     static_cast<Cantor::LatexResult*>(result)->showCode();
264             }
265             // So this is image
266             else if (Cantor::JupyterUtils::imageKeys(data).contains(mainKey))
267             {
268                 const QImage& image = Cantor::JupyterUtils::loadImage(data, mainKey);
269                 result = new Cantor::ImageResult(image, text);
270                 static_cast<Cantor::ImageResult*>(result)->setOriginalFormat(mainKey);
271 
272                 if (mainKey == Cantor::JupyterUtils::svgMime)
273                     static_cast<Cantor::ImageResult*>(result)->setSvgContent(Cantor::JupyterUtils::fromJupyterMultiline(data[Cantor::JupyterUtils::svgMime]));
274 
275                 const QJsonValue size = metadata.value(mainKey);
276                 if (size.isObject())
277                 {
278                     int w = size.toObject().value(QLatin1String("width")).toInt(-1);
279                     int h = size.toObject().value(QLatin1String("height")).toInt(-1);
280 
281                     if (w != -1 && h != -1)
282                     {
283                         static_cast<Cantor::ImageResult*>(result)->setDisplaySize(QSize(w, h));
284                         // Remove size information, because we don't need it after setting display size
285                         // Also, we encode image to 'image/png' on saving as .ipynb, even original image don't png
286                         // So, without removing the size info here, after loading for example 'image/tiff' to Cantor from .ipynb and saving the worksheet
287                         // (which means, that the image will be saved as png and not as tiff).
288                         // We will have outdated key 'image/tiff' in metadata.
289                         metadata.remove(mainKey);
290                     }
291                 }
292 
293             }
294             else if (data.keys().size() == 1 && data.keys()[0] == Cantor::JupyterUtils::textMime)
295                 result = new Cantor::TextResult(text);
296             // Cantor don't know, how handle this, so pack into mime container result
297             else
298             {
299                 qDebug() << "Found unsupported " << outputType << "result with mimes" << data.keys() << ", so add them to mime container result";
300                 result = new Cantor::MimeResult(data);
301             }
302 
303             if (result)
304             {
305                 result->setJupyterMetadata(metadata);
306                 int resultIndex = output.value(QLatin1String("execution_count")).toInt(-1);
307                 if (resultIndex != -1)
308                     result->setExecutionIndex(resultIndex);
309 
310                 addResult(result);
311             }
312         }
313     }
314 
315     if (errorMessage().isEmpty())
316         setStatus(Done);
317     else
318         setStatus(Error);
319 }
320