File indexing completed on 2025-01-19 03:53:11
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2011-04-12 0007 * Description : A tool to export items to Rajce web service 0008 * 0009 * SPDX-FileCopyrightText: 2011 by Lukas Krejci <krejci.l at centrum dot cz> 0010 * SPDX-FileCopyrightText: 2011-2024 by Gilles Caulier <caulier dot gilles at gmail dot com> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include "rajcecommand.h" 0017 0018 // Qt includes 0019 0020 #include <QWidget> 0021 #include <QMutex> 0022 #include <QQueue> 0023 #include <QCryptographicHash> 0024 #include <QXmlQuery> 0025 #include <QXmlResultItems> 0026 #include <QFileInfo> 0027 #include <QUrl> 0028 #include <QRandomGenerator> 0029 0030 // Local includes 0031 0032 #include "digikam_debug.h" 0033 #include "digikam_version.h" 0034 #include "dmetadata.h" 0035 #include "wstoolutils.h" 0036 #include "previewloadthread.h" 0037 0038 using namespace Digikam; 0039 0040 namespace DigikamGenericRajcePlugin 0041 { 0042 0043 const unsigned THUMB_SIZE = 100; 0044 0045 struct PreparedImage 0046 { 0047 QString scaledImagePath; 0048 QString thumbPath; 0049 }; 0050 0051 PreparedImage s_prepareImageForUpload(const QString& saveDir, 0052 const QImage& img, 0053 const QString& imagePath, 0054 unsigned maxDimension, 0055 unsigned thumbDimension, 0056 int jpgQuality) 0057 { 0058 PreparedImage ret; 0059 0060 if (img.isNull()) 0061 { 0062 return ret; 0063 } 0064 0065 QImage image(img); 0066 0067 // get temporary file name 0068 QString baseName = saveDir + QFileInfo(imagePath).baseName().trimmed(); 0069 ret.scaledImagePath = baseName + QLatin1String(".jpg"); 0070 ret.thumbPath = baseName + QLatin1String(".thumb.jpg"); 0071 0072 if ((maxDimension > 0) && 0073 (((unsigned)image.width() > maxDimension) || 0074 ((unsigned)image.height() > maxDimension))) 0075 { 0076 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Resizing to " << maxDimension; 0077 image = image.scaled(maxDimension, maxDimension, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0078 } 0079 0080 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Saving to temp file: " << ret.scaledImagePath; 0081 image.save(ret.scaledImagePath, "JPEG", jpgQuality); 0082 0083 QImage thumb = image.scaled(thumbDimension, thumbDimension, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0084 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Saving thumb to temp file: " << ret.thumbPath; 0085 thumb.save(ret.thumbPath, "JPEG", jpgQuality); 0086 0087 // copy meta data to temporary image 0088 0089 QScopedPointer<DMetadata> meta(new DMetadata); 0090 0091 if (meta->load(imagePath)) 0092 { 0093 meta->setItemDimensions(image.size()); 0094 meta->setItemOrientation(MetaEngine::ORIENTATION_NORMAL); 0095 meta->setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY); 0096 meta->save(ret.scaledImagePath, true); 0097 } 0098 0099 return ret; 0100 } 0101 0102 // ----------------------------------------------------------------------- 0103 0104 class Q_DECL_HIDDEN RajceCommand::Private 0105 { 0106 public: 0107 0108 explicit Private() 0109 { 0110 commandType = Logout; 0111 } 0112 0113 QString name; 0114 RajceCommandType commandType; 0115 QMap<QString, QString> parameters; 0116 }; 0117 0118 RajceCommand::RajceCommand(const QString& name, RajceCommandType type) 0119 : QObject(nullptr), 0120 d(new Private) 0121 { 0122 d->name = name; 0123 d->commandType = type; 0124 } 0125 0126 RajceCommand::~RajceCommand() 0127 { 0128 delete d; 0129 } 0130 0131 QMap<QString, QString>& RajceCommand::parameters() const 0132 { 0133 return const_cast<QMap<QString, QString> &>(d->parameters); 0134 } 0135 0136 QString RajceCommand::getXml() const 0137 { 0138 QString ret(QLatin1String("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")); 0139 0140 ret.append(QLatin1String("<request>\n")); 0141 ret.append(QLatin1String(" <command>")).append(d->name).append(QLatin1String("</command>\n")); 0142 ret.append(QLatin1String(" <parameters>\n")); 0143 0144 Q_FOREACH (QString key, d->parameters.keys()) 0145 { 0146 ret.append(QLatin1String(" <")).append(key).append(QLatin1String(">")); 0147 ret.append(d->parameters[key]); 0148 ret.append(QLatin1String("</")).append(key).append(QLatin1String(">\n")); 0149 } 0150 0151 ret.append(QLatin1String("</parameters>\n")); 0152 ret.append(additionalXml()); 0153 ret.append(QLatin1String("\n</request>\n")); 0154 0155 return ret; 0156 } 0157 0158 bool RajceCommand::parseErrorFromQuery(QXmlQuery& query, RajceSession& state) 0159 { 0160 QString results; 0161 0162 query.setQuery(QLatin1String("/response/string(errorCode)")); 0163 query.evaluateTo(&results); 0164 0165 if (results.trimmed().length() > 0) 0166 { 0167 state.lastErrorCode() = results.toUInt(); 0168 query.setQuery(QLatin1String("/response/string(result)")); 0169 query.evaluateTo(&results); 0170 state.lastErrorMessage() = results.trimmed(); 0171 0172 return true; 0173 } 0174 0175 return false; 0176 } 0177 0178 void RajceCommand::processResponse(const QString& response, RajceSession& state) 0179 { 0180 QXmlQuery q; 0181 q.setFocus(response); 0182 0183 state.lastCommand() = d->commandType; 0184 0185 if (parseErrorFromQuery(q, state)) 0186 { 0187 cleanUpOnError(state); 0188 } 0189 else 0190 { 0191 parseResponse(q, state); 0192 } 0193 } 0194 0195 QString RajceCommand::additionalXml() const 0196 { 0197 return QString(); 0198 } 0199 0200 QByteArray RajceCommand::encode() const 0201 { 0202 QByteArray ret = QString::fromLatin1("data=").toLatin1(); 0203 ret.append(QUrl::toPercentEncoding(getXml())); 0204 0205 return ret; 0206 } 0207 0208 QString RajceCommand::contentType() const 0209 { 0210 return QLatin1String("application/x-www-form-urlencoded"); 0211 } 0212 0213 RajceCommandType RajceCommand::commandType() const 0214 { 0215 return d->commandType; 0216 } 0217 0218 // ----------------------------------------------------------------------- 0219 0220 OpenAlbumCommand::OpenAlbumCommand(unsigned albumId, const RajceSession& state) 0221 : RajceCommand(QLatin1String("openAlbum"), OpenAlbum) 0222 { 0223 parameters()[QLatin1String("token")] = state.sessionToken(); 0224 parameters()[QLatin1String("albumID")] = QString::number(albumId); 0225 } 0226 0227 void OpenAlbumCommand::parseResponse(QXmlQuery& q, RajceSession& state) 0228 { 0229 state.openAlbumToken() = QString(); 0230 0231 QString result; 0232 0233 q.setQuery(QLatin1String("/response/data(albumToken)")); 0234 q.evaluateTo(&result); 0235 0236 state.openAlbumToken() = result.trimmed(); 0237 } 0238 0239 void OpenAlbumCommand::cleanUpOnError(RajceSession& state) 0240 { 0241 state.openAlbumToken() = QString(); 0242 } 0243 0244 // ----------------------------------------------------------------------- 0245 0246 LoginCommand::LoginCommand(const QString& username, const QString& password) 0247 : RajceCommand(QLatin1String("login"), Login) 0248 { 0249 parameters()[QLatin1String("login")] = username; 0250 parameters()[QLatin1String("password")] = QLatin1String( 0251 QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Md5).toHex()); 0252 } 0253 0254 void LoginCommand::parseResponse(QXmlQuery& q, RajceSession& state) 0255 { 0256 QString results; 0257 0258 q.setQuery(QLatin1String("/response/string(maxWidth)")); 0259 q.evaluateTo(&results); 0260 state.maxWidth() = results.toUInt(); 0261 0262 q.setQuery(QLatin1String("/response/string(maxHeight)")); 0263 q.evaluateTo(&results); 0264 state.maxHeight() = results.toUInt(); 0265 0266 q.setQuery(QLatin1String("/response/string(quality)")); 0267 q.evaluateTo(&results); 0268 state.imageQuality() = results.toUInt(); 0269 0270 q.setQuery(QLatin1String("/response/string(nick)")); 0271 q.evaluateTo(&results); 0272 state.nickname() = results.trimmed(); 0273 0274 q.setQuery(QLatin1String("data(/response/sessionToken)")); 0275 q.evaluateTo(&results); 0276 state.sessionToken() = results.trimmed(); 0277 0278 state.username() = parameters()[QLatin1String("login")]; 0279 } 0280 0281 void LoginCommand::cleanUpOnError(RajceSession& state) 0282 { 0283 state.openAlbumToken() = QLatin1String(""); 0284 state.nickname() = QLatin1String(""); 0285 state.username() = QLatin1String(""); 0286 state.imageQuality() = 0; 0287 state.maxHeight() = 0; 0288 state.maxWidth() = 0; 0289 state.sessionToken() = QLatin1String(""); 0290 state.albums().clear(); 0291 } 0292 0293 // ----------------------------------------------------------------------- 0294 0295 CreateAlbumCommand::CreateAlbumCommand(const QString& name, 0296 const QString& description, 0297 bool visible, 0298 const RajceSession& state) 0299 : RajceCommand(QLatin1String("createAlbum"), CreateAlbum) 0300 { 0301 parameters()[QLatin1String("token")] = state.sessionToken(); 0302 parameters()[QLatin1String("albumName")] = name; 0303 parameters()[QLatin1String("albumDescription")] = description; 0304 parameters()[QLatin1String("albumVisible")] = visible ? QLatin1String("1") : QLatin1String("0"); 0305 } 0306 0307 void CreateAlbumCommand::parseResponse(QXmlQuery&, RajceSession&) 0308 { 0309 } 0310 0311 void CreateAlbumCommand::cleanUpOnError(RajceSession&) 0312 { 0313 } 0314 0315 void CloseAlbumCommand::parseResponse(QXmlQuery&, RajceSession&) 0316 { 0317 } 0318 0319 void CloseAlbumCommand::cleanUpOnError(RajceSession&) 0320 { 0321 } 0322 0323 // ----------------------------------------------------------------------- 0324 0325 CloseAlbumCommand::CloseAlbumCommand(const RajceSession& state) 0326 : RajceCommand(QLatin1String("closeAlbum"), CloseAlbum) 0327 { 0328 parameters()[QLatin1String("token")] = state.sessionToken(); 0329 parameters()[QLatin1String("albumToken")] = state.openAlbumToken(); 0330 } 0331 0332 // ----------------------------------------------------------------------- 0333 0334 AlbumListCommand::AlbumListCommand(const RajceSession& state) 0335 : RajceCommand(QLatin1String("getAlbumList"), ListAlbums) 0336 { 0337 parameters()[QLatin1String("token")] = state.sessionToken(); 0338 } 0339 0340 void AlbumListCommand::parseResponse(QXmlQuery& q, RajceSession& state) 0341 { 0342 state.albums().clear(); 0343 0344 QXmlResultItems results; 0345 0346 q.setQuery(QLatin1String("/response/albums/album")); 0347 q.evaluateTo(&results); 0348 0349 QXmlItem item(results.next()); 0350 0351 while (!item.isNull()) 0352 { 0353 q.setFocus(item); 0354 RajceAlbum album; 0355 QString detail; 0356 0357 q.setQuery(QLatin1String("data(./@id)")); 0358 q.evaluateTo(&detail); 0359 album.id = detail.toUInt(); 0360 0361 q.setQuery(QLatin1String("data(./albumName)")); 0362 q.evaluateTo(&detail); 0363 album.name = detail.trimmed(); 0364 0365 q.setQuery(QLatin1String("data(./description)")); 0366 q.evaluateTo(&detail); 0367 album.description = detail.trimmed(); 0368 0369 q.setQuery(QLatin1String("data(./url)")); 0370 q.evaluateTo(&detail); 0371 album.url = detail.trimmed(); 0372 0373 q.setQuery(QLatin1String("data(./thumbUrl)")); 0374 q.evaluateTo(&detail); 0375 album.thumbUrl = detail.trimmed(); 0376 0377 q.setQuery(QLatin1String("data(./createDate)")); 0378 q.evaluateTo(&detail); 0379 album.createDate = QDateTime::fromString(detail.trimmed(), QLatin1String("yyyy-MM-dd hh:mm:ss")); 0380 0381 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Create date: " << detail.trimmed() << " = " 0382 << QDateTime::fromString(detail.trimmed(), QLatin1String("yyyy-MM-dd hh:mm:ss")); 0383 0384 q.setQuery(QLatin1String("data(./updateDate)")); 0385 q.evaluateTo(&detail); 0386 album.updateDate = QDateTime::fromString(detail.trimmed(), QLatin1String("yyyy-MM-dd hh:mm:ss")); 0387 0388 q.evaluateTo(&detail); 0389 album.isHidden = detail.toUInt() != 0; 0390 0391 q.setQuery(QLatin1String("data(./secure)")); 0392 q.evaluateTo(&detail); 0393 album.isSecure = detail.toUInt() != 0; 0394 0395 q.setQuery(QLatin1String("data(./startDateInterval)")); 0396 q.evaluateTo(&detail); 0397 0398 if (detail.trimmed().length() > 0) 0399 { 0400 album.validFrom = QDateTime::fromString(detail, QLatin1String("yyyy-MM-dd hh:mm:ss")); 0401 } 0402 0403 q.setQuery(QLatin1String("data(./endDateInterval)")); 0404 q.evaluateTo(&detail); 0405 0406 if (detail.trimmed().length() > 0) 0407 { 0408 album.validTo = QDateTime::fromString(detail, QLatin1String("yyyy-MM-dd hh:mm:ss")); 0409 } 0410 0411 q.setQuery(QLatin1String("data(./thumbUrlBest)")); 0412 q.evaluateTo(&detail); 0413 album.bestQualityThumbUrl = detail.trimmed(); 0414 0415 state.albums().append(album); 0416 item = results.next(); 0417 } 0418 } 0419 0420 void AlbumListCommand::cleanUpOnError(RajceSession& state) 0421 { 0422 state.albums().clear(); 0423 } 0424 0425 // ----------------------------------------------------------------------- 0426 0427 class Q_DECL_HIDDEN AddPhotoCommand::Private 0428 { 0429 public: 0430 0431 explicit Private() 0432 { 0433 jpgQuality = 90; 0434 desiredDimension = 0; 0435 maxDimension = 0; 0436 form = nullptr; 0437 } 0438 0439 int jpgQuality; 0440 0441 unsigned desiredDimension; 0442 unsigned maxDimension; 0443 0444 QString tmpDir; 0445 QString imagePath; 0446 0447 QImage image; 0448 0449 RajceMPForm* form; 0450 }; 0451 0452 AddPhotoCommand::AddPhotoCommand(const QString& tmpDir, 0453 const QString& path, 0454 unsigned dimension, 0455 int jpgQuality, 0456 const RajceSession& state) 0457 : RajceCommand(QLatin1String("addPhoto"), AddPhoto), 0458 d(new Private) 0459 { 0460 d->jpgQuality = jpgQuality; 0461 d->desiredDimension = dimension; 0462 d->tmpDir = tmpDir; 0463 d->imagePath = path; 0464 d->image = PreviewLoadThread::loadHighQualitySynchronously(path).copyQImage(); 0465 0466 if (d->image.isNull()) 0467 { 0468 d->image.load(path); 0469 } 0470 0471 // cppcheck-suppress duplicateCondition 0472 if (d->image.isNull()) 0473 { 0474 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Could not read in an image from " 0475 << path << ". Adding the photo will not work."; 0476 return; 0477 } 0478 0479 d->maxDimension = (state.maxHeight() > state.maxWidth()) ? state.maxWidth() 0480 : state.maxHeight(); 0481 parameters()[QLatin1String("token")] = state.sessionToken(); 0482 parameters()[QLatin1String("albumToken")] = state.openAlbumToken(); 0483 d->form = new RajceMPForm; 0484 } 0485 0486 AddPhotoCommand::~AddPhotoCommand() 0487 { 0488 delete d->form; 0489 delete d; 0490 } 0491 0492 void AddPhotoCommand::cleanUpOnError(RajceSession&) 0493 { 0494 } 0495 0496 void AddPhotoCommand::parseResponse(QXmlQuery&, RajceSession&) 0497 { 0498 } 0499 0500 QString AddPhotoCommand::additionalXml() const 0501 { 0502 if (d->image.isNull()) 0503 { 0504 return QString(); 0505 } 0506 0507 QMap<QString, QString> metadata; 0508 QFileInfo f(d->imagePath); 0509 0510 metadata[QLatin1String("FullFilePath")] = d->imagePath; 0511 metadata[QLatin1String("OriginalFileName")] = f.fileName(); 0512 metadata[QLatin1String("OriginalFileExtension")] = QLatin1Char('.') + f.suffix(); 0513 metadata[QLatin1String("PerceivedType")] = QLatin1String("image"); //what are the other values here? video? 0514 metadata[QLatin1String("OriginalWidth")] = QString::number(d->image.width()); 0515 metadata[QLatin1String("OriginalHeight")] = QString::number(d->image.height()); 0516 metadata[QLatin1String("LengthMS")] = QLatin1Char('0'); 0517 metadata[QLatin1String("FileSize")] = QString::number(f.size()); 0518 0519 //TODO extract these from exif 0520 metadata[QLatin1String("Title")] = QLatin1String(""); 0521 metadata[QLatin1String("KeywordSet")] = QLatin1String(""); 0522 metadata[QLatin1String("PeopleRegionSet")] = QLatin1String(""); 0523 0524 QString id = QString::number(QRandomGenerator::global()->generate()); 0525 QString ret(QLatin1String(" <objectInfo>\n <Item id=\"")); 0526 ret.append(id).append(QLatin1String("\">\n")); 0527 0528 Q_FOREACH (const QString& key, metadata.keys()) 0529 { 0530 ret.append(QLatin1String(" <")).append(key); 0531 QString value = metadata[key]; 0532 0533 if (value.length() == 0) 0534 { 0535 ret.append(QLatin1String(" />\n")); 0536 } 0537 else 0538 { 0539 ret.append(QLatin1String(">")); 0540 ret.append(value); 0541 ret.append(QLatin1String("</")); 0542 ret.append(key); 0543 ret.append(QLatin1String(">\n")); 0544 } 0545 } 0546 0547 ret.append(QLatin1String(" </Item>\n </objectInfo>\n")); 0548 0549 return ret; 0550 } 0551 0552 QString AddPhotoCommand::contentType() const 0553 { 0554 return d->form->contentType(); 0555 } 0556 0557 QByteArray AddPhotoCommand::encode() const 0558 { 0559 if (d->image.isNull()) 0560 { 0561 qCDebug(DIGIKAM_WEBSERVICES_LOG) << d->imagePath << " could not be read, no data will be sent."; 0562 return QByteArray(); 0563 } 0564 0565 PreparedImage prepared = s_prepareImageForUpload(d->tmpDir, 0566 d->image, 0567 d->imagePath, 0568 d->desiredDimension, 0569 THUMB_SIZE, 0570 d->jpgQuality); 0571 0572 //add the rest of the parameters to be encoded as xml 0573 QImage scaled(prepared.scaledImagePath); 0574 parameters()[QLatin1String("width")] = QString::number(scaled.width()); 0575 parameters()[QLatin1String("height")] = QString::number(scaled.height()); 0576 QString xml = getXml(); 0577 0578 qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Really sending:\n" << xml; 0579 0580 //now we can create the form with all the info 0581 d->form->reset(); 0582 0583 d->form->addPair(QLatin1String("data"), xml); 0584 0585 d->form->addFile(QLatin1String("thumb"), prepared.thumbPath); 0586 d->form->addFile(QLatin1String("photo"), prepared.scaledImagePath); 0587 0588 QFile::remove(prepared.thumbPath); 0589 QFile::remove(prepared.scaledImagePath); 0590 0591 d->form->finish(); 0592 0593 QByteArray ret = d->form->formData(); 0594 0595 return ret; 0596 } 0597 0598 } // namespace DigikamGenericRajcePlugin 0599 0600 #include "moc_rajcecommand.cpp"