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