Source code
Revision control
Copy as Markdown
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qwasmdom.h"
#include <QtCore/qdebug.h>
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qpoint.h>
#include <QtCore/qrect.h>
#include <QtGui/qimage.h>
#include <private/qstdweb_p.h>
#include <private/qwasmlocalfileengine_p.h>
#include <QtCore/qurl.h>
#include <QtCore/QBuffer>
#include <utility>
#include <emscripten/wire.h>
QT_BEGIN_NAMESPACE
namespace dom {
namespace {
std::string dropActionToDropEffect(Qt::DropAction action)
{
switch (action) {
case Qt::DropAction::CopyAction:
return "copy";
case Qt::DropAction::IgnoreAction:
return "none";
case Qt::DropAction::LinkAction:
return "link";
case Qt::DropAction::MoveAction:
case Qt::DropAction::TargetMoveAction:
return "move";
case Qt::DropAction::ActionMask:
Q_ASSERT(false);
return "";
}
}
} // namespace
DataTransfer::DataTransfer(emscripten::val webDataTransfer)
: webDataTransfer(webDataTransfer) {
}
DataTransfer::~DataTransfer() = default;
DataTransfer::DataTransfer(const DataTransfer &other) = default;
DataTransfer::DataTransfer(DataTransfer &&other) = default;
DataTransfer &DataTransfer::operator=(const DataTransfer &other) = default;
DataTransfer &DataTransfer::operator=(DataTransfer &&other) = default;
void DataTransfer::setDragImage(emscripten::val element, const QPoint &hotspot)
{
webDataTransfer.call<void>("setDragImage", element, emscripten::val(hotspot.x()),
emscripten::val(hotspot.y()));
}
void DataTransfer::setData(std::string format, std::string data)
{
webDataTransfer.call<void>("setData", emscripten::val(std::move(format)),
emscripten::val(std::move(data)));
}
void DataTransfer::setDropAction(Qt::DropAction action)
{
webDataTransfer.set("dropEffect", emscripten::val(dropActionToDropEffect(action)));
}
void DataTransfer::setDataFromMimeData(const QMimeData &mimeData)
{
for (const auto &format : mimeData.formats()) {
if (format.startsWith("text/")) {
auto data = mimeData.data(format);
setData(format.toStdString(), QString::fromLocal8Bit(data).toStdString());
} else if (format == "application/x-qt-image") {
auto image = qvariant_cast<QImage>(mimeData.imageData());
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
buffer.close();
setData(format.toStdString(), "QB64" + QString::fromLocal8Bit(byteArray.toBase64()).toStdString());
} else {
auto data = mimeData.data(format);
auto encoded = "QB64" + QString::fromLocal8Bit(data.toBase64()).toStdString();
setData(format.toStdString(), std::move(encoded));
}
}
}
// Converts a DataTransfer instance to a QMimeData instance. Invokes the
// given callback when the conversion is complete. The callback takes ownership
// of the QMimeData.
void DataTransfer::toMimeDataWithFile(std::function<void(QMimeData *)> callback)
{
enum class ItemKind {
File,
String,
};
class MimeContext {
public:
MimeContext(int itemCount, std::function<void(QMimeData *)> callback)
:m_remainingItemCount(itemCount), m_callback(callback)
{
}
void deref() {
if (--m_remainingItemCount > 0)
return;
QList<QUrl> allUrls;
allUrls.append(mimeData->urls());
allUrls.append(fileUrls);
mimeData->setUrls(allUrls);
m_callback(mimeData);
// Delete temporary files; we expect that the user callback reads/copies
// file content before returning.// Fixme: tie file lifetime to lifetime of the QMimeData?
// Note: QWasmFileEngine files (weblocalfile://) are managed by QWasmFileEngine
// and are not deleted here
for (QUrl fileUrl: fileUrls) {
if (!QWasmFileEngineHandler::isWasmFileName(fileUrl.toString()))
QFile(fileUrl.toLocalFile()).remove();
}
delete this;
}
QMimeData *mimeData = new QMimeData();
QList<QUrl> fileUrls;
private:
int m_remainingItemCount;
std::function<void(QMimeData *)> m_callback;
};
const auto items = webDataTransfer["items"];
const int itemCount = items["length"].as<int>();
const int fileCount = webDataTransfer["files"]["length"].as<int>();
MimeContext *mimeContext = new MimeContext(itemCount, callback);
for (int i = 0; i < itemCount; ++i) {
const auto item = items[i];
const auto itemKind =
item["kind"].as<std::string>() == "string" ? ItemKind::String : ItemKind::File;
const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>());
switch (itemKind) {
case ItemKind::File: {
qstdweb::File webfile(item.call<emscripten::val>("getAsFile"));
// Add a file access url for the local file. If asyncify is available,
// add a QWasmFileEngine managed url. Else fall back to placing a copy
// of the file at /tmp on Emsripten's in-memory file system.
if (qstdweb::haveAsyncify()) {
const auto fileName = QWasmFileEngineHandler::addFile(webfile); // because QUrl::toLocalFile does not work
QUrl fileUrl(fileName);
mimeContext->fileUrls.append(fileUrl);
QString mimeFormat = QString::fromStdString(webfile.type());
if (fileCount == 1 && mimeFormat.contains("image/")) {
QImage image;
if (image.load(fileName))
mimeContext->mimeData->setImageData(image);
else
qWarning() << "Failed to load" << fileName;
}
mimeContext->deref();
} else {
// Limit in-memory file size to 1 GB
if (webfile.size() > 1e+9) {
qWarning() << "File is too large (> 1GB) and will be skipped. File size is" << webfile.size();
mimeContext->deref();
continue;
}
// Read file content
QByteArray fileContent(webfile.size(), Qt::Uninitialized);
webfile.stream(fileContent.data(), [=]() {
QDir qtTmpDir("/qt/tmp/"); // "tmp": indicate that these files won't stay around
qtTmpDir.mkpath(qtTmpDir.path());
QUrl fileUrl = QUrl::fromLocalFile(qtTmpDir.filePath(QString::fromStdString(webfile.name())));
mimeContext->fileUrls.append(fileUrl);
QFile file(fileUrl.toLocalFile());
if (!file.open(QFile::WriteOnly)) {
qWarning() << "File was not opened";
mimeContext->deref();
return;
}
if (file.write(fileContent) < 0)
qWarning() << "Write failed";
file.close();
// If we get a single file, and that file is an image, then
// try to decode the image data. This handles the case where
// image data (i.e. not an image file) is pasted. The browsers
// will then create a fake "image.png" file which has the image
// data. As a side effect Qt will also decode the image for
// single-image-file drops, since there is no way to differentiate
// the fake "image.png" from a real one.
QString mimeFormat = QString::fromStdString(webfile.type());
if (fileCount == 1 && mimeFormat.contains("image/")) {
QImage image;
if (image.loadFromData(fileContent))
mimeContext->mimeData->setImageData(image);
}
mimeContext->deref();
});
}
break;
}
case ItemKind::String:
if (itemMimeType.contains("STRING", Qt::CaseSensitive)
|| itemMimeType.contains("TEXT", Qt::CaseSensitive)) {
mimeContext->deref();
break;
}
QString a;
QString data = QString::fromEcmaString(webDataTransfer.call<emscripten::val>(
"getData", emscripten::val(itemMimeType.toStdString())));
if (!data.isEmpty()) {
if (itemMimeType == "text/html")
mimeContext->mimeData->setHtml(data);
else if (itemMimeType.isEmpty() || itemMimeType == "text/plain")
mimeContext->mimeData->setText(data); // the type can be empty
else if (itemMimeType.isEmpty() || itemMimeType == "text/uri-list") {
QList<QUrl> urls;
urls.append(data);
mimeContext->mimeData->setUrls(urls);
} else if (itemMimeType == "application/x-qt-image") {
if (data.startsWith("QB64")) {
data.remove(0, 4);
auto ba = QByteArray::fromBase64(QByteArray::fromStdString(data.toStdString()));
QImage image;
image.loadFromData(ba);
mimeContext->mimeData->setImageData(image);
}
} else {
// TODO improve encoding
if (data.startsWith("QB64")) {
data.remove(0, 4);
mimeContext->mimeData->setData(itemMimeType,
QByteArray::fromBase64(QByteArray::fromStdString(
data.toStdString())));
} else {
mimeContext->mimeData->setData(itemMimeType, data.toLocal8Bit());
}
}
}
mimeContext->deref();
break;
}
} // for items
}
// Creates a preview QMimeData which contains the MIME types but with empty
// data. This is useful when handling native dragOver events where the
// native DataTransfer API does not provide drag data (the data becomes
// available on the drop event).
QMimeData *DataTransfer::toMimeDataPreview()
{
auto data = new QMimeData();
QList<QUrl> uriList;
for (int i = 0; i < webDataTransfer["items"]["length"].as<int>(); ++i) {
const auto item = webDataTransfer["items"][i];
if (item["kind"].as<std::string>() == "file") {
uriList.append(QUrl());
} else {
const auto itemMimeType = QString::fromStdString(item["type"].as<std::string>());
data->setData(itemMimeType, QByteArray());
}
}
if (!uriList.isEmpty())
data->setUrls(uriList);
return data;
}
void syncCSSClassWith(emscripten::val element, std::string cssClassName, bool flag)
{
if (flag) {
element["classList"].call<void>("add", emscripten::val(std::move(cssClassName)));
return;
}
element["classList"].call<void>("remove", emscripten::val(std::move(cssClassName)));
}
QPointF mapPoint(emscripten::val source, emscripten::val target, const QPointF &point)
{
const auto sourceBoundingRect =
QRectF::fromDOMRect(source.call<emscripten::val>("getBoundingClientRect"));
const auto targetBoundingRect =
QRectF::fromDOMRect(target.call<emscripten::val>("getBoundingClientRect"));
const auto offset = sourceBoundingRect.topLeft() - targetBoundingRect.topLeft();
return point + offset;
}
void drawImageToWebImageDataArray(const QImage &sourceImage, emscripten::val destinationImageData,
const QRect &sourceRect)
{
Q_ASSERT_X(destinationImageData["constructor"]["name"].as<std::string>() == "ImageData",
Q_FUNC_INFO, "The destination should be an ImageData instance");
constexpr int BytesPerColor = 4;
if (sourceRect.width() == sourceImage.width()) {
// Copy a contiguous chunk of memory
// ...............
// OOOOOOOOOOOOOOO
// OOOOOOOOOOOOOOO -> image data
// OOOOOOOOOOOOOOO
// ...............
auto imageMemory = emscripten::typed_memory_view(sourceRect.width() * sourceRect.height()
* BytesPerColor,
sourceImage.constScanLine(sourceRect.y()));
destinationImageData["data"].call<void>(
"set", imageMemory, sourceRect.y() * sourceImage.width() * BytesPerColor);
} else {
// Go through the scanlines manually to set the individual lines in bulk. This is
// marginally less performant than the above.
// ...............
// ...OOOOOOOOO... r = 0 -> image data
// ...OOOOOOOOO... r = 1 -> image data
// ...OOOOOOOOO... r = 2 -> image data
// ...............
for (int row = 0; row < sourceRect.height(); ++row) {
auto scanlineMemory =
emscripten::typed_memory_view(sourceRect.width() * BytesPerColor,
sourceImage.constScanLine(row + sourceRect.y())
+ BytesPerColor * sourceRect.x());
destinationImageData["data"].call<void>("set", scanlineMemory,
(sourceRect.y() + row) * sourceImage.width()
* BytesPerColor
+ sourceRect.x() * BytesPerColor);
}
}
}
} // namespace dom
QT_END_NAMESPACE