File indexing completed on 2024-04-28 04:20:13

0001 /*
0002    Copyright (c) 2003-2007 Clarence Dang <dang@kde.org>
0003    All rights reserved.
0004 
0005    Redistribution and use in source and binary forms, with or without
0006    modification, are permitted provided that the following conditions
0007    are met:
0008 
0009    1. Redistributions of source code must retain the above copyright
0010       notice, this list of conditions and the following disclaimer.
0011    2. Redistributions in binary form must reproduce the above copyright
0012       notice, this list of conditions and the following disclaimer in the
0013       documentation and/or other materials provided with the distribution.
0014 
0015    THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
0016    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
0017    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
0018    IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
0019    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
0020    NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
0021    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
0022    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
0023    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
0024    THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0025 */
0026 
0027 
0028 #define DEBUG_KP_DOCUMENT 0
0029 
0030 
0031 #include "kpDocument.h"
0032 #include "kpDocumentPrivate.h"
0033 
0034 
0035 #include <QFile>
0036 #include <QImage>
0037 #include <QSaveFile>
0038 #include <QTemporaryFile>
0039 #include <QMimeDatabase>
0040 
0041 #include "kpLogCategories.h"
0042 #include <KJobWidgets>
0043 #include <KIO/FileCopyJob>
0044 #include <KLocalizedString>
0045 #include <KMessageBox>
0046 
0047 #include "imagelib/kpColor.h"
0048 #include "widgets/toolbars/kpColorToolBar.h"
0049 #include "kpDefs.h"
0050 #include "environments/document/kpDocumentEnvironment.h"
0051 #include "document/kpDocumentSaveOptions.h"
0052 #include "imagelib/kpDocumentMetaInfo.h"
0053 #include "imagelib/effects/kpEffectReduceColors.h"
0054 #include "pixmapfx/kpPixmapFX.h"
0055 #include "tools/kpTool.h"
0056 #include "widgets/toolbars/kpToolToolBar.h"
0057 #include "lgpl/generic/kpUrlFormatter.h"
0058 #include "views/manager/kpViewManager.h"
0059 
0060 
0061 bool kpDocument::save (bool lossyPrompt)
0062 {
0063 #if DEBUG_KP_DOCUMENT
0064     qCDebug(kpLogDocument) << "kpDocument::save("
0065                << ",lossyPrompt=" << lossyPrompt
0066                << ") url=" << m_url
0067                << " savedAtLeastOnceBefore=" << savedAtLeastOnceBefore ();
0068 #endif
0069 
0070     // TODO: check feels weak
0071     if (m_url.isEmpty () || m_saveOptions->mimeType ().isEmpty ())
0072     {
0073         KMessageBox::detailedError (d->environ->dialogParent (),
0074             i18n ("Could not save image - insufficient information."),
0075             i18n ("URL: %1\n"
0076                   "Mimetype: %2",
0077                   prettyUrl (),
0078                   m_saveOptions->mimeType ().isEmpty () ?
0079                           i18n ("<empty>") :
0080                           m_saveOptions->mimeType ()),
0081             i18nc ("@title:window", "Internal Error"));
0082         return false;
0083     }
0084 
0085     return saveAs (m_url, *m_saveOptions,
0086                    lossyPrompt);
0087 }
0088 
0089 //---------------------------------------------------------------------
0090 
0091 // public static
0092 bool kpDocument::lossyPromptContinue (const QImage &pixmap,
0093                                       const kpDocumentSaveOptions &saveOptions,
0094                                       QWidget *parent)
0095 {
0096 #if DEBUG_KP_DOCUMENT
0097     qCDebug(kpLogDocument) << "kpDocument::lossyPromptContinue()";
0098 #endif
0099 
0100 #define QUIT_IF_CANCEL(messageBoxCommand)            \
0101 {                                                    \
0102     if (messageBoxCommand != KMessageBox::Continue)  \
0103     {                                                \
0104         return false;                                \
0105     }                                                \
0106 }
0107 
0108     const int lossyType = saveOptions.isLossyForSaving (pixmap);
0109     if (lossyType & (kpDocumentSaveOptions::MimeTypeMaximumColorDepthLow |
0110                      kpDocumentSaveOptions::Quality))
0111     {
0112         QMimeDatabase db;
0113 
0114         QUIT_IF_CANCEL (
0115             KMessageBox::warningContinueCancel (parent,
0116                 i18n ("<qt><p>The <b>%1</b> format may not be able"
0117                       " to preserve all of the image's color information.</p>"
0118 
0119                       "<p>Are you sure you want to save in this format?</p></qt>",
0120                       db.mimeTypeForName(saveOptions.mimeType()).comment()),
0121                 // TODO: caption misleading for lossless formats that have
0122                 //       low maximum colour depth
0123                 i18nc ("@title:window", "Lossy File Format"),
0124                 KStandardGuiItem::save (),
0125                 KStandardGuiItem::cancel(),
0126                 QLatin1String ("SaveInLossyMimeTypeDontAskAgain")));
0127     }
0128     else if (lossyType & kpDocumentSaveOptions::ColorDepthLow)
0129     {
0130         QUIT_IF_CANCEL (
0131             KMessageBox::warningContinueCancel (parent,
0132                 i18n ("<qt><p>Saving the image at the low color depth of %1-bit"
0133                         " may result in the loss of color information."
0134 
0135                         // TODO: It looks like 8-bit QImage's now support alpha.
0136                         //       Update kpDocumentSaveOptions::isLossyForSaving()
0137                         //       and change "might" to "will".
0138                         " Any transparency might also be removed.</p>"
0139 
0140                         "<p>Are you sure you want to save at this color depth?</p></qt>",
0141                       saveOptions.colorDepth ()),
0142                 i18nc ("@title:window", "Low Color Depth"),
0143                 KStandardGuiItem::save (),
0144                 KStandardGuiItem::cancel(),
0145                 QLatin1String ("SaveAtLowColorDepthDontAskAgain")));
0146     }
0147 #undef QUIT_IF_CANCEL
0148 
0149     return true;
0150 }
0151 
0152 //---------------------------------------------------------------------
0153 
0154 // public static
0155 bool kpDocument::savePixmapToDevice (const QImage &image,
0156                                      QIODevice *device,
0157                                      const kpDocumentSaveOptions &saveOptions,
0158                                      const kpDocumentMetaInfo &metaInfo,
0159                                      bool lossyPrompt,
0160                                      QWidget *parent,
0161                                      bool *userCancelled)
0162 {
0163     if (userCancelled)
0164         *userCancelled = false;
0165 
0166     QString type = QMimeDatabase().mimeTypeForName (saveOptions.mimeType ()).preferredSuffix ();
0167 #if DEBUG_KP_DOCUMENT
0168     qCDebug(kpLogDocument) << "\tmimeType=" << saveOptions.mimeType ()
0169                << " type=" << type;
0170 #endif
0171     if (type.isEmpty ())
0172         return false;
0173 
0174     if (lossyPrompt && !lossyPromptContinue (image, saveOptions, parent))
0175     {
0176         if (userCancelled)
0177             *userCancelled = true;
0178 
0179     #if DEBUG_KP_DOCUMENT
0180         qCDebug(kpLogDocument) << "\treturning false because of lossyPrompt";
0181     #endif
0182         return false;
0183     }
0184 
0185 
0186     // TODO: fix dup with kpDocumentSaveOptions::isLossyForSaving()
0187     const bool useSaveOptionsColorDepth =
0188         (saveOptions.mimeTypeHasConfigurableColorDepth () &&
0189          !saveOptions.colorDepthIsInvalid ());
0190 
0191     const bool useSaveOptionsQuality =
0192         (saveOptions.mimeTypeHasConfigurableQuality () &&
0193          !saveOptions.qualityIsInvalid ());
0194 
0195 
0196     //
0197     // Reduce colors if required
0198     //
0199 
0200 #if DEBUG_KP_DOCUMENT
0201     qCDebug(kpLogDocument) << "\tuseSaveOptionsColorDepth=" << useSaveOptionsColorDepth
0202               << "current image depth=" << image.depth ()
0203               << "save options depth=" << saveOptions.colorDepth ();
0204 #endif
0205     QImage imageToSave(image);
0206 
0207     if (useSaveOptionsColorDepth &&
0208         imageToSave.depth () != saveOptions.colorDepth ())
0209     {
0210         // TODO: I think this erases the mask!
0211         //
0212         //       I suspect this doesn't matter since this is only called to
0213         //       reduce color depth and QImage's with depth < 32 don't
0214         //       support masks anyway.
0215         //
0216         //       Later: I think the mask is preserved for 8-bit since Qt4
0217         //              seems to support it for QImage.
0218         imageToSave = kpEffectReduceColors::convertImageDepth (imageToSave,
0219                                            saveOptions.colorDepth (),
0220                                            saveOptions.dither ());
0221     }
0222 
0223 
0224     //
0225     // Write Meta Info
0226     //
0227 
0228     imageToSave.setDotsPerMeterX (metaInfo.dotsPerMeterX ());
0229     imageToSave.setDotsPerMeterY (metaInfo.dotsPerMeterY ());
0230     imageToSave.setOffset (metaInfo.offset ());
0231 
0232     const QStringList keys = metaInfo.textKeys();
0233     for (const QString &key : keys)
0234       imageToSave.setText(key, metaInfo.text(key));
0235 
0236     //
0237     // Save at required quality
0238     //
0239 
0240     int quality = -1;  // default
0241 
0242     if (useSaveOptionsQuality)
0243       quality = saveOptions.quality();
0244 
0245 #if DEBUG_KP_DOCUMENT
0246     qCDebug(kpLogDocument) << "\tsaving";
0247 #endif
0248     if (!imageToSave.save (device, type.toLatin1 ().constData(), quality))
0249     {
0250     #if DEBUG_KP_DOCUMENT
0251         qCDebug(kpLogDocument) << "\tQImage::save() returned false";
0252     #endif
0253         return false;
0254     }
0255 
0256 
0257 #if DEBUG_KP_DOCUMENT
0258     qCDebug(kpLogDocument) << "\tsave OK";
0259 #endif
0260     return true;
0261 }
0262 
0263 //---------------------------------------------------------------------
0264 
0265 static void CouldNotCreateTemporaryFileDialog (QWidget *parent)
0266 {
0267     KMessageBox::error (parent,
0268                         i18n ("Could not save image - unable to create temporary file."));
0269 }
0270 
0271 //---------------------------------------------------------------------
0272 
0273 static void CouldNotSaveDialog (const QUrl &url, const QString &error, QWidget *parent)
0274 {
0275     KMessageBox::error (parent,
0276                         i18n ("Could not save as \"%1\": %2",
0277                               kpUrlFormatter::PrettyFilename (url),
0278                               error));
0279 }
0280 
0281 //---------------------------------------------------------------------
0282 
0283 // public static
0284 bool kpDocument::savePixmapToFile (const QImage &pixmap,
0285                                    const QUrl &url,
0286                                    const kpDocumentSaveOptions &saveOptions,
0287                                    const kpDocumentMetaInfo &metaInfo,
0288                                    bool lossyPrompt,
0289                                    QWidget *parent)
0290 {
0291     // TODO: Use KIO::NetAccess:mostLocalURL() for accessing home:/ (and other
0292     //       such local URLs) for efficiency and because only local writes
0293     //       are atomic.
0294 #if DEBUG_KP_DOCUMENT
0295     qCDebug(kpLogDocument) << "kpDocument::savePixmapToFile ("
0296                << url
0297                << ",lossyPrompt=" << lossyPrompt
0298                << ")";
0299     saveOptions.printDebug (QLatin1String ("\tsaveOptions"));
0300     metaInfo.printDebug (QLatin1String ("\tmetaInfo"));
0301 #endif
0302 
0303     if (lossyPrompt && !lossyPromptContinue (pixmap, saveOptions, parent))
0304     {
0305     #if DEBUG_KP_DOCUMENT
0306         qCDebug(kpLogDocument) << "\treturning false because of lossyPrompt";
0307     #endif
0308         return false;
0309     }
0310 
0311 
0312     // Local file?
0313     if (url.isLocalFile ())
0314     {
0315         const QString filename = url.toLocalFile ();
0316 
0317         // sync: All failure exit paths _must_ call QSaveFile::cancelWriting() or
0318         //       else, the QSaveFile destructor will overwrite the file,
0319         //       <filename>, despite the failure.
0320         QSaveFile atomicFileWriter (filename);
0321         {
0322             if (!atomicFileWriter.open (QIODevice::WriteOnly))
0323             {
0324                 // We probably don't need this as <filename> has not been
0325                 // opened.
0326                 atomicFileWriter.cancelWriting ();
0327 
0328             #if DEBUG_KP_DOCUMENT
0329                 qCDebug(kpLogDocument) << "\treturning false because could not open QSaveFile"
0330                           << " error=" << atomicFileWriter.error () << endl;
0331             #endif
0332                 ::CouldNotCreateTemporaryFileDialog (parent);
0333                 return false;
0334             }
0335 
0336             // Write to local temporary file.
0337             if (!savePixmapToDevice (pixmap, &atomicFileWriter,
0338                                      saveOptions, metaInfo,
0339                                      false/*no lossy prompt*/,
0340                                      parent))
0341             {
0342                 atomicFileWriter.cancelWriting ();
0343 
0344             #if DEBUG_KP_DOCUMENT
0345                 qCDebug(kpLogDocument) << "\treturning false because could not save pixmap to device"
0346                           << endl;
0347             #endif
0348                 ::CouldNotSaveDialog (url, i18n("Error saving image"), parent);
0349                 return false;
0350             }
0351 
0352             // Atomically overwrite local file with the temporary file
0353             // we saved to.
0354             if (!atomicFileWriter.commit ())
0355             {
0356                 atomicFileWriter.cancelWriting ();
0357 
0358             #if DEBUG_KP_DOCUMENT
0359                 qCDebug(kpLogDocument) << "\tcould not close QSaveFile";
0360             #endif
0361                 ::CouldNotSaveDialog (url, atomicFileWriter.errorString(), parent);
0362                 return false;
0363             }
0364         }  // sync QSaveFile.cancelWriting()
0365     }
0366     // Remote file?
0367     else
0368     {
0369         // Create temporary file that is deleted when the variable goes
0370         // out of scope.
0371         QTemporaryFile tempFile;
0372         if (!tempFile.open ())
0373         {
0374         #if DEBUG_KP_DOCUMENT
0375             qCDebug(kpLogDocument) << "\treturning false because could not open tempFile";
0376         #endif
0377             ::CouldNotCreateTemporaryFileDialog (parent);
0378             return false;
0379         }
0380 
0381         // Write to local temporary file.
0382         if (!savePixmapToDevice (pixmap, &tempFile,
0383                                  saveOptions, metaInfo,
0384                                  false/*no lossy prompt*/,
0385                                  parent))
0386         {
0387         #if DEBUG_KP_DOCUMENT
0388             qCDebug(kpLogDocument) << "\treturning false because could not save pixmap to device"
0389                         << endl;
0390         #endif
0391             ::CouldNotSaveDialog (url, i18n("Error saving image"), parent);
0392             return false;
0393         }
0394 
0395         // Collect name of temporary file now, as QTemporaryFile::fileName()
0396         // stops working after close() is called.
0397         const QString tempFileName = tempFile.fileName ();
0398     #if DEBUG_KP_DOCUMENT
0399             qCDebug(kpLogDocument) << "\ttempFileName='" << tempFileName << "'";
0400     #endif
0401         Q_ASSERT (!tempFileName.isEmpty ());
0402 
0403         tempFile.close ();
0404         if (tempFile.error () != QFile::NoError)
0405         {
0406         #if DEBUG_KP_DOCUMENT
0407             qCDebug(kpLogDocument) << "\treturning false because could not close";
0408         #endif
0409             ::CouldNotSaveDialog (url, tempFile.errorString(), parent);
0410             return false;
0411         }
0412 
0413         // Copy local temporary file to overwrite remote.
0414         // It's the KIO worker's job to make this atomic (write to .part, then rename .part file)
0415         KIO::FileCopyJob *job = KIO::file_copy (QUrl::fromLocalFile (tempFileName),
0416                                                 url,
0417                                                 -1,
0418                                                 KIO::Overwrite);
0419         KJobWidgets::setWindow (job, parent);
0420         if (!job->exec ())
0421         {
0422         #if DEBUG_KP_DOCUMENT
0423             qCDebug(kpLogDocument) << "\treturning false because could not upload";
0424         #endif
0425             KMessageBox::error (parent,
0426                                 i18n ("Could not save image - failed to upload."));
0427             return false;
0428         }
0429     }
0430 
0431 
0432     return true;
0433 }
0434 
0435 //---------------------------------------------------------------------
0436 
0437 bool kpDocument::saveAs (const QUrl &url,
0438                          const kpDocumentSaveOptions &saveOptions,
0439                          bool lossyPrompt)
0440 {
0441 #if DEBUG_KP_DOCUMENT
0442     qCDebug(kpLogDocument) << "kpDocument::saveAs (" << url << ","
0443                << saveOptions.mimeType () << ")" << endl;
0444 #endif
0445 
0446     if (kpDocument::savePixmapToFile (imageWithSelection (),
0447                                       url,
0448                                       saveOptions, *metaInfo (),
0449                                       lossyPrompt,
0450                                       d->environ->dialogParent ()))
0451     {
0452         setURL (url, true/*is from url*/);
0453         *m_saveOptions = saveOptions;
0454         m_modified = false;
0455 
0456         m_savedAtLeastOnceBefore = true;
0457 
0458         Q_EMIT documentSaved ();
0459         return true;
0460     }
0461 
0462     return false;
0463 }
0464 
0465 //---------------------------------------------------------------------