// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include #include #include #include #include #include #include #include #if defined(Q_OS_MACOS) #import #else #include #endif #if defined(QT_PLATFORM_UIKIT) #import #endif #include "qutimimeconverter.h" #include "qmacmimeregistry_p.h" #include "qguiapplication.h" #include "private/qcore_mac_p.h" QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; /***************************************************************************** QDnD debug facilities *****************************************************************************/ //#define DEBUG_MIME_MAPS /*! \class QUtiMimeConverter \brief The QUtiMimeConverter class converts between a MIME type and a \l{https://developer.apple.com/documentation/uniformtypeidentifiers} {Uniform Type Identifier (UTI)} format. \since 6.5 \ingroup draganddrop \inmodule QtGui Qt's drag and drop and clipboard facilities use the MIME standard. On X11, this maps trivially to the Xdnd protocol. On Mac, although some applications use MIME to describe clipboard contents, it is more common to use Apple's UTI format. QUtiMimeConverter's role is to bridge the gap between MIME and UTI; By subclasses this class, one can extend Qt's drag and drop and clipboard handling to convert to and from unsupported, or proprietary, UTI formats. Construct an instance of your converter implementation after instantiating QGuiApplication: \code int main(int argc, char **argv) { QGuiApplication app(argc, argv); JsonMimeConverter jsonConverter; } \endcode Destroying the instance will unregister the converter and remove support for the conversion. It is also valid to heap-allocate the converter instance; Qt takes ownership and will delete the converter object during QGuiApplication shut-down. Qt has predefined support for the following UTIs: \list \li public.utf8-plain-text - converts to "text/plain" \li public.utf16-plain-text - converts to "text/plain" \li public.text - converts to "text/plain" \li public.html - converts to "text/html" \li public.url - converts to "text/uri-list" \li public.file-url - converts to "text/uri-list" \li public.tiff - converts to "application/x-qt-image" \li public.vcard - converts to "text/plain" \li com.apple.traditional-mac-plain-text - converts to "text/plain" \li com.apple.pict - converts to "application/x-qt-image" \endlist When working with MIME data, Qt will iterate through all instances of QUtiMimeConverter to find find an instance that can convert to, or from, a specific MIME type. It will do this by calling mimeForUti() or utiForMime() on each instance, starting with (and choosing) the last created instance first. The actual conversions will be done by using convertToMime() and convertFromMime(). */ /*! \enum QUtiMimeConverter::HandlerScope \internal */ /*! \internal Constructs a new conversion object of type \a scope, adding it to the globally accessed list of available converters. */ QUtiMimeConverter::QUtiMimeConverter(HandlerScope scope) : m_scope(scope) { QMacMimeRegistry::registerMimeConverter(this); } /*! Constructs a new conversion object and adds it to the globally accessed list of available converters. Call this constructor after QGuiApplication has been created. */ QUtiMimeConverter::QUtiMimeConverter() : QUtiMimeConverter(HandlerScopeFlag::All) { } /*! Destroys a conversion object, removing it from the global list of available converters. */ QUtiMimeConverter::~QUtiMimeConverter() { QMacMimeRegistry::unregisterMimeConverter(this); } /*! Returns the item count for the given \a mimeData */ int QUtiMimeConverter::count(const QMimeData *mimeData) const { Q_UNUSED(mimeData); return 1; } /*! \fn bool QUtiMimeConverter::canConvert(const QString &mime, const QString &uti) const Returns \c true if the converter can convert (both ways) between \a mime and \a uti; otherwise returns \c false. */ /*! \fn QString QUtiMimeConverter::mimeForUti(const QString &uti) const Returns the MIME type used for Mac UTI \a uti, or an empty string if this converter does not support converting from \a uti. All subclasses must reimplement this pure virtual function. */ /*! \fn QString QUtiMimeConverter::utiForMime(const QString &mime) const Returns the Mac UTI used for MIME type \a mime, or an empty string if this converter does not support converting from \a mime. All subclasses must reimplement this pure virtual function. */ /*! \fn QVariant QUtiMimeConverter::convertToMime(const QString &mime, const QList &data, const QString &uti) const Returns \a data converted from Mac UTI \a uti to MIME type \a mime. Note that Mac UTIs must all be self-terminating. The input \a data may contain trailing data. All subclasses must reimplement this pure virtual function. */ /*! \fn QList QUtiMimeConverter::convertFromMime(const QString &mime, const QVariant &data, const QString & uti) const Returns \a data converted from MIME type \a mime to Mac UTI \a uti. Note that Mac UTIs must all be self-terminating. The return value may contain trailing data. All subclasses must reimplement this pure virtual function. */ class QMacMimeAny : public QUtiMimeConverter { public: QMacMimeAny() : QUtiMimeConverter(HandlerScopeFlag::AllCompatible) {} QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeAny::utiForMime(const QString &mime) const { // do not handle the mime type name in the drag pasteboard if (mime == "application/x-qt-mime-type-name"_L1) return QString(); QString ret = "com.trolltech.anymime."_L1 + mime; return ret.replace(u'/', "--"_L1); } QString QMacMimeAny::mimeForUti(const QString &uti) const { const QString any_prefix = "com.trolltech.anymime."_L1; if (uti.size() > any_prefix.length() && uti.startsWith(any_prefix)) return uti.mid(any_prefix.length()).replace("--"_L1, "/"_L1); return QString(); } QVariant QMacMimeAny::convertToMime(const QString &mime, const QList &data, const QString &) const { if (data.count() > 1) qWarning("QMacMimeAny: Cannot handle multiple member data"); QVariant ret; if (mime == "text/plain"_L1) ret = QString::fromUtf8(data.first()); else ret = data.first(); return ret; } QList QMacMimeAny::convertFromMime(const QString &mime, const QVariant &data, const QString &) const { QList ret; if (mime == "text/plain"_L1) ret.append(data.toString().toUtf8()); else ret.append(data.toByteArray()); return ret; } class QMacMimeTypeName : public QUtiMimeConverter { private: public: QMacMimeTypeName(): QUtiMimeConverter(HandlerScopeFlag::AllCompatible) {} QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeTypeName::utiForMime(const QString &mime) const { if (mime == "application/x-qt-mime-type-name"_L1) return u"com.trolltech.qt.MimeTypeName"_s; return QString(); } QString QMacMimeTypeName::mimeForUti(const QString &) const { return QString(); } QVariant QMacMimeTypeName::convertToMime(const QString &, const QList &, const QString &) const { QVariant ret; return ret; } QList QMacMimeTypeName::convertFromMime(const QString &, const QVariant &, const QString &) const { QList ret; ret.append(QString("x-qt-mime-type-name"_L1).toUtf8()); return ret; } class QMacMimePlainTextFallback : public QUtiMimeConverter { public: QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimePlainTextFallback::utiForMime(const QString &mime) const { if (mime == "text/plain"_L1) return "public.text"_L1; return QString(); } QString QMacMimePlainTextFallback::mimeForUti(const QString &uti) const { if (uti == "public.text"_L1) return "text/plain"_L1; return QString(); } QVariant QMacMimePlainTextFallback::convertToMime(const QString &mimetype, const QList &data, const QString &uti) const { if (data.count() > 1) qWarning("QMacMimePlainTextFallback: Cannot handle multiple member data"); if (uti == "public.text"_L1) { // Note that public.text is documented by Apple to have an undefined encoding. From // testing it seems that utf8 is normally used, at least by Safari on iOS. const QByteArray &firstData = data.first(); return QString(QCFString(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast(firstData.constData()), firstData.size(), kCFStringEncodingUTF8, false))); } else { qWarning("QMime::convertToMime: unhandled mimetype: %s", qPrintable(mimetype)); } return QVariant(); } QList QMacMimePlainTextFallback::convertFromMime(const QString &, const QVariant &data, const QString &uti) const { QList ret; QString string = data.toString(); if (uti == "public.text"_L1) ret.append(string.toUtf8()); return ret; } class QMacMimeUnicodeText : public QUtiMimeConverter { public: QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeUnicodeText::utiForMime(const QString &mime) const { if (mime == "text/plain"_L1) return "public.utf16-plain-text"_L1; if (qsizetype i = mime.indexOf("charset="_L1); i >= 0) { QString cs(mime.mid(i + 8).toLower()); i = cs.indexOf(u';'); if (i >= 0) cs = cs.left(i); if (cs == "system"_L1) return "public.utf8-plain-text"_L1; else if (cs == "iso-10646-ucs-2"_L1 || cs == "utf16"_L1) return "public.utf16-plain-text"_L1; } return QString(); } QString QMacMimeUnicodeText::mimeForUti(const QString &uti) const { if (uti == "public.utf16-plain-text"_L1 || uti == "public.utf8-plain-text"_L1) return "text/plain"_L1; return QString(); } QVariant QMacMimeUnicodeText::convertToMime(const QString &mimetype, const QList &data, const QString &uti) const { if (data.count() > 1) qWarning("QMacMimeUnicodeText: Cannot handle multiple member data"); const QByteArray &firstData = data.first(); // I can only handle two types (system and unicode) so deal with them that way QVariant ret; if (uti == "public.utf8-plain-text"_L1) { ret = QString::fromUtf8(firstData); } else if (uti == "public.utf16-plain-text"_L1) { QString str = QStringDecoder(QStringDecoder::Utf16)(firstData); ret = str; } else { qWarning("QMime::convertToMime: unhandled mimetype: %s", qPrintable(mimetype)); } return ret; } QList QMacMimeUnicodeText::convertFromMime(const QString &, const QVariant &data, const QString &uti) const { QList ret; QString string = data.toString(); if (uti == "public.utf8-plain-text"_L1) ret.append(string.toUtf8()); else if (uti == "public.utf16-plain-text"_L1) { QStringEncoder::Flags f; #if defined(Q_OS_MACOS) // Some applications such as Microsoft Excel, don't deal well with // a BOM present, so we follow the traditional approach of Qt on // macOS to not generate public.utf16-plain-text with a BOM. f = QStringEncoder::Flag::Default; #else // Whereas iOS applications will fail to paste if we do _not_ // include a BOM in the public.utf16-plain-text content, most // likely due to converting the data using NSUTF16StringEncoding // which assumes big-endian byte order if there is no BOM. f = QStringEncoder::Flag::WriteBom; #endif QStringEncoder encoder(QStringEncoder::Utf16, f); ret.append(encoder(string)); } return ret; } class QMacMimeHTMLText : public QUtiMimeConverter { public: QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeHTMLText::utiForMime(const QString &mime) const { if (mime == "text/html"_L1) return "public.html"_L1; return QString(); } QString QMacMimeHTMLText::mimeForUti(const QString &uti) const { if (uti == "public.html"_L1) return "text/html"_L1; return QString(); } QVariant QMacMimeHTMLText::convertToMime(const QString &mimeType, const QList &data, const QString &uti) const { if (!canConvert(mimeType, uti)) return QVariant(); if (data.count() > 1) qWarning("QMacMimeHTMLText: Cannot handle multiple member data"); return data.first(); } QList QMacMimeHTMLText::convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const { QList ret; if (!canConvert(mime, uti)) return ret; ret.append(data.toByteArray()); return ret; } class QMacMimeRtfText : public QUtiMimeConverter { public: QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeRtfText::utiForMime(const QString &mime) const { if (mime == "text/html"_L1) return "public.rtf"_L1; return QString(); } QString QMacMimeRtfText::mimeForUti(const QString &uti) const { if (uti == "public.rtf"_L1) return "text/html"_L1; return QString(); } QVariant QMacMimeRtfText::convertToMime(const QString &mimeType, const QList &data, const QString &uti) const { if (!canConvert(mimeType, uti)) return QVariant(); if (data.count() > 1) qWarning("QMacMimeHTMLText: Cannot handle multiple member data"); // Read RTF into to NSAttributedString, then convert the string to HTML NSAttributedString *string = [[NSAttributedString alloc] initWithData:data.at(0).toNSData() options:@{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType} documentAttributes:nil error:nil]; NSError *error; NSRange range = NSMakeRange(0, [string length]); NSDictionary *dict = @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType}; NSData *htmlData = [string dataFromRange:range documentAttributes:dict error:&error]; // Note: We trim the data here, as NSAttributedString wrongly inserts a newline at the // end when generating HTML, which HTML parsers (including our own QTextHtmlParser) // correctly treat as an additional white space in the body of the HTML document // (see HTML5 ยง 13.2.6.4.22 - The "after after body" insertion mode). return QByteArray::fromNSData(htmlData).trimmed(); } QList QMacMimeRtfText::convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const { QList ret; if (!canConvert(mime, uti)) return ret; NSAttributedString *string = [[NSAttributedString alloc] initWithData:data.toByteArray().toNSData() options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType} documentAttributes:nil error:nil]; NSError *error; NSRange range = NSMakeRange(0, [string length]); NSDictionary *dict = @{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType}; NSData *rtfData = [string dataFromRange:range documentAttributes:dict error:&error]; ret << QByteArray::fromNSData(rtfData); return ret; } class QMacMimeFileUri : public QUtiMimeConverter { public: QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; int count(const QMimeData *mimeData) const override; }; QString QMacMimeFileUri::utiForMime(const QString &mime) const { if (mime == "text/uri-list"_L1) return "public.file-url"_L1; return QString(); } QString QMacMimeFileUri::mimeForUti(const QString &uti) const { if (uti == "public.file-url"_L1) return "text/uri-list"_L1; return QString(); } QVariant QMacMimeFileUri::convertToMime(const QString &mime, const QList &data, const QString &uti) const { if (!canConvert(mime, uti)) return QVariant(); QList ret; for (int i = 0; i < data.size(); ++i) { const QByteArray &a = data.at(i); NSString *urlString = [[[NSString alloc] initWithBytesNoCopy:(void *)a.data() length:a.size() encoding:NSUTF8StringEncoding freeWhenDone:NO] autorelease]; NSURL *nsurl = [NSURL URLWithString:urlString]; QUrl url; // OS X 10.10 sends file references instead of file paths if ([nsurl isFileReferenceURL]) { url = QUrl::fromNSURL([nsurl filePathURL]); } else { url = QUrl::fromNSURL(nsurl); } if (url.host().toLower() == "localhost"_L1) url.setHost(QString()); url.setPath(url.path().normalized(QString::NormalizationForm_C)); ret.append(url); } return QVariant(ret); } QList QMacMimeFileUri::convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const { QList ret; if (!canConvert(mime, uti)) return ret; QList urls = data.toList(); for (int i = 0; i < urls.size(); ++i) { QUrl url = urls.at(i).toUrl(); if (url.scheme().isEmpty()) url.setScheme("file"_L1); if (url.scheme() == "file"_L1) { if (url.host().isEmpty()) url.setHost("localhost"_L1); url.setPath(url.path().normalized(QString::NormalizationForm_D)); } if (url.isLocalFile()) ret.append(url.toEncoded()); } return ret; } int QMacMimeFileUri::count(const QMimeData *mimeData) const { return mimeData->urls().count(); } class QMacMimeUrl : public QUtiMimeConverter { public: QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeUrl::utiForMime(const QString &mime) const { if (mime.startsWith("text/uri-list"_L1)) return "public.url"_L1; return QString(); } QString QMacMimeUrl::mimeForUti(const QString &uti) const { if (uti == "public.url"_L1) return "text/uri-list"_L1; return QString(); } QVariant QMacMimeUrl::convertToMime(const QString &mime, const QList &data, const QString &uti) const { if (!canConvert(mime, uti)) return QVariant(); QList ret; for (int i=0; i QMacMimeUrl::convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const { QList ret; if (!canConvert(mime, uti)) return ret; QList urls = data.toList(); for (int i=0; i &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeVCard::utiForMime(const QString &mime) const { if (mime.startsWith("text/vcard"_L1)) return "public.vcard"_L1; return QString(); } QString QMacMimeVCard::mimeForUti(const QString &uti) const { if (uti == "public.vcard"_L1) return "text/vcard"_L1; return QString(); } QVariant QMacMimeVCard::convertToMime(const QString &mime, const QList &data, const QString &uti) const { if (!canConvert(mime, uti)) return QVariant(); QByteArray cards; if (uti == "public.vcard"_L1) { for (int i=0; i QMacMimeVCard::convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const { QList ret; if (!canConvert(mime, uti)) return ret; if (mime == "text/vcard"_L1) ret.append(data.toString().toUtf8()); return ret; } extern QImage qt_mac_toQImage(CGImageRef image); extern CGImageRef qt_mac_toCGImage(const QImage &qImage); class QMacMimeTiff : public QUtiMimeConverter { public: QString utiForMime(const QString &mime) const override; QString mimeForUti(const QString &uti) const override; QVariant convertToMime(const QString &mime, const QList &data, const QString &uti) const override; QList convertFromMime(const QString &mime, const QVariant &data, const QString &uti) const override; }; QString QMacMimeTiff::utiForMime(const QString &mime) const { if (mime.startsWith("application/x-qt-image"_L1)) return "public.tiff"_L1; return QString(); } QString QMacMimeTiff::mimeForUti(const QString &uti) const { if (uti == "public.tiff"_L1) return "application/x-qt-image"_L1; return QString(); } QVariant QMacMimeTiff::convertToMime(const QString &mime, const QList &data, const QString &uti) const { if (data.count() > 1) qWarning("QMacMimeTiff: Cannot handle multiple member data"); if (!canConvert(mime, uti)) return QVariant(); QCFType tiffData = data.first().toRawCFData(); QCFType imageSource = CGImageSourceCreateWithData(tiffData, 0); if (QCFType image = CGImageSourceCreateImageAtIndex(imageSource, 0, 0)) return QVariant(qt_mac_toQImage(image)); return QVariant(); } QList QMacMimeTiff::convertFromMime(const QString &mime, const QVariant &variant, const QString &uti) const { if (!canConvert(mime, uti)) return QList(); QCFType data = CFDataCreateMutable(0, 0); QCFType imageDestination = CGImageDestinationCreateWithData(data, (CFStringRef)UTTypeTIFF.identifier, 1, 0); if (!imageDestination) return QList(); QImage img = qvariant_cast(variant); NSDictionary *props = @{ static_cast(kCGImagePropertyPixelWidth): @(img.width()), static_cast(kCGImagePropertyPixelHeight): @(img.height()) }; CGImageDestinationAddImage(imageDestination, qt_mac_toCGImage(img), static_cast(props)); CGImageDestinationFinalize(imageDestination); return QList() << QByteArray::fromCFData(data); } namespace QMacMimeRegistry { void registerBuiltInTypes() { // Create QMacMimeAny first to put it at the end of globalMimeList // with lowest priority. (the constructor prepends to the list) new QMacMimeAny; //standard types that we wrap new QMacMimeTiff; new QMacMimePlainTextFallback; new QMacMimeUnicodeText; new QMacMimeRtfText; new QMacMimeHTMLText; new QMacMimeFileUri; new QMacMimeUrl; new QMacMimeTypeName; new QMacMimeVCard; } } QT_END_NAMESPACE