diff --git a/CMakeLists.txt b/CMakeLists.txt index be537c2..160933c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,67 +1,67 @@ cmake_minimum_required(VERSION 3.5) -set(PIM_VERSION "5.14.40") +set(PIM_VERSION "5.14.41") project(KPkPass VERSION ${PIM_VERSION}) set(KF5_MIN_VERSION "5.70.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMAddTests) include(ECMGenerateHeaders) include(ECMQtDeclareLoggingCategory) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(GenerateExportHeader) ecm_setup_version(PROJECT VARIABLE_PREFIX KPKPASS VERSION_HEADER kpkpass_version.h PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KPimPkPassConfigVersion.cmake" ) set(QT_REQUIRED_VERSION "5.13.0") find_package(Qt5 ${QT_REQUIRED_VERSION} REQUIRED COMPONENTS Gui) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Archive) find_package(SharedMimeInfo 1.3 REQUIRED) option(NO_REGENERATE_MIME "Don't regenerate mime file (only for developper)" FALSE ) if (EXISTS "${CMAKE_SOURCE_DIR}/.git") add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050f00) add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x054700) endif() add_definitions(-DQT_NO_FOREACH) add_definitions(-DQT_NO_KEYWORDS) add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(autotests) endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) set(CMAKECONFIG_INSTALL_DIR "${CMAKECONFIG_INSTALL_PREFIX}/KPimPkPass") configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KPimPkPassConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KPimPkPassConfig.cmake" INSTALL_DESTINATION "${CMAKECONFIG_INSTALL_DIR}" ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KPimPkPassConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KPimPkPassConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel) install(EXPORT KPimPkPassTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KPimPkPassTargets.cmake NAMESPACE KPim:: ) diff --git a/src/pass.cpp b/src/pass.cpp index f424515..3d6f164 100644 --- a/src/pass.cpp +++ b/src/pass.cpp @@ -1,552 +1,557 @@ /* Copyright (c) 2017-2018 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "pass.h" #include "pass_p.h" #include "barcode.h" #include "boardingpass.h" #include "location.h" #include "logging.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KPkPass; static const char * const passTypes[] = { "boardingPass", "coupon", "eventTicket", "generic", "storeCard" }; static const auto passTypesCount = sizeof(passTypes) / sizeof(passTypes[0]); QJsonObject PassPrivate::passData() const { return passObj.value(QLatin1String(passTypes[passType])).toObject(); } QString PassPrivate::message(const QString &key) const { const auto it = messages.constFind(key); if (it != messages.constEnd()) { return it.value(); } return key; } void PassPrivate::parse() { // find the message catalog auto lang = QLocale().name(); auto idx = lang.indexOf(QLatin1Char('_')); if (idx > 0) { lang = lang.left(idx); } lang += QLatin1String(".lproj"); if (!parseMessages(lang)) { parseMessages(QStringLiteral("en.lproj")); } } static int indexOfUnquoted(const QString &catalog, QLatin1Char c, int start) { for (int i = start; i < catalog.size(); ++i) { const QChar catalogChar = catalog.at(i); if (catalogChar == c) { return i; } if (catalogChar == QLatin1Char('\\')) { ++i; } } return -1; } static QString unquote(const QStringRef &str) { QString res; res.reserve(str.size()); for (int i = 0; i < str.size(); ++i) { const auto c1 = str.at(i); if (c1 == QLatin1Char('\\') && i < str.size() - 1) { const auto c2 = str.at(i + 1); if (c2 == QLatin1Char('r')) { res.push_back(QLatin1Char('\r')); } else if (c2 == QLatin1Char('n')) { res.push_back(QLatin1Char('\n')); } else if (c2 == QLatin1Char('\\')) { res.push_back(c2); } else { res.push_back(c1); res.push_back(c2); } ++i; } else { res.push_back(c1); } } return res; } bool PassPrivate::parseMessages(const QString &lang) { auto entry = zip->directory()->entry(lang); if (!entry || !entry->isDirectory()) { return false; } auto dir = static_cast(entry); auto file = dir->file(QStringLiteral("pass.strings")); if (!file) { return false; } std::unique_ptr dev(file->createDevice()); const auto rawData = dev->readAll(); // this should be UTF-16BE, but that doesn't stop Eurowings from using UTF-8, // so do a primitive auto-detection here. UTF-16's first byte would either be the BOM // or \0. QString catalog; if (rawData.at(0) == '"') { catalog = QString::fromUtf8(rawData); } else { auto codec = QTextCodec::codecForName("UTF-16BE"); catalog = codec->toUnicode(rawData); } int idx = 0; while (idx < catalog.size()) { // key const auto keyBegin = indexOfUnquoted(catalog, QLatin1Char('"'), idx) + 1; if (keyBegin < 1) { break; } const auto keyEnd = indexOfUnquoted(catalog, QLatin1Char('"'), keyBegin); if (keyEnd <= keyBegin) { break; } // value const auto valueBegin = indexOfUnquoted(catalog, QLatin1Char('"'), keyEnd + 2) + 1; // there's at least also the '=' if (valueBegin <= keyEnd) { break; } const auto valueEnd = indexOfUnquoted(catalog, QLatin1Char('"'), valueBegin); if (valueEnd <= valueBegin) { break; } const auto key = catalog.mid(keyBegin, keyEnd - keyBegin); const auto value = unquote(catalog.midRef(valueBegin, valueEnd - valueBegin)); messages.insert(key, value); idx = valueEnd + 1; // there's at least the linebreak and/or a ';' } return !messages.isEmpty(); } QVector PassPrivate::fields(const QLatin1String &fieldType, const Pass *q) const { const auto a = passData().value(fieldType).toArray(); QVector f; f.reserve(a.size()); for (const auto &v : a) { f.push_back(Field{v.toObject(), q}); } return f; } Pass *PassPrivate::fromData(std::unique_ptr device, QObject *parent) { std::unique_ptr zip(new KZip(device.get())); if (!zip->open(QIODevice::ReadOnly)) { return nullptr; } // extract pass.json auto file = zip->directory()->file(QStringLiteral("pass.json")); if (!file) { return nullptr; } std::unique_ptr dev(file->createDevice()); QJsonParseError error; const auto passObj = QJsonDocument::fromJson(dev->readAll(), &error).object(); if (error.error != QJsonParseError::NoError) { qCWarning(Log) << "Error parsing pass.json:" << error.errorString(); return nullptr; } if (passObj.value(QLatin1String("formatVersion")).toInt() > 1) { qCWarning(Log) << "pass.json has unsupported format version!"; return nullptr; } // determine pass type int passTypeIdx = -1; for (unsigned int i = 0; i < passTypesCount; ++i) { if (passObj.contains(QLatin1String(passTypes[i]))) { passTypeIdx = static_cast(i); break; } } if (passTypeIdx < 0) { qCWarning(Log) << "pkpass file has no pass data structure!"; return nullptr; } Pass *pass = nullptr; switch (passTypeIdx) { case Pass::BoardingPass: pass = new KPkPass::BoardingPass(parent); break; default: pass = new Pass(static_cast(passTypeIdx), parent); break; } pass->d->buffer = std::move(device); pass->d->zip = std::move(zip); pass->d->passObj = passObj; pass->d->parse(); return pass; } Pass::Pass(Type passType, QObject *parent) : QObject(parent) , d(new PassPrivate) { d->passType = passType; } Pass::~Pass() = default; Pass::Type Pass::type() const { return d->passType; } QString Pass::description() const { return d->passObj.value(QLatin1String("description")).toString(); } QString Pass::organizationName() const { return d->passObj.value(QLatin1String("organizationName")).toString(); } QString Pass::passTypeIdentifier() const { return d->passObj.value(QLatin1String("passTypeIdentifier")).toString(); } QString Pass::serialNumber() const { return d->passObj.value(QLatin1String("serialNumber")).toString(); } QDateTime Pass::expirationDate() const { return QDateTime::fromString(d->passObj.value(QLatin1String("expirationDate")).toString(), Qt::ISODate); } bool Pass::isVoided() const { return d->passObj.value(QLatin1String("voided")).toString() == QLatin1String("true"); } QVector Pass::locations() const { QVector locs; const auto a = d->passObj.value(QLatin1String("locations")).toArray(); locs.reserve(a.size()); for (const auto &loc : a) { locs.push_back(Location(loc.toObject())); } return locs; } int Pass::maximumDistance() const { return d->passObj.value(QLatin1String("maxDistance")).toInt(500); } QDateTime Pass::relevantDate() const { return QDateTime::fromString(d->passObj.value(QLatin1String("relevantDate")).toString(), Qt::ISODate); } static QColor parseColor(const QString &s) { if (s.startsWith(QLatin1String("rgb("), Qt::CaseInsensitive)) { const auto l = s.midRef(4, s.length() - 5).split(QLatin1Char(',')); if (l.size() != 3) return {}; return QColor(l[0].trimmed().toInt(), l[1].trimmed().toInt(), l[2].trimmed().toInt()); } return QColor(s); } QColor Pass::backgroundColor() const { return parseColor(d->passObj.value(QLatin1String("backgroundColor")).toString()); } QColor Pass::foregroundColor() const { return parseColor(d->passObj.value(QLatin1String("foregroundColor")).toString()); } QString Pass::groupingIdentifier() const { return d->passObj.value(QLatin1String("groupingIdentifier")).toString(); } QColor Pass::labelColor() const { const auto c = parseColor(d->passObj.value(QLatin1String("labelColor")).toString()); if (c.isValid()) { return c; } return foregroundColor(); } QString Pass::logoText() const { return d->message(d->passObj.value(QLatin1String("logoText")).toString()); } QImage Pass::image(const QString& baseName, unsigned int devicePixelRatio) const { const KArchiveFile *file = nullptr; for (; devicePixelRatio > 1; --devicePixelRatio) { file = d->zip->directory()->file(baseName + QLatin1Char('@') + QString::number(devicePixelRatio) + QLatin1String("x.png")); if (file) break; } if (!file) file = d->zip->directory()->file(baseName + QLatin1String(".png")); if (!file) return {}; std::unique_ptr dev(file->createDevice()); auto img = QImage::fromData(dev->readAll()); img.setDevicePixelRatio(devicePixelRatio); return img; } QImage Pass::icon(unsigned int devicePixelRatio) const { return image(QStringLiteral("icon"), devicePixelRatio); } QImage Pass::logo(unsigned int devicePixelRatio) const { return image(QStringLiteral("logo"), devicePixelRatio); } QImage Pass::strip(unsigned int devicePixelRatio) const { return image(QStringLiteral("strip"), devicePixelRatio); } QImage Pass::background(unsigned int devicePixelRatio) const { return image(QStringLiteral("background"), devicePixelRatio); } -QImage KPkPass::Pass::footer(unsigned int devicePixelRatio) const +QImage Pass::footer(unsigned int devicePixelRatio) const { return image(QStringLiteral("footer"), devicePixelRatio); } +QImage Pass::thumbnail(unsigned int devicePixelRatio) const +{ + return image(QStringLiteral("thumbnail"), devicePixelRatio); +} + QString Pass::authenticationToken() const { return d->passObj.value(QLatin1String("authenticationToken")).toString(); } QUrl Pass::webServiceUrl() const { return QUrl(d->passObj.value(QLatin1String("webServiceURL")).toString()); } QUrl Pass::passUpdateUrl() const { QUrl url(webServiceUrl()); if (!url.isValid()) { return {}; } url.setPath(url.path() + QLatin1String("/v1/passes/") + passTypeIdentifier() + QLatin1Char('/') + serialNumber()); return url; } QVector Pass::barcodes() const { QVector codes; // barcodes array const auto a = d->passObj.value(QLatin1String("barcodes")).toArray(); codes.reserve(a.size()); for (const auto &bc : a) codes.push_back(Barcode(bc.toObject(), this)); // just a single barcode if (codes.isEmpty()) { const auto bc = d->passObj.value(QLatin1String("barcode")).toObject(); if (!bc.isEmpty()) codes.push_back(Barcode(bc, this)); } return codes; } static const char * const fieldNames[] = { "auxiliaryFields", "backFields", "headerFields", "primaryFields", "secondaryFields" }; static const auto fieldNameCount = sizeof(fieldNames) / sizeof(fieldNames[0]); QVector Pass::auxiliaryFields() const { return d->fields(QLatin1String(fieldNames[0]), this); } QVector Pass::backFields() const { return d->fields(QLatin1String(fieldNames[1]), this); } QVector Pass::headerFields() const { return d->fields(QLatin1String(fieldNames[2]), this); } QVector Pass::primaryFields() const { return d->fields(QLatin1String(fieldNames[3]), this); } QVector Pass::secondaryFields() const { return d->fields(QLatin1String(fieldNames[4]), this); } Field Pass::field(const QString& key) const { for (unsigned int i = 0; i < fieldNameCount; ++i) { const auto fs = d->fields(QLatin1String(fieldNames[i]), this); for (const auto &f : fs) { if (f.key() == key) { return f; } } } return {}; } QVector Pass::fields() const { QVector fs; for (unsigned int i = 0; i < fieldNameCount; ++i) { fs += d->fields(QLatin1String(fieldNames[i]), this); } return fs; } Pass *Pass::fromData(const QByteArray &data, QObject *parent) { std::unique_ptr buffer(new QBuffer); buffer->setData(data); buffer->open(QBuffer::ReadOnly); return PassPrivate::fromData(std::move(buffer), parent); } Pass *Pass::fromFile(const QString &fileName, QObject *parent) { std::unique_ptr file(new QFile(fileName)); if (file->open(QFile::ReadOnly)) { return PassPrivate::fromData(std::move(file), parent); } qCWarning(Log) << "Failed to open" << fileName << ":" << file->errorString(); return nullptr; } template static QVariantList toVariantList(const QVector &elems) { QVariantList l; l.reserve(elems.size()); std::for_each(elems.begin(), elems.end(), [&l](const T &e) { l.push_back(QVariant::fromValue(e)); }); return l; } QVariantList Pass::auxiliaryFieldsVariant() const { return toVariantList(auxiliaryFields()); } QVariantList Pass::backFieldsVariant() const { return toVariantList(backFields()); } QVariantList Pass::headerFieldsVariant() const { return toVariantList(headerFields()); } QVariantList Pass::primaryFieldsVariant() const { return toVariantList(primaryFields()); } QVariantList Pass::secondaryFieldsVariant() const { return toVariantList(secondaryFields()); } QVariantList Pass::barcodesVariant() const { return toVariantList(barcodes()); } QVariantList Pass::locationsVariant() const { return toVariantList(locations()); } QVariantMap Pass::fieldsVariantMap() const { QVariantMap m; const auto elems = fields(); std::for_each(elems.begin(), elems.end(), [&m](const Field &f) { m.insert(f.key(), QVariant::fromValue(f)); }); return m; } #include "moc_pass.cpp" diff --git a/src/pass.h b/src/pass.h index 9d7fd2d..66dfb00 100644 --- a/src/pass.h +++ b/src/pass.h @@ -1,181 +1,183 @@ /* Copyright (c) 2017-2018 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPKPASS_PASS_H #define KPKPASS_PASS_H #include "kpkpass_export.h" #include "field.h" #include #include #include class QByteArray; class QColor; class QDateTime; class QString; class QUrl; class QVariant; namespace KPkPass { class Barcode; class Location; class PassPrivate; /** Base class for a pkpass file. * @see https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/PassKit_PG/index.html * @see https://developer.apple.com/library/content/documentation/UserExperience/Reference/PassKit_Bundle/Chapters/TopLevel.html */ class KPKPASS_EXPORT Pass : public QObject { Q_OBJECT Q_PROPERTY(Type type READ type CONSTANT) Q_PROPERTY(QString description READ description CONSTANT) Q_PROPERTY(QString organizationName READ organizationName CONSTANT) Q_PROPERTY(QString passTypeIdentifier READ passTypeIdentifier CONSTANT) Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT) Q_PROPERTY(QDateTime expirationDate READ expirationDate CONSTANT) Q_PROPERTY(bool isVoided READ isVoided CONSTANT) Q_PROPERTY(QDateTime relevantDate READ relevantDate CONSTANT) Q_PROPERTY(QColor backgroundColor READ backgroundColor CONSTANT) Q_PROPERTY(QColor foregroundColor READ foregroundColor CONSTANT) Q_PROPERTY(QString groupingIdentifier READ groupingIdentifier CONSTANT) Q_PROPERTY(QColor labelColor READ labelColor CONSTANT) Q_PROPERTY(QString logoText READ logoText CONSTANT) // needs to be QVariantList just for QML (Grantlee would also work with QVector Q_PROPERTY(QVariantList barcodes READ barcodesVariant CONSTANT) Q_PROPERTY(QVariantList auxiliaryFields READ auxiliaryFieldsVariant CONSTANT) Q_PROPERTY(QVariantList backFields READ backFieldsVariant CONSTANT) Q_PROPERTY(QVariantList headerFields READ headerFieldsVariant CONSTANT) Q_PROPERTY(QVariantList primaryFields READ primaryFieldsVariant CONSTANT) Q_PROPERTY(QVariantList secondaryFields READ secondaryFieldsVariant CONSTANT) Q_PROPERTY(QVariantList locations READ locationsVariant CONSTANT) Q_PROPERTY(QVariantMap field READ fieldsVariantMap CONSTANT) public: virtual ~Pass(); /** Type of the pass. */ enum Type { BoardingPass, Coupon, EventTicket, Generic, StoreCard }; Q_ENUM(Type) Q_REQUIRED_RESULT Type type() const; // standard keys Q_REQUIRED_RESULT QString description() const; Q_REQUIRED_RESULT QString organizationName() const; Q_REQUIRED_RESULT QString passTypeIdentifier() const; Q_REQUIRED_RESULT QString serialNumber() const; // expiration keys Q_REQUIRED_RESULT QDateTime expirationDate() const; Q_REQUIRED_RESULT bool isVoided() const; // relevance keys /** Locations associated with this pass. */ Q_REQUIRED_RESULT QVector locations() const; /** Distance in meters to any of the pass locations before this pass becomes relevant. */ Q_REQUIRED_RESULT int maximumDistance() const; Q_REQUIRED_RESULT QDateTime relevantDate() const; // visual appearance keys /** Returns all barcodes defined in the pass. */ Q_REQUIRED_RESULT QVector barcodes() const; Q_REQUIRED_RESULT QColor backgroundColor() const; Q_REQUIRED_RESULT QColor foregroundColor() const; Q_REQUIRED_RESULT QString groupingIdentifier() const; Q_REQUIRED_RESULT QColor labelColor() const; Q_REQUIRED_RESULT QString logoText() const; /** Returns an image asset of this pass. * @param baseName The name of the asset, without the file name extension. * @param devicePixelRatio The device pixel ration, for loading highdpi assets. */ QImage image(const QString &baseName, unsigned int devicePixelRatio = 1) const; /** Returns the pass icon. */ QImage icon(unsigned int devicePixelRatio = 1) const; /** Returns the pass logo. */ QImage logo(unsigned int devicePixelRatio = 1) const; /** Returns the strip image if present. */ QImage strip(unsigned int devicePixelRatio = 1) const; /** Returns the background image if present. */ QImage background(unsigned int devicePixelRatio = 1) const; /** Returns the footer image if present. */ QImage footer(unsigned int devicePixelRatio = 1) const; + /** Returns the thumbnail image if present. */ + QImage thumbnail(unsigned int devicePixelRatio = 1) const; // web service keys Q_REQUIRED_RESULT QString authenticationToken() const; Q_REQUIRED_RESULT QUrl webServiceUrl() const; /** Pass update URL. * @see https://developer.apple.com/library/content/documentation/PassKit/Reference/PassKit_WebService/WebService.html */ Q_REQUIRED_RESULT QUrl passUpdateUrl() const; QVector auxiliaryFields() const; QVector backFields() const; QVector headerFields() const; QVector primaryFields() const; QVector secondaryFields() const; /** Returns the field with key @p key. */ Field field(const QString &key) const; /** Returns all fields found in this pass. */ QVector fields() const; /** Create a appropriate sub-class based on the pkpass file type. */ static Pass *fromData(const QByteArray &data, QObject *parent = nullptr); /** Create a appropriate sub-class based on the pkpass file type. */ static Pass *fromFile(const QString &fileName, QObject *parent = nullptr); protected: ///@cond internal friend class Barcode; friend class Field; friend class PassPrivate; explicit Pass (Type passType, QObject *parent = nullptr); std::unique_ptr d; ///@endcond private: QVariantList auxiliaryFieldsVariant() const; QVariantList backFieldsVariant() const; QVariantList headerFieldsVariant() const; QVariantList primaryFieldsVariant() const; QVariantList secondaryFieldsVariant() const; QVariantList barcodesVariant() const; QVariantList locationsVariant() const; QVariantMap fieldsVariantMap() const; }; } #endif // KPKPASS_PASS_H