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 }