Commit ce9e265b authored by Nikita Sirgienko's avatar Nikita Sirgienko
Browse files

[Jupyter] Add support for attachments inside Markdown cells

parent 68b693ed
......@@ -499,7 +499,7 @@ QJsonValue CommandEntry::toJupyterJson()
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
data.insert(QLatin1String("image/png"), QString::fromLatin1(ba.toBase64()));
data.insert(JupyterUtils::pngMime, QString::fromLatin1(ba.toBase64()));
root.insert(QLatin1String("data"), data);
......
......@@ -29,6 +29,10 @@
#include <QJsonDocument>
#include <QStringList>
#include <QSet>
#include <QImageReader>
#include <QImageWriter>
#include <QBuffer>
#include <QString>
const QString JupyterUtils::cellsKey = QLatin1String("cells");
const QString JupyterUtils::metadataKey = QLatin1String("metadata");
......@@ -41,6 +45,9 @@ const QString JupyterUtils::outputTypeKey = QLatin1String("output_type");
const QString JupyterUtils::executionCountKey = QLatin1String("execution_count");
const QString JupyterUtils::outputsKey = QLatin1String("outputs");
const QString JupyterUtils::dataKey = QLatin1String("data");
const QString JupyterUtils::pngMime = QLatin1String("image/png");
const QMimeDatabase JupyterUtils::mimeDatabase;
QJsonValue JupyterUtils::toJupyterMultiline(const QString& source)
{
......@@ -251,3 +258,65 @@ QJsonObject JupyterUtils::getKernelspec(const Cantor::Backend* backend)
return kernelspec;
}
QImage JupyterUtils::loadImage(const QJsonValue& mimeBundle, const QString& key)
{
QImage image;
if (mimeBundle.isObject())
{
const QJsonObject& bundleObject = mimeBundle.toObject();
const QJsonValue& data = bundleObject.value(key);
if (data.isString())
{
// In jupyter mime-bundle key for data is mime type of this data
// So we need convert mimetype to format, for example "image/png" to "png"
// for loading from data
if (QImageReader::supportedMimeTypes().contains(key.toLatin1()))
{
// https://doc.qt.io/qt-5/qimagereader.html#supportedImageFormats
// Maybe there is a better way to convert image key to image format
// but this is all that I could to do
const QByteArray& format = mimeDatabase.mimeTypeForName(key).preferredSuffix().toLatin1();
const QString& base64 = data.toString();
image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), format.data());
}
}
}
return image;
}
QJsonObject JupyterUtils::packMimeBundle(const QImage& image, const QString& mime)
{
QJsonObject mimeBundle;
if (QImageWriter::supportedMimeTypes().contains(mime.toLatin1()))
{
const QByteArray& format = mimeDatabase.mimeTypeForName(mime).preferredSuffix().toLatin1();
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, format.data());
mimeBundle.insert(mime, QString::fromLatin1(ba.toBase64()));
}
return mimeBundle;
}
QStringList JupyterUtils::imageKeys(const QJsonValue& mimeBundle)
{
QStringList imageKeys;
if (mimeBundle.isObject())
{
const QStringList& keys = mimeBundle.toObject().keys();
const QList<QByteArray>& mimes = QImageReader::supportedMimeTypes();
for (const QString& key : keys)
if (mimes.contains(key.toLatin1()))
imageKeys.append(key);
}
return imageKeys;
}
......@@ -24,11 +24,14 @@
#include <vector>
#include <QString>
#include <QMimeDatabase>
class QJsonValue;
class QJsonObject;
class QJsonArray;
class QJsonDocument;
class QImage;
class QStringList;
namespace Cantor {
class Backend;
......@@ -71,6 +74,10 @@ class JupyterUtils
static QString getKernelName(const QJsonValue& kernelspecValue);
static QJsonObject getKernelspec(const Cantor::Backend* backend);
static QImage loadImage(const QJsonValue& mimeBundle, const QString& key);
static QJsonObject packMimeBundle(const QImage& image, const QString& mime);
static QStringList imageKeys(const QJsonValue& mimeBundle);
public:
static const QString cellsKey;
static const QString metadataKey;
......@@ -83,6 +90,9 @@ class JupyterUtils
static const QString executionCountKey;
static const QString outputsKey;
static const QString dataKey;
static const QString pngMime;
static const QMimeDatabase mimeDatabase;
};
#endif // JUPYTERUTILS_H
......@@ -193,37 +193,32 @@ void LatexEntry::setContentFromJupyter(const QJsonObject& cell)
if (outputs.size() == 1 && JupyterUtils::isJupyterDisplayOutput(outputs[0]))
{
const QJsonObject data = outputs[0].toObject().value(JupyterUtils::dataKey).toObject();
const QJsonValue imageData = data.value(QLatin1String("image/png"));
if (imageData.isString())
const QImage& image = JupyterUtils::loadImage(data, JupyterUtils::pngMime);
if (!image.isNull())
{
const QByteArray& ba = QByteArray::fromBase64(imageData.toString().toLatin1());
QImage image;
if (image.loadFromData(ba))
{
QUrl internal;
internal.setScheme(QLatin1String("internal"));
internal.setPath(QUuid::createUuid().toString());
m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
m_renderedFormat.setName(internal.url());
m_renderedFormat.setWidth(image.width());
m_renderedFormat.setHeight(image.height());
m_renderedFormat.setProperty(EpsRenderer::CantorFormula, EpsRenderer::LatexFormula);
m_renderedFormat.setProperty(EpsRenderer::Code, m_latex);
cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
useLatexCode = false;
m_textItem->denyEditing();
}
QUrl internal;
internal.setScheme(QLatin1String("internal"));
internal.setPath(QUuid::createUuid().toString());
m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
m_renderedFormat.setName(internal.url());
m_renderedFormat.setWidth(image.width());
m_renderedFormat.setHeight(image.height());
m_renderedFormat.setProperty(EpsRenderer::CantorFormula, EpsRenderer::LatexFormula);
m_renderedFormat.setProperty(EpsRenderer::Code, m_latex);
cursor.insertText(QString(QChar::ObjectReplacementCharacter), m_renderedFormat);
useLatexCode = false;
m_textItem->denyEditing();
}
}
if (useLatexCode)
{
cursor.insertText(m_latex);
m_latex.clear(); // We no rendered image, so clear
m_latex.clear(); // We don't render image, so clear latex code cache
}
}
......@@ -260,7 +255,7 @@ QJsonValue LatexEntry::toJupyterJson()
imageResult.insert(JupyterUtils::outputTypeKey, QLatin1String("display_data"));
QJsonObject data;
data.insert(QLatin1String("image/png"), JupyterUtils::toJupyterMultiline(QString::fromLatin1(ba.toBase64())));
data.insert(JupyterUtils::pngMime, JupyterUtils::toJupyterMultiline(QString::fromLatin1(ba.toBase64())));
imageResult.insert(QLatin1String("data"), data);
imageResult.insert(JupyterUtils::metadataKey, QJsonObject());
......
......@@ -142,19 +142,18 @@ void LoadedExpression::loadFromJupyter(const QJsonObject& cell)
{
const QJsonObject& data = output.value(QLatin1String("data")).toObject();
const QLatin1String imageKey("image/png");
const QLatin1String textKey("text/plain");
if (data.contains(imageKey))
if (data.contains(JupyterUtils::pngMime))
{
// Load image data from base64, save into file, and create image result from this file
// Jupyter TODO: maybe add way to create ImageResult directly from QImage?
QString base64 = data.value(imageKey).toString();
QString base64 = data.value(JupyterUtils::pngMime).toString();
QImage image;
image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), imageKey.data());
image.loadFromData(QByteArray::fromBase64(base64.toLatin1()),"PNG");
const QJsonObject& metadata = JupyterUtils::getMetadata(output);
const QJsonValue size = metadata.value(imageKey);
const QJsonValue size = metadata.value(JupyterUtils::pngMime);
if (size.isObject())
{
int w = size.toObject().value(QLatin1String("width")).toInt();
......
......@@ -23,6 +23,8 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QImage>
#include <QImageReader>
#include "jupyterutils.h"
#include <config-cantor.h>
......@@ -107,7 +109,25 @@ void MarkdownEntry::setContentFromJupyter(const QJsonObject& cell)
if (!JupyterUtils::isMarkdownCell(cell))
return;
// Jupyter TODO: handle metadata and attachments
// Jupyter TODO: handle metadata
const QJsonObject attachments = cell.value(QLatin1String("attachments")).toObject();
for (const QString& key : attachments.keys())
{
const QJsonValue& attachment = attachments.value(key);
const QStringList& keys = JupyterUtils::imageKeys(attachment);
// Jupyter TODO: what if keys will be 2?
// Is it valid scheme at all?
if (keys.size() == 1)
{
const QString& mimeKey = keys[0];
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(adaptJupyterMarkdown(JupyterUtils::getSource(cell)));
}
......@@ -143,6 +163,20 @@ QJsonValue MarkdownEntry::toJupyterJson()
// Jupyter TODO: Handle metadata
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<QImage>();
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;
......
......@@ -21,6 +21,8 @@
#ifndef MARKDOWNENTRY_H
#define MARKDOWNENTRY_H
#include <vector>
#include "worksheetentry.h"
#include "worksheettextitem.h"
......@@ -77,6 +79,7 @@ class MarkdownEntry : public WorksheetEntry
QString plain;
QString html;
bool rendered;
std::vector<std::pair<QUrl,QString>> attachedImages;
};
#endif //MARKDOWNENTRY_H
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment