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 //---------------------------------------------------------------------