File indexing completed on 2024-05-12 04:34:56
0001 /* This file is part of Spectacle, the KDE screenshot utility 0002 * SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de> 0003 * SPDX-FileCopyrightText: 2015 Boudhayan Gupta <bgupta@kde.org> 0004 * SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "ExportManager.h" 0008 0009 #include "settings.h" 0010 #include <kio_version.h> 0011 0012 #include <QApplication> 0013 #include <QClipboard> 0014 #include <QDir> 0015 #include <QFileDialog> 0016 #include <QImageWriter> 0017 #include <QLocale> 0018 #include <QMimeData> 0019 #include <QMimeDatabase> 0020 #include <QPainter> 0021 #include <QPrinter> 0022 #include <QRandomGenerator> 0023 #include <QRegularExpression> 0024 #include <QRegularExpressionMatch> 0025 #include <QString> 0026 #include <QTemporaryDir> 0027 #include <QTemporaryFile> 0028 #include <QtConcurrent/QtConcurrentRun> 0029 0030 #include <KIO/DeleteJob> 0031 #include <KIO/FileCopyJob> 0032 #include <KIO/ListJob> 0033 #include <KIO/MkpathJob> 0034 #include <KIO/StatJob> 0035 #include <KRecentDocument> 0036 #include <KSharedConfig> 0037 #include <KSystemClipboard> 0038 #include <ZXing/ReadBarcode.h> 0039 0040 using namespace Qt::StringLiterals; 0041 0042 ExportManager::ExportManager(QObject *parent) 0043 : QObject(parent) 0044 , m_imageSavedNotInTemp(false) 0045 , m_saveImage(QImage()) 0046 , m_tempFile(QUrl()) 0047 { 0048 connect(this, &ExportManager::imageExported, [](Actions actions, const QUrl &url) { 0049 if (actions & AnySave) { 0050 Settings::setLastImageSaveLocation(url); 0051 if (actions & SaveAs) { 0052 Settings::setLastImageSaveAsLocation(url); 0053 } 0054 } 0055 }); 0056 } 0057 0058 ExportManager::~ExportManager() = default; 0059 0060 ExportManager *ExportManager::instance() 0061 { 0062 static ExportManager instance; 0063 // Ensure the SystemClipboard is instantiated early enough 0064 KSystemClipboard::instance(); 0065 return &instance; 0066 } 0067 0068 // screenshot image setter and getter 0069 0070 QImage ExportManager::image() const 0071 { 0072 return m_saveImage; 0073 } 0074 0075 void ExportManager::setWindowTitle(const QString &windowTitle) 0076 { 0077 m_windowTitle = windowTitle; 0078 } 0079 0080 QString ExportManager::windowTitle() const 0081 { 0082 return m_windowTitle; 0083 } 0084 0085 void ExportManager::setImage(const QImage &image) 0086 { 0087 m_saveImage = image; 0088 0089 // reset our saved tempfile 0090 if (m_tempFile.isValid()) { 0091 m_usedTempFileNames.append(m_tempFile); 0092 QFile file(m_tempFile.toLocalFile()); 0093 file.remove(); 0094 m_tempFile = QUrl(); 0095 } 0096 0097 // since the image was modified, we now consider the image unsaved 0098 m_imageSavedNotInTemp = false; 0099 0100 Q_EMIT imageChanged(); 0101 } 0102 0103 void ExportManager::updateTimestamp() 0104 { 0105 m_timestamp = QDateTime::currentDateTime(); 0106 } 0107 0108 void ExportManager::setTimestamp(const QDateTime ×tamp) 0109 { 0110 m_timestamp = timestamp; 0111 } 0112 0113 QDateTime ExportManager::timestamp() const 0114 { 0115 return m_timestamp; 0116 } 0117 0118 // native file save helpers 0119 0120 static QString ensureDefaultLocationExists(const QUrl &saveUrl) 0121 { 0122 QString savePath = saveUrl.isRelative() ? saveUrl.toString() : saveUrl.toLocalFile(); 0123 savePath = QDir::cleanPath(savePath); 0124 0125 QDir savePathDir(savePath); 0126 if (!(savePathDir.exists())) { 0127 savePathDir.mkpath(u"."_s); 0128 } 0129 return savePath; 0130 } 0131 0132 QString ExportManager::defaultSaveLocation() const 0133 { 0134 return ensureDefaultLocationExists(Settings::imageSaveLocation()); 0135 } 0136 0137 QString ExportManager::defaultVideoSaveLocation() const 0138 { 0139 return ensureDefaultLocationExists(Settings::videoSaveLocation()); 0140 } 0141 0142 QUrl ExportManager::getAutosaveFilename() const 0143 { 0144 const QString baseDir = defaultSaveLocation(); 0145 const QDir baseDirPath(baseDir); 0146 const QString filename = formattedFilename(); 0147 const QString fullpath = autoIncrementFilename(baseDirPath.filePath(filename), 0148 Settings::preferredImageFormat().toLower(), 0149 &ExportManager::isFileExists); 0150 0151 const QUrl fileNameUrl = QUrl::fromUserInput(fullpath); 0152 if (fileNameUrl.isValid()) { 0153 return fileNameUrl; 0154 } else { 0155 return QUrl(); 0156 } 0157 } 0158 0159 QUrl ExportManager::tempVideoUrl() 0160 { 0161 const auto format = static_cast<VideoPlatform::Format>(Settings::preferredVideoFormat()); 0162 auto extension = VideoPlatform::extensionForFormat(format); 0163 QString baseDir = defaultVideoSaveLocation(); 0164 const QDir baseDirPath(baseDir); 0165 const QString filename = formattedFilename(Settings::videoFilenameTemplate()); 0166 QString filepath = autoIncrementFilename(baseDirPath.filePath(filename), extension, &ExportManager::isFileExists); 0167 0168 auto tempDir = temporaryDir(); 0169 if (!tempDir) { 0170 return {}; 0171 } 0172 0173 if (!baseDir.isEmpty() && !baseDir.endsWith(u'/')) { 0174 baseDir += u'/'; 0175 } 0176 filepath = tempDir->path() + u'/' + filepath.right(filepath.size() - baseDir.size()); 0177 return QUrl::fromLocalFile(filepath); 0178 } 0179 0180 const QTemporaryDir *ExportManager::temporaryDir() 0181 { 0182 if (!m_tempDir) { 0183 // Cleanup Spectacle's temp dirs after startup instead of while quitting. 0184 const auto filters = QDir::Filter::Dirs | QDir::NoDotAndDotDot | QDir::CaseSensitive | QDir::NoSymLinks; 0185 const auto oldDirs = QDir::temp().entryList({u"Spectacle.??????"_s}, filters); 0186 QList<QUrl> oldUrls; 0187 for (const auto &dir : oldDirs) { 0188 oldUrls << QUrl::fromLocalFile(QDir::tempPath() + u'/' + dir); 0189 } 0190 KIO::del(oldUrls, KIO::HideProgressInfo)->start(); 0191 m_tempDir = std::make_unique<QTemporaryDir>(QDir::tempPath() + u"/Spectacle.XXXXXX"_s); 0192 m_tempDir->setAutoRemove(false); 0193 } 0194 return m_tempDir->isValid() ? m_tempDir.get() : nullptr; 0195 } 0196 0197 QString ExportManager::truncatedFilename(QString const &filename) const 0198 { 0199 QString result = filename; 0200 constexpr auto maxFilenameLength = 255; 0201 constexpr auto maxExtensionLength = 5; // For example, ".jpeg" 0202 constexpr auto maxCounterLength = 20; // std::numeric_limits<quint64>::max() == 18446744073709551615 0203 constexpr auto maxLength = maxFilenameLength - maxCounterLength - maxExtensionLength; 0204 result.truncate(maxLength); 0205 return result; 0206 } 0207 0208 QString ExportManager::formattedFilename(const QString &nameTemplate) const 0209 { 0210 const QDateTime timestamp = m_timestamp; 0211 QString baseName = nameTemplate; 0212 QString baseDir = defaultSaveLocation(); 0213 QString title = m_windowTitle; 0214 0215 if (!title.isEmpty()) { 0216 title.replace(u'/', u'_'); // POSIX doesn't allow "/" in filenames 0217 } else { 0218 // Remove <title> with separators around it 0219 const auto wordSymbol = uR"(\p{L}\p{M}\p{N})"_s; 0220 const auto separator = u"([^%1]+)"_s.arg(wordSymbol); 0221 const auto re = QRegularExpression(u"(.*?)(%1<title>|<title>%1)(.*?)"_s.arg(separator)); 0222 baseName.replace(re, uR"(\1\5)"_s); 0223 } 0224 0225 QString result = baseName; 0226 const auto &locale = QLocale::system(); 0227 // QDateTime 0228 for (auto it = filenamePlaceholders.cbegin(); it != filenamePlaceholders.cend(); ++it) { 0229 if (it->flags.testFlags(Placeholder::QDateTime)) { 0230 result.replace(it->plainKey, locale.toString(timestamp, it->baseKey)); 0231 } 0232 } 0233 // Manual interpretation 0234 // `h` and `hh` in QDateTime::toString or QLocale::toString 0235 // are only 12 hour when paired with an AM/PM display in the same toString call, 0236 // so we have to get the 12 hour format for `<h>` and `<hh>` manually. 0237 result.replace("<h>"_L1, timestamp.toString(u"hAP"_s).chopped(2)); 0238 result.replace("<hh>"_L1, timestamp.toString(u"hhAP"_s).chopped(2)); 0239 result.replace("<UnixTime>"_L1, QString::number(timestamp.toSecsSinceEpoch())); 0240 result.replace("<title>"_L1, title); 0241 0242 // check if basename includes %[N]d token for sequential file numbering 0243 QRegularExpression paddingRE; 0244 paddingRE.setPattern(u"<(#+)>"_s); 0245 QRegularExpressionMatchIterator it = paddingRE.globalMatch(result); 0246 if (it.hasNext()) { 0247 // strip any subdirectories from the template to construct the filename matching regex 0248 // we are matching filenames only, not paths 0249 QString resultCopy = QRegularExpression::escape(result.section(u'/', -1)); 0250 QList<QRegularExpressionMatch> matches; 0251 while (it.hasNext()) { 0252 QRegularExpressionMatch paddingMatch = it.next(); 0253 matches.push_back(paddingMatch); 0254 // determine padding value 0255 int paddedLength = 1; 0256 if (!paddingMatch.captured(1).isEmpty()) { 0257 paddedLength = paddingMatch.captured(1).length(); 0258 } 0259 QString escapedMatch = QRegularExpression::escape(paddingMatch.captured()); 0260 resultCopy.replace(escapedMatch, u"(\\d{%1,})"_s.arg(QString::number(paddedLength))); 0261 } 0262 if (result.contains(u'/')) { 0263 // In case the filename template contains a subdirectory, 0264 // we need to search for files in the subdirectory instead of the baseDir. 0265 // so let's add that to baseDir before we search for files. 0266 baseDir += u"/%1"_s.arg(result.section(u'/', 0, -2)); 0267 } 0268 // search save directory for files 0269 QDir dir(baseDir); 0270 const QStringList fileNames = dir.entryList(QDir::Files, QDir::Name); 0271 int highestFileNumber = 0; 0272 0273 // if there are files in the directory... 0274 if (fileNames.length() > 0) { 0275 QRegularExpression fileNumberRE; 0276 fileNumberRE.setPattern(resultCopy); 0277 // ... check the file names for string matching token with padding specified in result 0278 const QStringList filteredFiles = fileNames.filter(fileNumberRE); 0279 // if there are files in the directory that look like the file name with sequential numbering 0280 if (filteredFiles.length() > 0) { 0281 // loop through filtered file names looking for highest number 0282 for (const QString &filteredFile : filteredFiles) { 0283 int currentFileNumber = fileNumberRE.match(filteredFile).captured(1).length(); 0284 if (currentFileNumber > highestFileNumber) { 0285 highestFileNumber = currentFileNumber; 0286 } 0287 } 0288 } 0289 } 0290 // replace placeholder with next number padded 0291 for (const auto &match : matches) { 0292 int paddedLength = 1; 0293 if (!match.captured(1).isEmpty()) { 0294 paddedLength = match.captured(1).length(); 0295 } 0296 const QString nextFileNumberPadded = QString::number(highestFileNumber + 1).rightJustified(paddedLength, u'0'); 0297 result.replace(match.captured(), nextFileNumberPadded); 0298 } 0299 } 0300 0301 // Remove leading and trailing '/' 0302 while (result.startsWith(u'/')) { 0303 result.remove(0, 1); 0304 } 0305 while (result.endsWith(u'/')) { 0306 result.chop(1); 0307 } 0308 0309 if (result.isEmpty()) { 0310 result = u"Screenshot"_s; 0311 } 0312 return truncatedFilename(result); 0313 } 0314 0315 QString ExportManager::autoIncrementFilename(const QString &baseName, const QString &extension, FileNameAlreadyUsedCheck isFileNameUsed) const 0316 { 0317 QString result = truncatedFilename(baseName) + u'.' + extension; 0318 if (!((this->*isFileNameUsed)(QUrl::fromUserInput(result)))) { 0319 return result; 0320 } 0321 0322 QString fileNameFmt = truncatedFilename(baseName) + u"-%1."; 0323 for (quint64 i = 1; i < std::numeric_limits<quint64>::max(); i++) { 0324 result = fileNameFmt.arg(i) + extension; 0325 if (!((this->*isFileNameUsed)(QUrl::fromUserInput(result)))) { 0326 return result; 0327 } 0328 } 0329 0330 // unlikely this will ever happen, but just in case we've run 0331 // out of numbers 0332 0333 result = fileNameFmt.arg(u"OVERFLOW-" + QString::number(QRandomGenerator::global()->bounded(10000))); 0334 return truncatedFilename(result) + extension; 0335 } 0336 0337 QString ExportManager::imageFileSuffix(const QUrl &url) const 0338 { 0339 QMimeDatabase mimedb; 0340 const QString type = mimedb.mimeTypeForUrl(url).preferredSuffix(); 0341 0342 if (type.isEmpty()) { 0343 return Settings::self()->preferredImageFormat().toLower(); 0344 } 0345 return type; 0346 } 0347 0348 bool ExportManager::writeImage(QIODevice *device, const QByteArray &suffix) 0349 { 0350 // In the documentation for QImageWriter, it is a bit ambiguous what "format" means. 0351 // From looking at how QImageWriter handles the built-in supported formats internally, 0352 // "format" basically means the file extension, not the mimetype. 0353 QImageWriter imageWriter(device, suffix); 0354 imageWriter.setQuality(Settings::imageCompressionQuality()); 0355 /** Set compression 50 if the format is png. Otherwise if no compression value is specified 0356 * it will fallback to using quality (QTBUG-43618) and produce huge files. 0357 * See also qpnghandler.cpp#n1075. The other formats that do compression seem to have it 0358 * enabled by default and only disabled if compression is set to 0, also any value except 0 0359 * has the same effect for them. 0360 */ 0361 if (suffix == "png") { 0362 imageWriter.setCompression(50); 0363 } 0364 if (!(imageWriter.canWrite())) { 0365 Q_EMIT errorMessage(i18n("QImageWriter cannot write image: %1", imageWriter.errorString())); 0366 return false; 0367 } 0368 0369 return imageWriter.write(m_saveImage); 0370 } 0371 0372 bool ExportManager::localSave(const QUrl &url, const QString &suffix) 0373 { 0374 // Create save directory if it doesn't exist 0375 const QUrl dirPath(url.adjusted(QUrl::RemoveFilename)); 0376 const QDir dir(dirPath.path()); 0377 0378 if (!dir.mkpath(u"."_s)) { 0379 Q_EMIT errorMessage(xi18nc("@info", 0380 "Cannot save screenshot because creating " 0381 "the directory failed:<nl/><filename>%1</filename>", 0382 dirPath.path())); 0383 return false; 0384 } 0385 0386 QFile outputFile(url.toLocalFile()); 0387 0388 outputFile.open(QFile::WriteOnly); 0389 if (!writeImage(&outputFile, suffix.toLatin1())) { 0390 Q_EMIT errorMessage(i18n("Cannot save screenshot. Error while writing file.")); 0391 return false; 0392 } 0393 return true; 0394 } 0395 0396 bool ExportManager::remoteSave(const QUrl &url, const QString &suffix) 0397 { 0398 // Check if remote save directory exists 0399 const QUrl dirPath(url.adjusted(QUrl::RemoveFilename)); 0400 KIO::ListJob *listJob = KIO::listDir(dirPath); 0401 listJob->exec(); 0402 0403 if (listJob->error() != KJob::NoError) { 0404 // Create remote save directory 0405 KIO::MkpathJob *mkpathJob = KIO::mkpath(dirPath, QUrl(defaultSaveLocation())); 0406 mkpathJob->exec(); 0407 0408 if (mkpathJob->error() != KJob::NoError) { 0409 Q_EMIT errorMessage(xi18nc("@info", 0410 "Cannot save screenshot because creating the " 0411 "remote directory failed:<nl/><filename>%1</filename>", 0412 dirPath.path())); 0413 return false; 0414 } 0415 } 0416 0417 QTemporaryFile tmpFile; 0418 0419 if (tmpFile.open()) { 0420 if (!writeImage(&tmpFile, suffix.toLatin1())) { 0421 Q_EMIT errorMessage(i18n("Cannot save screenshot. Error while writing temporary local file.")); 0422 return false; 0423 } 0424 0425 KIO::FileCopyJob *uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmpFile.fileName()), url); 0426 uploadJob->exec(); 0427 0428 if (uploadJob->error() != KJob::NoError) { 0429 Q_EMIT errorMessage(i18n("Unable to save image. Could not upload file to remote location.")); 0430 return false; 0431 } 0432 return true; 0433 } 0434 0435 return false; 0436 } 0437 0438 QUrl ExportManager::tempSave() 0439 { 0440 // if we already have a temp file saved, use that 0441 if (m_tempFile.isValid()) { 0442 if (QFile(m_tempFile.toLocalFile()).exists()) { 0443 return m_tempFile; 0444 } 0445 } 0446 0447 auto tempDir = this->temporaryDir(); 0448 if (tempDir) { 0449 // create the temporary file itself with normal file name and also unique one for this session 0450 // supports the use-case of creating multiple screenshots in a row 0451 // and exporting them to the same destination e.g. via clipboard, 0452 // where the temp file name is used as filename suggestion 0453 const QString baseFileName = m_tempDir->path() + u'/' + QUrl::fromLocalFile(formattedFilename()).fileName(); 0454 0455 QString suffix = imageFileSuffix(QUrl(baseFileName)); 0456 const QString fileName = autoIncrementFilename(baseFileName, suffix, &ExportManager::isTempFileAlreadyUsed); 0457 QFile tmpFile(fileName); 0458 if (tmpFile.open(QFile::WriteOnly)) { 0459 if (writeImage(&tmpFile, suffix.toLatin1())) { 0460 m_tempFile = QUrl::fromLocalFile(tmpFile.fileName()); 0461 // try to make sure 3rd-party which gets the url of the temporary file e.g. on export 0462 // properly treats this as readonly, also hide from other users 0463 tmpFile.setPermissions(QFile::ReadUser); 0464 return m_tempFile; 0465 } 0466 } 0467 } 0468 0469 Q_EMIT errorMessage(i18n("Cannot save screenshot. Error while writing temporary local file.")); 0470 return QUrl(); 0471 } 0472 0473 bool ExportManager::save(const QUrl &url) 0474 { 0475 if (!(url.isValid())) { 0476 Q_EMIT errorMessage(i18n("Cannot save screenshot. The save filename is invalid.")); 0477 return false; 0478 } 0479 0480 const QString suffix = imageFileSuffix(url); 0481 bool saveSucceded = false; 0482 if (url.isLocalFile()) { 0483 saveSucceded = localSave(url, suffix); 0484 } else { 0485 saveSucceded = remoteSave(url, suffix); 0486 } 0487 if (saveSucceded) { 0488 m_imageSavedNotInTemp = true; 0489 KRecentDocument::add(url, QGuiApplication::desktopFileName()); 0490 } 0491 return saveSucceded; 0492 } 0493 0494 bool ExportManager::isFileExists(const QUrl &url) const 0495 { 0496 if (!(url.isValid())) { 0497 return false; 0498 } 0499 // Using StatJob instead of QFileInfo::exists() is necessary for checking non-local URLs. 0500 KIO::StatJob *existsJob = KIO::stat(url, KIO::StatJob::DestinationSide, KIO::StatNoDetails, KIO::HideProgressInfo); 0501 0502 existsJob->exec(); 0503 0504 return (existsJob->error() == KJob::NoError); 0505 } 0506 0507 bool ExportManager::isImageSavedNotInTemp() const 0508 { 0509 return m_imageSavedNotInTemp; 0510 } 0511 0512 bool ExportManager::isTempFileAlreadyUsed(const QUrl &url) const 0513 { 0514 return m_usedTempFileNames.contains(url); 0515 } 0516 0517 void ExportManager::exportImage(ExportManager::Actions actions, QUrl url) 0518 { 0519 if (m_saveImage.isNull() && actions & (Save | SaveAs | CopyImage)) { 0520 Q_EMIT errorMessage(i18n("Cannot save an empty screenshot image.")); 0521 return; 0522 } 0523 0524 bool success = false; 0525 if (actions & SaveAs) { 0526 QStringList supportedFilters; 0527 0528 // construct the supported mimetype list 0529 const auto mimeTypes = QImageWriter::supportedMimeTypes(); 0530 supportedFilters.reserve(mimeTypes.count()); 0531 for (const auto &mimeType : mimeTypes) { 0532 supportedFilters.append(QString::fromUtf8(mimeType).trimmed()); 0533 } 0534 0535 // construct the file name 0536 const QString filenameExtension = Settings::self()->preferredImageFormat().toLower(); 0537 const QString mimetype = QMimeDatabase().mimeTypeForFile(u"~/fakefile."_s + filenameExtension, QMimeDatabase::MatchExtension).name(); 0538 QFileDialog dialog; 0539 dialog.setAcceptMode(QFileDialog::AcceptSave); 0540 dialog.setFileMode(QFileDialog::AnyFile); 0541 QUrl dirUrl = url.adjusted(QUrl::RemoveFilename); 0542 if (!dirUrl.isValid()) { 0543 dirUrl = Settings::self()->lastImageSaveAsLocation().adjusted(QUrl::RemoveFilename); 0544 } 0545 dialog.setDirectoryUrl(dirUrl); 0546 dialog.selectFile(formattedFilename() + u"."_s + filenameExtension); 0547 dialog.setDefaultSuffix(u"."_s + filenameExtension); 0548 dialog.setMimeTypeFilters(supportedFilters); 0549 dialog.selectMimeTypeFilter(mimetype); 0550 0551 // launch the dialog 0552 const bool accepted = dialog.exec() == QDialog::Accepted; 0553 if (accepted && !dialog.selectedUrls().isEmpty()) { 0554 url = dialog.selectedUrls().constFirst(); 0555 } 0556 actions.setFlag(SaveAs, accepted && url.isValid()); 0557 } 0558 0559 bool saved = actions & AnySave; 0560 if (saved) { 0561 if (!url.isValid()) { 0562 url = getAutosaveFilename(); 0563 } 0564 saved = success = save(url); 0565 if (!success) { 0566 actions.setFlag(Save, false); 0567 actions.setFlag(SaveAs, false); 0568 } 0569 } 0570 0571 if (actions & CopyImage) { 0572 auto data = new QMimeData(); 0573 bool savedLocal = saved && url.isLocalFile(); 0574 bool canWriteTemp = temporaryDir() != nullptr; 0575 if (savedLocal || canWriteTemp) { 0576 QString localFilePath; 0577 if (savedLocal || (url.isEmpty() && canWriteTemp)) { 0578 if (url.isEmpty()) { 0579 url = tempSave(); 0580 } 0581 localFilePath = url.toLocalFile(); 0582 } else { 0583 localFilePath = tempSave().toLocalFile(); 0584 } 0585 QFile imageFile(localFilePath); 0586 imageFile.open(QFile::ReadOnly); 0587 const auto mimetype = QMimeDatabase().mimeTypeForFile(localFilePath); 0588 data->setData(mimetype.name(), imageFile.readAll()); 0589 imageFile.close(); 0590 } else { 0591 // Fallback to the old way if we can't save a temp file for some reason. 0592 data->setImageData(m_saveImage); 0593 } 0594 // "x-kde-force-image-copy" is handled by Klipper. 0595 // It ensures that the image is copied to Klipper even with the 0596 // "Non-text selection: Never save in history" setting selected in Klipper. 0597 data->setData(u"x-kde-force-image-copy"_s, QByteArray()); 0598 KSystemClipboard::instance()->setMimeData(data, QClipboard::Clipboard); 0599 success = true; 0600 } 0601 0602 if (actions & CopyPath 0603 // This behavior has no relation to the setting in the config UI, 0604 // but it was added to solve this feature request: 0605 // https://bugs.kde.org/show_bug.cgi?id=357423 0606 || (saved && Settings::clipboardGroup() == Settings::PostScreenshotCopyLocation)) { 0607 if (!url.isValid()) { 0608 if (m_imageSavedNotInTemp) { 0609 // The image has been saved (manually or automatically), 0610 // we need to choose that file path 0611 url = Settings::self()->lastImageSaveLocation(); 0612 } else { 0613 // use a temporary save path, and copy that to clipboard instead 0614 url = ExportManager::instance()->tempSave(); 0615 } 0616 } 0617 0618 // will be deleted for us by the platform's clipboard manager. 0619 auto data = new QMimeData(); 0620 data->setText(url.toLocalFile()); 0621 KSystemClipboard::instance()->setMimeData(data, QClipboard::Clipboard); 0622 success = true; 0623 } 0624 0625 if (success) { 0626 Q_EMIT imageExported(actions, url); 0627 } 0628 } 0629 0630 void ExportManager::scanQRCode() 0631 { 0632 auto scan = [this] { 0633 const auto image = ExportManager::instance()->image(); 0634 const auto zximage = ZXing::ImageView(image.constBits(), image.width(), image.height(), ZXing::ImageFormat::XRGB); 0635 const auto result = ZXing::ReadBarcode(zximage, {}); 0636 0637 if (result.isValid()) { 0638 QVariant content; 0639 if (result.contentType() == ZXing::ContentType::Text) { 0640 content = QString::fromStdString(result.text()); 0641 } else { 0642 QByteArray b; 0643 b.resize(result.bytes().size()); 0644 std::copy(result.bytes().begin(), result.bytes().end(), b.begin()); 0645 content = b; 0646 } 0647 Q_EMIT qrCodeScanned(content); 0648 } 0649 }; 0650 auto future = QtConcurrent::run(scan); 0651 } 0652 0653 void ExportManager::exportVideo(ExportManager::Actions actions, const QUrl &inputUrl, QUrl outputUrl) 0654 { 0655 // input can be empty or nonexistent, but not if we're saving 0656 const auto &inputFile = inputUrl.toLocalFile(); 0657 const auto &inputName = inputUrl.fileName(); 0658 if ((inputName.isEmpty() || !QFileInfo::exists(inputFile)) && actions & (Save | SaveAs)) { 0659 Q_EMIT errorMessage(i18nc("@info:shell","Failed to export video: Temporary file URL must be an existing local file")); 0660 return; 0661 } 0662 0663 // output can be empty, but not invalid or with an empty name when not empty and saving 0664 const auto &outputName = outputUrl.fileName(); 0665 if (!outputUrl.isEmpty() && (!outputUrl.isValid() || outputName.isEmpty()) && actions & (Save | SaveAs)) { 0666 Q_EMIT errorMessage(i18nc("@info:shell","Failed to export video: Output file URL must be a valid URL with a file name")); 0667 return; 0668 } 0669 0670 if (actions & SaveAs) { 0671 QMimeDatabase mimeDatabase; 0672 // construct the file name 0673 const auto &extension = inputName.mid(inputName.lastIndexOf(u'.')); 0674 const auto &mimetype = mimeDatabase.mimeTypeForFile(inputName, QMimeDatabase::MatchExtension).name(); 0675 QFileDialog dialog; 0676 dialog.setAcceptMode(QFileDialog::AcceptSave); 0677 dialog.setFileMode(QFileDialog::AnyFile); 0678 const auto outputDir = outputUrl.adjusted(QUrl::RemoveFilename); 0679 if (!outputDir.isValid()) { 0680 dialog.setDirectoryUrl(Settings::self()->lastVideoSaveAsLocation().adjusted(QUrl::RemoveFilename)); 0681 } else { 0682 dialog.setDirectoryUrl(outputUrl); 0683 } 0684 dialog.setDefaultSuffix(extension); 0685 dialog.selectFile(!outputName.isEmpty() ? outputName : inputName); 0686 dialog.setMimeTypeFilters({mimetype}); 0687 dialog.selectMimeTypeFilter(mimetype); 0688 0689 // launch the dialog 0690 const bool accepted = dialog.exec() == QDialog::Accepted; 0691 const auto &selectedUrl = dialog.selectedUrls().value(0, QUrl()); 0692 if (accepted && !selectedUrl.fileName().isEmpty()) { 0693 outputUrl = selectedUrl; 0694 } 0695 actions.setFlag(SaveAs, accepted && outputUrl.isValid()); 0696 } 0697 0698 bool inputFromTemp = temporaryDir() && inputFile.startsWith(m_tempDir->path()); 0699 if (!outputUrl.isValid()) { 0700 if (actions & AnySave && inputFromTemp) { 0701 // Use the temp url without the temp dir as the new url, if necessary 0702 const auto &tempDirPath = m_tempDir->path() + u'/'; 0703 const auto &reducedPath = inputFile.right(inputFile.size() - tempDirPath.size()); 0704 outputUrl = Settings::videoSaveLocation().adjusted(QUrl::StripTrailingSlash); 0705 outputUrl.setPath(outputUrl.path() + u'/' + reducedPath); 0706 } else { 0707 outputUrl = inputUrl; 0708 } 0709 } 0710 0711 // When the input is the output, it should still count as a successful save 0712 bool saved = inputUrl == outputUrl; 0713 if (actions & AnySave && !saved) { 0714 const auto &saveDirUrl = outputUrl.adjusted(QUrl::RemoveFilename); 0715 bool saveDirExists = false; 0716 if (saveDirUrl.isLocalFile()) { 0717 saveDirExists = QFileInfo::exists(saveDirUrl.toLocalFile()); 0718 } else { 0719 KIO::ListJob *listJob = KIO::listDir(saveDirUrl); 0720 listJob->exec(); 0721 saveDirExists = listJob->error() == KJob::NoError; 0722 } 0723 if (!saveDirExists) { 0724 KIO::MkpathJob *mkpathJob = KIO::mkpath(saveDirUrl); 0725 mkpathJob->exec(); 0726 saveDirExists = mkpathJob->error() == KJob::NoError; 0727 } 0728 if (saveDirExists) { 0729 if (inputFromTemp) { 0730 auto fileMoveJob = KIO::file_move(inputUrl, outputUrl); 0731 fileMoveJob->exec(); 0732 saved = fileMoveJob->error() == KJob::NoError; 0733 } else { 0734 auto fileCopyJob = KIO::file_copy(inputUrl, outputUrl); 0735 fileCopyJob->exec(); 0736 saved = fileCopyJob->error() == KJob::NoError; 0737 } 0738 } 0739 if (!saved) { 0740 actions.setFlag(AnySave, false); 0741 Q_EMIT errorMessage(i18nc("@info", "Unable to save recording. Could not move file to location: %1", outputUrl.toString())); 0742 return; 0743 } 0744 } 0745 0746 bool copiedPath = false; 0747 if (actions & CopyPath 0748 // This behavior has no relation to the setting in the config UI, 0749 // but it was added to solve this feature request: 0750 // https://bugs.kde.org/show_bug.cgi?id=357423 0751 || (saved && Settings::clipboardGroup() == Settings::PostScreenshotCopyLocation)) { 0752 // will be deleted for us by the platform's clipboard manager. 0753 auto data = new QMimeData(); 0754 data->setText(outputUrl.isLocalFile() ? outputUrl.toLocalFile() : outputUrl.toString()); 0755 KSystemClipboard::instance()->setMimeData(data, QClipboard::Clipboard); 0756 copiedPath = true; 0757 } 0758 0759 if (saved || copiedPath) { 0760 Q_EMIT videoExported(actions, outputUrl); 0761 } 0762 } 0763 0764 void ExportManager::doPrint(QPrinter *printer) 0765 { 0766 QPainter painter; 0767 0768 if (!(painter.begin(printer))) { 0769 Q_EMIT errorMessage(i18n("Printing failed. The printer failed to initialize.")); 0770 return; 0771 } 0772 0773 painter.setRenderHint(QPainter::LosslessImageRendering); 0774 0775 QRect devRect(0, 0, printer->width(), printer->height()); 0776 QImage image = m_saveImage.scaled(devRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); 0777 QRect srcRect = image.rect(); 0778 srcRect.moveCenter(devRect.center()); 0779 0780 painter.drawImage(srcRect.topLeft(), image); 0781 painter.end(); 0782 } 0783 0784 KLocalizedString placeholderDescription(const char *string) 0785 { 0786 return ki18nc("A placeholder in the user configurable filename will replaced by the specified value", string); 0787 } 0788 0789 using enum ExportManager::Placeholder::Flag; 0790 const QList<ExportManager::Placeholder> ExportManager::filenamePlaceholders{ 0791 {Date | Extra | QDateTime, u"d"_s, placeholderDescription("Day (1–31)")}, 0792 {Date | QDateTime, u"dd"_s, placeholderDescription("Day (01–31)")}, 0793 {Date | Extra | QDateTime, u"ddd"_s, placeholderDescription("Day (short name)")}, 0794 {Date | Extra | QDateTime, u"dddd"_s, placeholderDescription("Day (long name)")}, 0795 {Date | Extra | QDateTime, u"M"_s, placeholderDescription("Month (1–12)")}, 0796 {Date | QDateTime, u"MM"_s, placeholderDescription("Month (01–12)")}, 0797 {Date | QDateTime, u"MMM"_s, placeholderDescription("Month (short name)")}, 0798 {Date | QDateTime, u"MMMM"_s, placeholderDescription("Month (long name)")}, 0799 {Date | QDateTime, u"yy"_s, placeholderDescription("Year (2 digit)")}, 0800 {Date | QDateTime, u"yyyy"_s, placeholderDescription("Year (4 digit)")}, 0801 {Time | Extra | QDateTime, u"H"_s, placeholderDescription("Hour (0–23)")}, 0802 {Time | QDateTime, u"HH"_s, placeholderDescription("Hour (00–23)")}, 0803 {Time | Extra, u"h"_s, placeholderDescription("Hour (1–12)")}, 0804 {Time, u"hh"_s, placeholderDescription("Hour (01–12)")}, 0805 {Time | Extra | QDateTime, u"m"_s, placeholderDescription("Minute (0–59)")}, 0806 {Time | QDateTime, u"mm"_s, placeholderDescription("Minute (00–59)")}, 0807 {Time | Extra | QDateTime, u"s"_s, placeholderDescription("Second (0–59)")}, 0808 {Time | QDateTime, u"ss"_s, placeholderDescription("Second (00–59)")}, 0809 {Time | Extra | QDateTime, u"z"_s, placeholderDescription("Millisecond (0–999)")}, 0810 {Time | Extra | QDateTime, u"zz"_s}, // same as `z` 0811 {Time | Extra | QDateTime, u"zzz"_s, placeholderDescription("Millisecond (000–999)")}, 0812 {Time | Extra | QDateTime, u"AP"_s, placeholderDescription("AM/PM")}, 0813 {Time | Extra | QDateTime, u"A"_s}, // same as `AP` 0814 {Time | Extra | QDateTime, u"ap"_s, placeholderDescription("am/pm")}, 0815 {Time | Extra | QDateTime, u"a"_s}, // same as `ap` 0816 {Time | QDateTime, u"Ap"_s, placeholderDescription("AM/PM or am/pm (localized)")}, 0817 {Time | Extra | QDateTime, u"aP"_s}, // same as `Ap` 0818 {Time | Extra | QDateTime, u"t"_s, placeholderDescription("Timezone (short name)")}, 0819 {Time | QDateTime, u"tt"_s, placeholderDescription("Timezone offset (±0000)")}, 0820 {Time | Extra | QDateTime, u"ttt"_s, placeholderDescription("Timezone offset (±00:00)")}, 0821 {Time | Extra | QDateTime, u"tttt"_s, placeholderDescription("Timezone (long name)")}, 0822 {Time | Extra, u"UnixTime"_s, placeholderDescription("Seconds since the Unix epoch")}, 0823 {Other, u"title"_s, placeholderDescription("Window Title")}, 0824 {Other, u"#"_s, placeholderDescription("Sequential numbering, padded by inserting additional '#' characters")}, 0825 }; 0826 0827 #include "moc_ExportManager.cpp"