File indexing completed on 2024-04-14 14:35:55
0001 /************************************************************************ 0002 * * 0003 * This file is part of Kooka, a scanning/OCR application using * 0004 * Qt <http://www.qt.io> and KDE Frameworks <http://www.kde.org>. * 0005 * * 0006 * Copyright (C) 1999-2016 Klaas Freitag <freitag@suse.de> * 0007 * Jonathan Marten <jjm@keelhaul.me.uk> * 0008 * * 0009 * Kooka is free software; you can redistribute it and/or modify it * 0010 * under the terms of the GNU Library General Public License as * 0011 * published by the Free Software Foundation and appearing in the * 0012 * file COPYING included in the packaging of this file; either * 0013 * version 2 of the License, or (at your option) any later version. * 0014 * * 0015 * As a special exception, permission is given to link this program * 0016 * with any version of the KADMOS OCR/ICR engine (a product of * 0017 * reRecognition GmbH, Kreuzlingen), and distribute the resulting * 0018 * executable without including the source code for KADMOS in the * 0019 * source distribution. * 0020 * * 0021 * This program is distributed in the hope that it will be useful, * 0022 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0023 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0024 * GNU General Public License for more details. * 0025 * * 0026 * You should have received a copy of the GNU General Public * 0027 * License along with this program; see the file COPYING. If * 0028 * not, see <http://www.gnu.org/licenses/>. * 0029 * * 0030 ************************************************************************/ 0031 0032 #include "imgsaver.h" 0033 0034 #include <qdir.h> 0035 #include <qregexp.h> 0036 #include <qmimedatabase.h> 0037 #include <qtemporaryfile.h> 0038 0039 #include <kwidgetsaddons_version.h> 0040 #include <klocalizedstring.h> 0041 #include <kmessagebox.h> 0042 #include <kconfigskeleton.h> 0043 0044 #include <kio/statjob.h> 0045 #include <kio/mkdirjob.h> 0046 #include <kio/filecopyjob.h> 0047 #include <kio/udsentry.h> 0048 0049 #include "imageformat.h" 0050 #include "scanimage.h" 0051 0052 #include "galleryroot.h" 0053 #include "kookasettings.h" 0054 #include "formatdialog.h" 0055 #include "kooka_logging.h" 0056 0057 0058 #if 0 0059 // This may come in useful one day if we ever support remote galleries. 0060 static void createDir(const QUrl &url) 0061 { 0062 qCDebug(KOOKA_LOG) << url; 0063 KIO::StatJob *job = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatBasic); 0064 if (!job->exec()) 0065 { 0066 KMessageBox::error(nullptr, xi18nc("@info", "The directory <filename>%2</filename><nl/>could not be accessed.<nl/>%1", 0067 job->errorString(), url.url(QUrl::PreferLocalFile))); 0068 return; 0069 } 0070 0071 if (!job->statResult().isDir()) 0072 { 0073 qCDebug(KOOKA_LOG) << "directory does not exist, try to create"; 0074 KIO::MkdirJob *job = KIO::mkdir(url); 0075 if (!job->exec()) 0076 { 0077 KMessageBox::error(nullptr, xi18nc("@info", "The directory <filename>%2</filename><nl/>could not be created.<nl/>%1", 0078 job->errorString(), url.url(QUrl::PreferLocalFile))); 0079 return; 0080 } 0081 } 0082 } 0083 #endif 0084 0085 0086 ImgSaver::ImgSaver(const QUrl &dir) 0087 : mSaveUrl(QUrl()), 0088 mSaveFormat("") 0089 { 0090 if (dir.isValid() && !dir.isEmpty() && dir.isLocalFile()) { 0091 // can use specified place 0092 m_saveDirectory = dir; 0093 qCDebug(KOOKA_LOG) << "specified directory" << m_saveDirectory; 0094 } else { // cannot, so use default 0095 m_saveDirectory = GalleryRoot::root(); 0096 qCDebug(KOOKA_LOG) << "default directory" << m_saveDirectory; 0097 } 0098 } 0099 0100 0101 QString extension(const QUrl &url) 0102 { 0103 QMimeDatabase db; 0104 return (db.suffixForFileName(url.path())); 0105 } 0106 0107 0108 static KConfigSkeleton::ItemString *configItemForType(ScanImage::ImageType type) 0109 { 0110 switch (type) 0111 { 0112 case ScanImage::LowColour: return (KookaSettings::self()->formatLowColourItem()); 0113 case ScanImage::Greyscale: return (KookaSettings::self()->formatGreyscaleItem()); 0114 case ScanImage::BlackWhite: return (KookaSettings::self()->formatBlackWhiteItem()); 0115 case ScanImage::HighColour: return (KookaSettings::self()->formatHighColourItem()); 0116 default: return (KookaSettings::self()->formatUnknownItem()); 0117 } 0118 } 0119 0120 0121 static void storeFormatForType(ScanImage::ImageType type, const ImageFormat &format) 0122 { 0123 // We don't save OP_FILE_ASK_FORMAT here, this is the global setting 0124 // "Always use the Save Assistant" from the Kooka configuration which 0125 // is a preference option affecting all image types. To get automatic 0126 // saving in the preferred format, turn off that option in "Configure 0127 // Kooka - Image Saver" and select "Always use this format for this type 0128 // of file" when saving an image. As long as an image of that type has 0129 // scanned and saved, then the Save Assistant will not subsequently 0130 // appear for that image type. 0131 // 0132 // This means that turning on the "Always use the Save Assistant" option 0133 // will do exactly what it says. 0134 0135 KConfigSkeleton::ItemString *ski = configItemForType(type); 0136 Q_ASSERT(ski!=nullptr); 0137 ski->setValue(format.name()); 0138 KookaSettings::self()->save(); 0139 } 0140 0141 0142 static ImageFormat getFormatForType(ScanImage::ImageType type) 0143 { 0144 const KConfigSkeleton::ItemString *ski = configItemForType(type); 0145 Q_ASSERT(ski!=nullptr); 0146 return (ImageFormat(ski->value().toLocal8Bit())); 0147 } 0148 0149 0150 static QString findSubFormat(const ImageFormat &format) 0151 { 0152 return (QString()); // no subformats currently used 0153 } 0154 0155 0156 ImgSaver::ImageSaveStatus ImgSaver::getFilenameAndFormat(ScanImage::ImageType type) 0157 { 0158 if (type==ScanImage::None) return (ImgSaver::SaveStatusParam); 0159 0160 QString saveFilename = createFilename(); // find next unused filename 0161 ImageFormat saveFormat = getFormatForType(type); // find saved image format 0162 QString saveSubformat = findSubFormat(saveFormat); // currently not used 0163 // get dialogue preferences 0164 m_saveAskFilename = KookaSettings::saverAskForFilename(); 0165 m_saveAskFormat = KookaSettings::saverAskForFormat(); 0166 0167 qCDebug(KOOKA_LOG) << "before dialogue," 0168 << "type=" << type 0169 << "ask_filename=" << m_saveAskFilename 0170 << "ask_format=" << m_saveAskFormat 0171 << "filename=" << saveFilename 0172 << "format=" << saveFormat 0173 << "subformat=" << saveSubformat; 0174 0175 while (saveFilename.isEmpty() || !saveFormat.isValid() || m_saveAskFormat || m_saveAskFilename) 0176 { // is a dialogue needed? 0177 FormatDialog fd(nullptr, type, m_saveAskFormat, saveFormat, m_saveAskFilename, saveFilename); 0178 if (!fd.exec()) { 0179 return (ImgSaver::SaveStatusCanceled); 0180 } 0181 // do the dialogue 0182 saveFilename = fd.getFilename(); // get filename as entered 0183 if (fd.useAssistant()) { // redo with format options 0184 m_saveAskFormat = true; 0185 continue; 0186 } 0187 0188 saveFormat = fd.getFormat(); // get results from that 0189 saveSubformat = fd.getSubFormat(); 0190 0191 if (saveFormat.isValid()) { // have a valid format 0192 if (fd.alwaysUseFormat()) storeFormatForType(type, saveFormat); 0193 break; // save format for future 0194 } 0195 } 0196 0197 QUrl fi = m_saveDirectory.resolved(QUrl(saveFilename)); 0198 QString ext = saveFormat.extension(); 0199 if (extension(fi) != ext) // already has correct extension? 0200 { 0201 fi.setPath(fi.path()+'.'+ext); // no, add it on 0202 } 0203 0204 mSaveUrl = fi; 0205 mSaveFormat = saveFormat; 0206 mSaveSubformat = saveSubformat; 0207 0208 qCDebug(KOOKA_LOG) << "after dialogue," 0209 << "filename=" << saveFilename 0210 << "format=" << mSaveFormat 0211 << "subformat=" << mSaveSubformat 0212 << "url=" << mSaveUrl; 0213 return (ImgSaver::SaveStatusOk); 0214 } 0215 0216 0217 ImgSaver::ImageSaveStatus ImgSaver::setImageInfo(ScanImage::ImageType type) 0218 { 0219 return (getFilenameAndFormat(type)); 0220 } 0221 0222 0223 ImgSaver::ImageSaveStatus ImgSaver::saveImage(const ScanImage::Ptr image) 0224 { 0225 if (image==nullptr) return (ImgSaver::SaveStatusParam); 0226 0227 if (!mSaveFormat.isValid()) // see if have this already 0228 { 0229 // if not, get from image now 0230 ImgSaver::ImageSaveStatus stat = getFilenameAndFormat(image->imageType()); 0231 if (stat != ImgSaver::SaveStatusOk) return (stat); 0232 qCDebug(KOOKA_LOG) << "format from image" << mSaveFormat; 0233 } 0234 0235 if (!mSaveUrl.isValid() || !mSaveFormat.isValid()) // must have these now 0236 { 0237 qCWarning(KOOKA_LOG) << "format not resolved!"; 0238 return (ImgSaver::SaveStatusParam); 0239 } 0240 0241 // save image to file 0242 return (saveImage(image, mSaveUrl, mSaveFormat, mSaveSubformat)); 0243 } 0244 0245 0246 ImgSaver::ImageSaveStatus ImgSaver::saveImage(const ScanImage::Ptr image, 0247 const QUrl &url, 0248 const ImageFormat &format, 0249 const QString &subformat) 0250 { 0251 if (image == nullptr) return (ImgSaver::SaveStatusParam); 0252 0253 qCDebug(KOOKA_LOG) << "to" << url << "format" << format << "subformat" << subformat; 0254 0255 mLastFormat = format.name(); // save for error message later 0256 mLastUrl = url; 0257 0258 if (!url.isLocalFile()) // file must be local 0259 { 0260 qCDebug(KOOKA_LOG) << "Can only save local files"; 0261 // TODO: allow non-local files 0262 return (ImgSaver::SaveStatusProtocol); 0263 } 0264 0265 QString filename = url.path(); // local file path 0266 QFileInfo fi(filename); // information for that 0267 QString dirPath = fi.path(); // containing directory 0268 0269 QDir dir(dirPath); 0270 if (!dir.exists()) // should always exist, except 0271 { // for first preview save 0272 qCDebug(KOOKA_LOG) << "Creating directory" << dirPath; 0273 if (!dir.mkdir(dirPath)) 0274 { 0275 qCWarning(KOOKA_LOG) << "Could not create directory" << dirPath; 0276 return (ImgSaver::SaveStatusMkdir); 0277 } 0278 } 0279 0280 if (fi.exists() && !fi.isWritable()) 0281 { 0282 qCWarning(KOOKA_LOG) << "Cannot overwrite existing file" << filename; 0283 return (ImgSaver::SaveStatusPermission); 0284 } 0285 0286 if (!format.canWrite()) // check format, is it writable? 0287 { 0288 qCWarning(KOOKA_LOG) << "Cannot write format" << format; 0289 return (ImgSaver::SaveStatusFormatNoWrite); 0290 } 0291 0292 bool result = image->save(filename, format.name()); 0293 return (result ? ImgSaver::SaveStatusOk : ImgSaver::SaveStatusFailed); 0294 } 0295 0296 /** 0297 * Find the next filename to use for the image to save. 0298 * This is done by enumerating and checking against all existing files, 0299 * regardless of format - because we have not resolved the format yet. 0300 **/ 0301 QString ImgSaver::createFilename() 0302 { 0303 if (!m_saveDirectory.isLocalFile()) return (QString()); 0304 // TODO: allow non-local files 0305 QDir files(m_saveDirectory.path(), "kscan_[0-9][0-9][0-9][0-9].*"); 0306 QStringList l(files.entryList()); 0307 l.replaceInStrings(QRegExp("\\..*$"), ""); 0308 0309 QString fname; 0310 for (int c = 1; c <= l.count() + 1; ++c) { // that must be the upper bound 0311 fname = "kscan_" + QString::number(c).rightJustified(4, '0'); 0312 if (!l.contains(fname)) { 0313 break; 0314 } 0315 } 0316 0317 qCDebug(KOOKA_LOG) << "returning" << fname; 0318 return (fname); 0319 } 0320 0321 0322 QString ImgSaver::picTypeAsString(ScanImage::ImageType type) 0323 { 0324 QString res; 0325 0326 switch (type) { 0327 case ScanImage::LowColour: 0328 res = i18n("indexed color image (up to 8 bit depth)"); 0329 break; 0330 0331 case ScanImage::Greyscale: 0332 res = i18n("gray scale image (up to 8 bit depth)"); 0333 break; 0334 0335 case ScanImage::BlackWhite: 0336 res = i18n("lineart image (black and white, 1 bit depth)"); 0337 break; 0338 0339 case ScanImage::HighColour: 0340 res = i18n("high/true color image (more than 8 bit depth)"); 0341 break; 0342 0343 default: 0344 res = i18n("unknown image type %1", type); 0345 break; 0346 } 0347 0348 return (res); 0349 } 0350 0351 /* 0352 * This method returns true if the image format given in format is remembered 0353 * for that image type. 0354 */ 0355 bool ImgSaver::isRememberedFormat(ScanImage::ImageType type, const ImageFormat &format) 0356 { 0357 return (getFormatForType(type) == format); 0358 } 0359 0360 0361 QString ImgSaver::errorString(ImgSaver::ImageSaveStatus status) const 0362 { 0363 QString re; 0364 switch (status) { 0365 case ImgSaver::SaveStatusOk: 0366 re = i18n("Save OK"); break; 0367 case ImgSaver::SaveStatusPermission: 0368 re = i18n("Permission denied"); break; 0369 case ImgSaver::SaveStatusBadFilename: // never used 0370 re = i18n("Bad file name"); break; 0371 case ImgSaver::SaveStatusNoSpace: // never used 0372 re = i18n("No space left on device"); break; 0373 case ImgSaver::SaveStatusFormatNoWrite: 0374 re = i18n("Cannot write image format '%1'", mLastFormat.constData()); break; 0375 case ImgSaver::SaveStatusProtocol: 0376 re = i18n("Cannot write using protocol '%1'", mLastUrl.scheme()); break; 0377 case ImgSaver::SaveStatusCanceled: 0378 re = i18n("User cancelled saving"); break; 0379 case ImgSaver::SaveStatusMkdir: 0380 re = i18n("Cannot create directory"); break; 0381 case ImgSaver::SaveStatusFailed: 0382 re = i18n("Save failed"); break; 0383 case ImgSaver::SaveStatusParam: 0384 re = i18n("Bad parameter"); break; 0385 default: 0386 re = i18n("Unknown status %1", status); break; 0387 } 0388 return (re); 0389 } 0390 0391 0392 bool copyRenameImage(bool isCopying, const QUrl &fromUrl, const QUrl &toUrl, bool askExt, QWidget *overWidget) 0393 { 0394 QString errorString; 0395 0396 /* Check if the provided filename has a extension */ 0397 QString extFrom = extension(fromUrl); 0398 QString extTo = extension(toUrl); 0399 0400 QUrl targetUrl(toUrl); 0401 if (extTo.isEmpty() && !extFrom.isEmpty()) 0402 { // ask if the extension should be added 0403 int result = KMessageBox::Yes; 0404 QString fName = toUrl.fileName(); 0405 if (!fName.endsWith(".")) fName += "."; 0406 fName += extFrom; 0407 0408 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) 0409 if (askExt) result = KMessageBox::questionTwoActions(overWidget, 0410 #else 0411 if (askExt) result = KMessageBox::questionYesNo(overWidget, 0412 #endif 0413 xi18nc("@info", "The file name you supplied has no file extension.<nl/>" 0414 "Should the original one be added?<nl/><nl/>" 0415 "This would result in the new file name <filename>%1</filename>", fName), 0416 i18n("Extension Missing"), 0417 KGuiItem(i18n("Add Extension")), 0418 KGuiItem(i18n("Do Not Add")), 0419 "AutoAddExtensions"); 0420 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) 0421 if (result == KMessageBox::PrimaryAction) 0422 #else 0423 if (result == KMessageBox::Yes) 0424 #endif 0425 { 0426 targetUrl.setPath(targetUrl.adjusted(QUrl::RemoveFilename).path()+fName); 0427 } 0428 } 0429 else if (!extFrom.isEmpty() && extFrom != extTo) 0430 { 0431 QMimeDatabase db; 0432 const QMimeType fromType = db.mimeTypeForUrl(fromUrl); 0433 const QMimeType toType = db.mimeTypeForUrl(toUrl); 0434 if (toType.name() != fromType.name()) 0435 { 0436 errorString = "Changing the image format is not currently supported"; 0437 } 0438 } 0439 0440 if (errorString.isEmpty()) // no problem so far 0441 { 0442 qCDebug(KOOKA_LOG) << (isCopying ? "Copy" : "Rename") << "->" << targetUrl; 0443 0444 KJob *job = KIO::statDetails(targetUrl, KIO::StatJob::DestinationSide, KIO::StatNoDetails); 0445 if (job->exec()) // stat with minimal details 0446 { // to see if destination exists 0447 errorString = i18n("Target already exists"); 0448 } 0449 else 0450 { 0451 if (isCopying) job = KIO::file_copy(fromUrl, targetUrl); 0452 else job = KIO::file_move(fromUrl, targetUrl); 0453 // copy/rename the file 0454 if (!job->exec()) errorString = job->errorString(); 0455 } 0456 } 0457 0458 if (!errorString.isEmpty()) // file operation error 0459 { 0460 QString msg = (isCopying ? i18n("Unable to copy the file") : 0461 i18n("Unable to rename the file")); 0462 QString title = (isCopying ? i18n("Error copying file") : 0463 i18n("Error renaming file")); 0464 KMessageBox::error(overWidget, xi18nc("@info", "%1 <filename>%3</filename><nl/>%2", 0465 msg, errorString, 0466 fromUrl.url(QUrl::PreferLocalFile)), title); 0467 return (false); 0468 } 0469 0470 return (true); // file operation succeeded 0471 } 0472 0473 bool ImgSaver::renameImage(const QUrl &fromUrl, const QUrl &toUrl, bool askExt, QWidget *overWidget) 0474 { 0475 return (copyRenameImage(false, fromUrl, toUrl, askExt, overWidget)); 0476 } 0477 0478 bool ImgSaver::copyImage(const QUrl &fromUrl, const QUrl &toUrl, QWidget *overWidget) 0479 { 0480 return (copyRenameImage(true, fromUrl, toUrl, true, overWidget)); 0481 }