1 /*******************************************************************************************************
2 DkProcess.cpp
3 Created on: 27.12.2014
4
5 nomacs is a fast and small image viewer with the capability of synchronizing multiple instances
6
7 Copyright (C) 2011-2014 Markus Diem <markus@nomacs.org>
8 Copyright (C) 2011-2014 Stefan Fiel <stefan@nomacs.org>
9 Copyright (C) 2011-2014 Florian Kleber <florian@nomacs.org>
10
11 This file is part of nomacs.
12
13 nomacs is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
17
18 nomacs is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
22
23 You should have received a copy of the GNU General Public License
24 along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 *******************************************************************************************************/
27
28 #include "DkProcess.h"
29 #include "DkUtils.h"
30 #include "DkImageContainer.h"
31 #include "DkImageStorage.h"
32 #include "DkPluginManager.h"
33 #include "DkSettings.h"
34 #include "DkMath.h"
35 #include "DkManipulators.h"
36 #include "DkTimer.h"
37
38 #include "DkMetaData.h"
39
40 #pragma warning(push, 0) // no warnings from includes - begin
41 #include <QFuture>
42 #include <QFutureWatcher>
43 #include <QtConcurrentMap>
44 #include <QWidget>
45 #pragma warning(pop) // no warnings from includes - end
46
47 #include <cassert>
48
49 namespace nmc {
50
51 /// <summary>
52 /// Generic compute method for DkBatch Info <see cref="DkBatchInfo"/>.
53 /// This method allows for a simplified interface if a derived class
54 /// just needs to process the image itself (not meta data).
55 /// </summary>
56 /// <param name="container">Container the image container to be processed.</param>
57 /// <param name="logStrings">log strings.</param>
58 /// <param name="batchInfo">The batch information. You can save any information into this class.</param>
59 /// <returns>
60 /// true on success
61 /// </returns>
compute(QSharedPointer<DkImageContainer> container,const DkSaveInfo &,QStringList & logStrings,QVector<QSharedPointer<DkBatchInfo>> &) const62 bool DkAbstractBatch::compute(QSharedPointer<DkImageContainer> container, const DkSaveInfo& , QStringList& logStrings, QVector<QSharedPointer<DkBatchInfo> >&) const {
63
64 return compute(container, logStrings);
65 }
66
67 /// <summary>
68 /// Generic compute method.
69 /// This method allows for a simplified interface if a derived class
70 /// just needs to process the image itself (not meta data).
71 /// </summary>
72 /// <param name="container">Container the image container to be processed.</param>
73 /// <param name="logStrings">log strings.</param>
74 /// <returns>true on success</returns>
compute(QSharedPointer<DkImageContainer> container,QStringList & logStrings) const75 bool DkAbstractBatch::compute(QSharedPointer<DkImageContainer> container, QStringList& logStrings) const {
76
77 QImage img = container->image();
78
79 bool isOk = compute(img, logStrings);
80
81 if (isOk)
82 container->setImage(img, QObject::tr("Batch Action"));
83
84 return isOk;
85 }
86
settingsName() const87 QString DkAbstractBatch::settingsName() const {
88
89 // make name() settings friendly
90 QString sn = name();
91 sn.replace("[", "");
92 sn.replace("]", "");
93 sn.replace(" ", "");
94
95 return sn;
96 }
97
createFromName(const QString & settingsName)98 QSharedPointer<DkAbstractBatch> DkAbstractBatch::createFromName(const QString& settingsName) {
99
100 QSharedPointer<DkAbstractBatch> batch;
101 batch = QSharedPointer<DkBatchTransform>::create();
102
103 if (batch->settingsName() == settingsName)
104 return batch;
105
106 batch = QSharedPointer<DkManipulatorBatch>::create();
107
108 if (batch->settingsName() == settingsName)
109 return batch;
110
111 #ifdef WITH_PLUGINS
112 batch = QSharedPointer<DkPluginBatch>::create();
113
114 if (batch->settingsName() == settingsName)
115 return batch;
116 #endif
117
118 qCritical() << "cannot instantiate batch, illegal settings name: " << settingsName;
119 return QSharedPointer<DkAbstractBatch>();
120 }
121
122 // DkTransformBatch --------------------------------------------------------------------
DkBatchTransform()123 DkBatchTransform::DkBatchTransform() {
124 mResizeIplMethod = DkImage::ipl_area; // define here because of includes
125 }
126
name() const127 QString DkBatchTransform::name() const {
128 return QObject::tr("[Transform Batch]");
129 }
130
setProperties(int angle,bool cropFromMetadata,QRect cropRect,float scaleFactor,const ResizeMode & mode,const ResizeProperty & prop,int iplMethod,bool correctGamma)131 void DkBatchTransform::setProperties(
132 int angle,
133 bool cropFromMetadata,
134 QRect cropRect,
135 float scaleFactor,
136 const ResizeMode& mode /*= resize_mode_default*/,
137 const ResizeProperty& prop /*= resize_prop_default*/,
138 int iplMethod /*= DkImage::ipl_area*/,
139 bool correctGamma /*= false*/) {
140
141 mAngle = angle;
142 mCropFromMetadata = cropFromMetadata;
143 mCropRect = cropRect;
144
145 mResizeScaleFactor = scaleFactor;
146 mResizeMode = mode;
147 mResizeProperty = prop;
148 mResizeIplMethod = iplMethod;
149 mResizeCorrectGamma = correctGamma;
150 }
151
saveSettings(QSettings & settings) const152 void DkBatchTransform::saveSettings(QSettings & settings) const {
153
154 settings.beginGroup(settingsName());
155 settings.setValue("Angle", mAngle);
156 settings.setValue("CropFromMetadata", mCropFromMetadata);
157 settings.setValue("CropRectangle", rectToString(mCropRect));
158
159 // resize
160 settings.setValue("ScaleFactor", mResizeScaleFactor);
161 settings.setValue("Mode", mResizeMode);
162 settings.setValue("Property", mResizeProperty);
163 settings.setValue("IplMethod", mResizeIplMethod);
164 settings.setValue("CorrectGamma", mResizeCorrectGamma);
165
166 settings.endGroup();
167 }
168
loadSettings(QSettings & settings)169 void DkBatchTransform::loadSettings(QSettings & settings) {
170
171 settings.beginGroup(settingsName());
172 mAngle = settings.value("Angle", mAngle).toInt();
173 mCropFromMetadata = settings.value("CropFromMetadata", mCropFromMetadata).toBool();
174 mCropRect = stringToRect(settings.value("CropRectangle", mCropRect).toString());
175
176 mResizeScaleFactor = settings.value("ScaleFactor", mResizeScaleFactor).toFloat();
177 mResizeMode = (ResizeMode)settings.value("Mode", mResizeMode).toInt();
178 mResizeProperty = (ResizeProperty)settings.value("Property", mResizeProperty).toInt();
179 mResizeIplMethod = settings.value("IplMethod", mResizeIplMethod).toInt();
180 mResizeCorrectGamma = settings.value("Correct Gamma", mResizeCorrectGamma).toBool();
181 settings.endGroup();
182 }
183
isResizeActive() const184 bool DkBatchTransform::isResizeActive() const {
185
186 if (mResizeMode != resize_mode_default)
187 return true;
188
189 if (mResizeScaleFactor != 1.0f)
190 return true;
191
192 return false;
193 }
194
rectToString(const QRect & r) const195 QString DkBatchTransform::rectToString(const QRect & r) const {
196
197 QString str;
198 str += QString::number(r.x()) + ",";
199 str += QString::number(r.y()) + ",";
200 str += QString::number(r.width()) + ",";
201 str += QString::number(r.height());
202
203 return str;
204 }
205
stringToRect(const QString & s) const206 QRect DkBatchTransform::stringToRect(const QString & s) const {
207
208 QStringList sl = s.split(",");
209
210 if (sl.size() != 4) {
211 qWarning() << "could not parse rect from" << s;
212 return QRect();
213 }
214
215 QRect rect;
216
217 bool okX, okY, okW, okH = false;
218 rect.setX(sl[0].toInt(&okX));
219 rect.setY(sl[1].toInt(&okY));
220 rect.setWidth(sl[2].toInt(&okW));
221 rect.setHeight(sl[3].toInt(&okH));
222
223 if (!okX || !okY || !okW || !okH) {
224 qWarning() << "could not parse rect from" << s;
225 return QRect();
226 }
227
228 return rect;
229 }
230
isActive() const231 bool DkBatchTransform::isActive() const {
232
233 return mAngle != 0 || mCropFromMetadata || cropFromRectangle() || isResizeActive();
234 }
235
angle() const236 int DkBatchTransform::angle() const {
237 return mAngle;
238 }
239
cropMetatdata() const240 bool DkBatchTransform::cropMetatdata() const {
241 return mCropFromMetadata;
242 }
243
cropFromRectangle() const244 bool DkBatchTransform::cropFromRectangle() const {
245 return !mCropRect.isEmpty();
246 }
247
cropRectangle() const248 QRect DkBatchTransform::cropRectangle() const {
249 return mCropRect;
250 }
251
mode() const252 DkBatchTransform::ResizeMode DkBatchTransform::mode() const {
253 return mResizeMode;
254 }
255
prop() const256 DkBatchTransform::ResizeProperty DkBatchTransform::prop() const {
257 return mResizeProperty;
258 }
259
iplMethod() const260 int DkBatchTransform::iplMethod() const {
261 return mResizeIplMethod;
262 }
263
scaleFactor() const264 float DkBatchTransform::scaleFactor() const {
265 return mResizeScaleFactor;
266 }
267
correctGamma() const268 bool DkBatchTransform::correctGamma() const {
269 return mResizeCorrectGamma;
270 }
271
compute(QSharedPointer<DkImageContainer> container,QStringList & logStrings) const272 bool DkBatchTransform::compute(QSharedPointer<DkImageContainer> container, QStringList& logStrings) const {
273
274
275 if (!isActive()) {
276 logStrings.append(QObject::tr("%1 inactive -> skipping").arg(name()));
277 return true;
278 }
279
280 DkRotatingRect rect = container->cropRect();
281 if (mCropFromMetadata) {
282 if (!rect.isEmpty())
283 container->cropImage(rect, QColor(), false);
284 }
285
286 QImage img = container->image();
287 QImage tmpImg;
288
289 // resize
290 if (isResizeActive()) {
291 QSize size;
292 float sf = 1.0f;
293
294 if (prepareProperties(img.size(), size, sf, logStrings))
295 tmpImg = DkImage::resizeImage(img, size, sf, mResizeIplMethod, mResizeCorrectGamma);
296 else
297 tmpImg = img;
298 }
299 else
300 tmpImg = img;
301
302 // rotate
303 if (mAngle != 0) {
304 QTransform rotationMatrix;
305 rotationMatrix.rotate((double)mAngle);
306 tmpImg = tmpImg.transformed(rotationMatrix);
307 }
308
309 // crop from rectangle
310 if (cropFromRectangle()) {
311 QRect r = mCropRect.intersected(container->image().rect());
312 tmpImg = tmpImg.copy(r);
313 }
314
315 // logs
316 if (!tmpImg.isNull()) {
317
318 container->setImage(tmpImg, QObject::tr("transformed"));
319
320 if (rect.isEmpty() && mCropFromMetadata)
321 logStrings.append(QObject::tr("%1 image transformed.").arg(name()));
322 else if (isResizeActive()) {
323 if (mResizeMode == resize_mode_default)
324 logStrings.append(QObject::tr("%1 image resized, scale factor: %2%").arg(name()).arg(mResizeScaleFactor*100.0f));
325 else
326 logStrings.append(QObject::tr("%1 image resized, new side: %2 px").arg(name()).arg(mResizeScaleFactor));
327 }
328 else
329 logStrings.append(QObject::tr("%1 image transformed and cropped.").arg(name()));
330
331 }
332 else {
333 logStrings.append(QObject::tr("%1 error, could not transform image.").arg(name()));
334 return false;
335 }
336
337 return true;
338 }
339
prepareProperties(const QSize & imgSize,QSize & size,float & scaleFactor,QStringList & logStrings) const340 bool DkBatchTransform::prepareProperties(const QSize& imgSize, QSize& size, float& scaleFactor, QStringList& logStrings) const {
341
342 float sf = 1.0f;
343 QSize normalizedSize = imgSize;
344
345 if (mResizeMode == resize_mode_default) {
346 scaleFactor = mResizeScaleFactor;
347 return true;
348 }
349 else if (mResizeMode == resize_mode_long_side) {
350
351 if (imgSize.width() < imgSize.height())
352 normalizedSize.transpose();
353 }
354 else if (mResizeMode == resize_mode_short_side) {
355
356 if (imgSize.width() > imgSize.height())
357 normalizedSize.transpose();
358 }
359 else if (mResizeMode == resize_mode_height)
360 normalizedSize.transpose();
361
362 sf = mResizeScaleFactor/normalizedSize.width();
363
364 if (sf > 1.0 && mResizeProperty == resize_prop_decrease_only) {
365
366 logStrings.append(QObject::tr("%1 I need to increase the image size, but the option is set to 'decrease only' -> skipping.").arg(name()));
367 return false;
368 }
369 else if (sf < 1.0f && mResizeProperty == resize_prop_increase_only) {
370 logStrings.append(QObject::tr("%1 I need to decrease the image size, but the option is set to 'increase only' -> skipping.").arg(name()));
371 return false;
372 }
373 else if (sf == 1.0f) {
374 logStrings.append(QObject::tr("%1 image size matches scale factor -> skipping.").arg(name()));
375 return false;
376 }
377
378 size.setWidth(qRound(mResizeScaleFactor));
379 size.setHeight(qRound(sf*normalizedSize.height()));
380
381 if (normalizedSize != imgSize)
382 size.transpose();
383
384 return true;
385 }
386
387
388
389 // DkManipulatorBatch --------------------------------------------------------------------
DkManipulatorBatch()390 DkManipulatorBatch::DkManipulatorBatch() {
391
392 }
393
saveSettings(QSettings & settings) const394 void DkManipulatorBatch::saveSettings(QSettings & settings) const {
395
396 settings.beginGroup(settingsName());
397 mManager.saveSettings(settings);
398 settings.endGroup();
399 }
400
loadSettings(QSettings & settings)401 void DkManipulatorBatch::loadSettings(QSettings & settings) {
402
403 settings.beginGroup(settingsName());
404 mManager.loadSettings(settings);
405 settings.endGroup();
406 }
407
setProperties(const DkManipulatorManager & manager)408 void DkManipulatorBatch::setProperties(const DkManipulatorManager& manager) {
409 mManager = manager;
410 }
411
compute(QSharedPointer<DkImageContainer> container,QStringList & logStrings) const412 bool DkManipulatorBatch::compute(QSharedPointer<DkImageContainer> container, QStringList & logStrings) const {
413
414 if (!isActive()) {
415 logStrings.append(QObject::tr("%1 inactive -> skipping").arg(name()));
416 return true;
417 }
418
419 if (container && container->hasImage()) {
420 for (const QSharedPointer<DkBaseManipulator>& mpl : mManager.manipulators()) {
421
422 if (mpl->isSelected()) {
423 QImage img = mpl->apply(container->image());
424 if (!img.isNull()) {
425 container->setImage(img, mpl->name());
426 logStrings.append(QObject::tr("%1 %2 applied.").arg(name()).arg(mpl->name()));
427 }
428 else
429 logStrings.append(QObject::tr("%1 Cannot apply %2.").arg(name()).arg(mpl->name()));
430 }
431 }
432 }
433
434 if (!container || !container->hasImage()) {
435 logStrings.append(QObject::tr("%1 error, could not apply image adjustments.").arg(name()));
436 return false;
437 }
438
439 return true;
440 }
441
name() const442 QString DkManipulatorBatch::name() const {
443 return QObject::tr("[Adjustment Batch]");
444 }
445
isActive() const446 bool DkManipulatorBatch::isActive() const {
447 return mManager.numSelected() > 0;
448 }
449
manager() const450 DkManipulatorManager DkManipulatorBatch::manager() const {
451 return mManager;
452 }
453
454 #ifdef WITH_PLUGINS
455 // DkPluginBatch --------------------------------------------------------------------
DkPluginBatch()456 DkPluginBatch::DkPluginBatch() {
457 }
458
setProperties(const QStringList & pluginList)459 void DkPluginBatch::setProperties(const QStringList & pluginList) {
460 mPluginList = pluginList;
461 }
462
saveSettings(QSettings & settings) const463 void DkPluginBatch::saveSettings(QSettings & settings) const {
464
465 settings.beginGroup(settingsName());
466 settings.setValue("pluginList", mPluginList.join(";"));
467
468 for (const QSharedPointer<DkPluginContainer> plugin : mPlugins) {
469
470 if (!plugin)
471 continue;
472
473 DkBatchPluginInterface* bPlugin = plugin->batchPlugin();
474
475 if (bPlugin) {
476 bPlugin->saveSettings(settings);
477 }
478 else
479 qWarning() << "Illegal plugin detected: " << plugin->pluginName();
480 }
481
482 settings.endGroup();
483 }
484
loadSettings(QSettings & settings)485 void DkPluginBatch::loadSettings(QSettings & settings) {
486
487 settings.beginGroup(settingsName());
488 mPluginList = settings.value("pluginList", mPluginList).toString().split(";");
489
490 loadAllPlugins();
491
492 for (QSharedPointer<DkPluginContainer> plugin : mPlugins) {
493
494 if (!plugin)
495 continue;
496
497 DkBatchPluginInterface* bPlugin = plugin->batchPlugin();
498
499 if (bPlugin) {
500 assert(bPlugin);
501 bPlugin->loadSettings(settings);
502 }
503 }
504
505 settings.endGroup();
506 }
507
508
preLoad()509 void DkPluginBatch::preLoad() {
510
511 loadAllPlugins();
512 }
513
postLoad(const QVector<QSharedPointer<DkBatchInfo>> & batchInfo) const514 void DkPluginBatch::postLoad(const QVector<QSharedPointer<DkBatchInfo> >& batchInfo) const {
515
516 for (int idx = 0; idx < mPlugins.size(); idx++) {
517
518 QSharedPointer<DkPluginContainer> pluginContainer = mPlugins[idx];
519 QString runID = mRunIDs[idx];
520
521 if (pluginContainer) {
522
523 // get plugin
524 DkBatchPluginInterface* plugin = pluginContainer->batchPlugin();
525
526 qDebug() << "[POST LOAD]" << pluginContainer->pluginName() << "id:" << runID;
527 QVector<QSharedPointer<DkBatchInfo> > fInfos = DkBatchInfo::filter(batchInfo, runID);
528
529 // check if it is ok
530 if (plugin) {
531 plugin->postLoadPlugin(fInfos);
532 }
533 }
534 }
535 }
536
compute(QSharedPointer<DkImageContainer> container,const DkSaveInfo & saveInfo,QStringList & logStrings,QVector<QSharedPointer<DkBatchInfo>> & batchInfos) const537 bool DkPluginBatch::compute(
538 QSharedPointer<DkImageContainer> container,
539 const DkSaveInfo& saveInfo,
540 QStringList& logStrings,
541 QVector<QSharedPointer<DkBatchInfo> >& batchInfos) const {
542
543 if (!isActive()) {
544 logStrings.append(QObject::tr("%1 inactive -> skipping").arg(name()));
545 return true;
546 }
547
548 for (int idx = 0; idx < mPlugins.size(); idx++) {
549
550 QSharedPointer<DkPluginContainer> pluginContainer = mPlugins[idx];
551 QString runID = mRunIDs[idx];
552
553 if (pluginContainer) {
554 // get plugin
555 DkPluginInterface* plugin = pluginContainer->plugin();
556
557 // check if it is ok
558 if ( plugin &&
559 (plugin->interfaceType() == DkPluginInterface::interface_basic ||
560 plugin->interfaceType() == DkPluginInterface::interface_batch)) {
561
562 // apply the plugin
563 QSharedPointer<DkImageContainer> result;
564
565 if (plugin->interfaceType() == DkPluginInterface::interface_basic)
566 result = plugin->runPlugin(runID, container);
567 else if (plugin->interfaceType() == DkPluginInterface::interface_batch) {
568
569 DkBatchPluginInterface* bPlugin = pluginContainer->batchPlugin();
570 QSharedPointer<DkBatchInfo> info;
571
572 if (bPlugin)
573 result = bPlugin->runPlugin(runID, container, saveInfo, info);
574 else
575 logStrings.append(QObject::tr("%1 Cannot cast batch plugin %2.").arg(name()).arg(pluginContainer->pluginName()));
576
577 batchInfos << info;
578 }
579
580 if (result && result->hasImage())
581 container = result;
582 else
583 logStrings.append(QObject::tr("%1 Cannot apply %2.").arg(name()).arg(pluginContainer->pluginName()));
584 }
585 else
586 logStrings.append(QObject::tr("%1 illegal plugin interface: %2").arg(name()).arg(pluginContainer->pluginName()));
587 }
588 else
589 logStrings.append(QObject::tr("%1 Cannot apply plugin because it is NULL.").arg(name()));
590 }
591
592 if (!container || !container->hasImage()) {
593 logStrings.append(QObject::tr("%1 error, could not apply plugins.").arg(name()));
594 return false;
595 }
596 else
597 logStrings.append(QObject::tr("%1 plugins applied.").arg(name()));
598
599 return true;
600 }
601
name() const602 QString DkPluginBatch::name() const {
603 return QObject::tr("[Plugin Batch]");
604 }
605
isActive() const606 bool DkPluginBatch::isActive() const {
607
608 return !mPluginList.empty();
609 }
610
pluginList() const611 QStringList DkPluginBatch::pluginList() const {
612 return mPluginList;
613 }
614
loadAllPlugins()615 void DkPluginBatch::loadAllPlugins() {
616
617 // already loaded?
618 if (mPlugins.size() == mPluginList.size())
619 return;
620
621 // manager cares that they are not loaded twice
622 // we need to load them here for CMD inputs
623 DkPluginManager::instance().loadPlugins();
624
625 QString runId;
626
627 for (const QString& cPluginString : mPluginList) {
628
629 // load plugin
630 QSharedPointer<DkPluginContainer> pluginContainer;
631 QString runID;
632 loadPlugin(cPluginString, pluginContainer, runID);
633 mPlugins << pluginContainer; // also add the empty ones...
634 mRunIDs << runID;
635
636 if (pluginContainer) {
637
638 qDebug() << "loading" << pluginContainer->pluginName() << "id:" << runID;
639
640 // get plugin
641 DkBatchPluginInterface* plugin = pluginContainer->batchPlugin();
642
643 // check if it is ok
644 if (plugin) {
645 plugin->preLoadPlugin();
646 }
647 }
648 else
649 qWarning() << "could not load: " << cPluginString;
650 }
651 }
652
loadPlugin(const QString & pluginString,QSharedPointer<DkPluginContainer> & plugin,QString & runID) const653 void DkPluginBatch::loadPlugin(const QString & pluginString, QSharedPointer<DkPluginContainer> & plugin, QString& runID) const {
654
655 QString uiSeparator = " | ";
656
657 QStringList ids = pluginString.split(uiSeparator);
658
659 if (ids.size() != 2) {
660 qWarning() << "plugin string does not match:" << pluginString;
661 }
662 else {
663 plugin = DkPluginManager::instance().getPluginByName(ids[0]);
664
665 if (plugin)
666 runID = plugin->actionNameToRunId(ids[1]);
667 }
668 }
669 #endif
670
671 // DkBatchProcess --------------------------------------------------------------------
DkBatchProcess(const DkSaveInfo & saveInfo)672 DkBatchProcess::DkBatchProcess(const DkSaveInfo& saveInfo) {
673 mSaveInfo = saveInfo;
674 }
675
setProcessChain(const QVector<QSharedPointer<DkAbstractBatch>> processes)676 void DkBatchProcess::setProcessChain(const QVector<QSharedPointer<DkAbstractBatch> > processes) {
677
678 mProcessFunctions = processes;
679 }
680
inputFile() const681 QString DkBatchProcess::inputFile() const {
682
683 return mSaveInfo.inputFilePath();
684 }
685
outputFile() const686 QString DkBatchProcess::outputFile() const {
687
688 return mSaveInfo.outputFilePath();
689 }
690
batchInfo() const691 QVector<QSharedPointer<DkBatchInfo> > DkBatchProcess::batchInfo() const {
692
693 return mInfos;
694 }
695
hasFailed() const696 bool DkBatchProcess::hasFailed() const {
697
698 return mFailure != 0;
699 }
700
wasProcessed() const701 bool DkBatchProcess::wasProcessed() const {
702
703 return mIsProcessed;
704 }
705
compute()706 bool DkBatchProcess::compute() {
707
708 mIsProcessed = true;
709
710 QFileInfo fInfoIn(mSaveInfo.inputFilePath());
711 QFileInfo fInfoOut(mSaveInfo.outputFilePath());
712
713 // check errors
714 if ((mSaveInfo.mode() & DkSaveInfo::mode_do_not_save_output) == 0 && // do not save is not set
715 (fInfoOut.exists() && mSaveInfo.mode() == DkSaveInfo::mode_skip_existing)) {
716 mLogStrings.append(QObject::tr("%1 already exists -> skipping (check 'overwrite' if you want to overwrite the file)").arg(mSaveInfo.outputFilePath()));
717 mFailure++;
718 return mFailure == 0;
719 }
720 else if (!fInfoIn.exists()) {
721 mLogStrings.append(QObject::tr("Error: input file does not exist"));
722 mLogStrings.append(QObject::tr("Input: %1").arg(mSaveInfo.inputFilePath()));
723 mFailure++;
724 return mFailure == 0;
725 }
726 else if (mSaveInfo.inputFilePath() == mSaveInfo.outputFilePath() && mProcessFunctions.empty()) {
727 mLogStrings.append(QObject::tr("Skipping: nothing to do here."));
728 mFailure++;
729 return mFailure == 0;
730 }
731
732 // rename operation?
733 if (mProcessFunctions.empty() &&
734 mSaveInfo.inputFilePath() == mSaveInfo.outputFilePath() &&
735 fInfoIn.suffix() == fInfoOut.suffix()) {
736 if (!renameFile())
737 mFailure++;
738 return mFailure == 0;
739 }
740 // copy operation?
741 else if (mProcessFunctions.empty() && fInfoIn.suffix() == fInfoOut.suffix()) {
742 if (!copyFile())
743 mFailure++;
744 else
745 deleteOriginalFile();
746
747 return mFailure == 0;
748 }
749
750 // do the work
751 process();
752
753 // delete the original file if the user requested it
754 deleteOriginalFile();
755
756 return mFailure == 0;
757 }
758
getLog() const759 QStringList DkBatchProcess::getLog() const {
760
761 return mLogStrings;
762 }
763
process()764 bool DkBatchProcess::process() {
765
766 mLogStrings.append(QObject::tr("processing %1").arg(mSaveInfo.inputFilePath()));
767
768 QSharedPointer<DkImageContainer> imgC(new DkImageContainer(mSaveInfo.inputFilePath()));
769
770 if (!imgC->loadImage() || imgC->image().isNull()) {
771 mLogStrings.append(QObject::tr("Error while loading..."));
772 mFailure++;
773 return false;
774 }
775
776 for (QSharedPointer<DkAbstractBatch> batch : mProcessFunctions) {
777
778 if (!batch) {
779 mLogStrings.append(QObject::tr("Error: cannot process a NULL function."));
780 continue;
781 }
782
783 QVector<QSharedPointer<DkBatchInfo> > cInfos;
784 if (!batch->compute(imgC, mSaveInfo, mLogStrings, cInfos)) {
785 mLogStrings.append(QObject::tr("%1 failed").arg(batch->name()));
786 mFailure++;
787 }
788
789 mInfos << cInfos;
790 }
791
792 // report we could not back-up & break here
793 if (!prepareDeleteExisting()) {
794 mFailure++;
795 return false;
796 }
797
798 // early break
799 if (mSaveInfo.mode() & DkSaveInfo::mode_do_not_save_output) {
800 mLogStrings.append(QObject::tr("%1 not saved - option 'Do not Save' is checked...").arg(mSaveInfo.outputFilePath()));
801 return true;
802 }
803
804 // udpate metadata
805 if (updateMetaData(imgC->getMetaData().data()))
806 mLogStrings.append(QObject::tr("Original filename added to Exif"));
807
808 // save the image
809 if (imgC->saveImage(mSaveInfo.outputFilePath(), mSaveInfo.compression())) {
810 mLogStrings.append(QObject::tr("%1 saved...").arg(mSaveInfo.outputFilePath()));
811 }
812 else {
813 mLogStrings.append(QObject::tr("Could not save: %1").arg(mSaveInfo.outputFilePath()));
814 mFailure++;
815 }
816
817 if (!deleteOrRestoreExisting()) {
818 mFailure++;
819 return false;
820 }
821
822 return true;
823 }
824
renameFile()825 bool DkBatchProcess::renameFile() {
826
827 if (QFileInfo(mSaveInfo.outputFilePath()).exists()) {
828 mLogStrings.append(QObject::tr("Error: could not rename file, the target file exists already."));
829 return false;
830 }
831
832 QFile file(mSaveInfo.inputFilePath());
833
834 QSharedPointer<DkMetaDataT> md(new DkMetaDataT());
835 md->readMetaData(mSaveInfo.inputFilePath());
836
837 if (updateMetaData(md.data())) {
838 if (md->saveMetaData(mSaveInfo.inputFilePath()))
839 mLogStrings.append(QObject::tr("Original filename added to Exif"));
840 }
841
842 // Note: if two images are renamed at the same time to the same name, one image is lost -> see Qt comment Race Condition
843 if (!file.rename(mSaveInfo.outputFilePath())) {
844 mLogStrings.append(QObject::tr("Error: could not rename file"));
845 mLogStrings.append(file.errorString());
846 return false;
847 }
848 else
849 mLogStrings.append(QObject::tr("Renaming: %1 -> %2").arg(mSaveInfo.inputFilePath()).arg(mSaveInfo.outputFilePath()));
850
851 return true;
852 }
853
updateMetaData(DkMetaDataT * md)854 bool DkBatchProcess::updateMetaData(DkMetaDataT * md) {
855
856 if (!md)
857 return false;
858
859 if (!md->isLoaded())
860 return false;
861
862 QString ifn = mSaveInfo.inputFileInfo().fileName();
863 if (ifn != mSaveInfo.outputFileInfo().fileName() &&
864 md->getExifValue("ImageDescription").isEmpty()) {
865
866 if (md->setExifValue("Exif.Image.ImageDescription", ifn))
867 return true;
868 }
869
870 return false;
871 }
872
copyFile()873 bool DkBatchProcess::copyFile() {
874
875 QFile file(mSaveInfo.inputFilePath());
876
877 if (mSaveInfo.mode() == DkSaveInfo::mode_do_not_save_output) {
878 mLogStrings.append(QObject::tr("I should copy the file, but 'Do not Save' is checked - so I will do nothing..."));
879 return false;
880 }
881
882 // report we could not back-up & break here
883 if (!prepareDeleteExisting()) {
884 mFailure++;
885 return false;
886 }
887
888 QSharedPointer<DkMetaDataT> md(new DkMetaDataT());
889 md->readMetaData(mSaveInfo.inputFilePath());
890
891 bool exifUpdated = updateMetaData(md.data());
892
893 if (!file.copy(mSaveInfo.outputFilePath())) {
894 mLogStrings.append(QObject::tr("Error: could not copy file"));
895 mLogStrings.append(QObject::tr("Input: %1").arg(mSaveInfo.inputFilePath()));
896 mLogStrings.append(QObject::tr("Output: %1").arg(mSaveInfo.outputFilePath()));
897 mLogStrings.append(file.errorString());
898 return false;
899 }
900 else {
901 if (exifUpdated && md->saveMetaData(mSaveInfo.outputFilePath()))
902 mLogStrings.append(QObject::tr("Original filename added to Exif"));
903
904 mLogStrings.append(QObject::tr("Copying: %1 -> %2").arg(mSaveInfo.inputFilePath()).arg(mSaveInfo.outputFilePath()));
905 }
906
907 if (!deleteOrRestoreExisting()) {
908 mFailure++;
909 return false;
910 }
911
912 return true;
913 }
914
prepareDeleteExisting()915 bool DkBatchProcess::prepareDeleteExisting() {
916
917 if (QFileInfo(mSaveInfo.outputFilePath()).exists() && mSaveInfo.mode() == DkSaveInfo::mode_overwrite) {
918
919 mSaveInfo.createBackupFilePath();
920
921 // check the uniqueness : )
922 if (QFileInfo(mSaveInfo.backupFilePath()).exists()) {
923 mLogStrings.append(QObject::tr("Error: back-up (%1) file already exists").arg(mSaveInfo.backupFilePath()));
924 mSaveInfo.clearBackupFilePath();
925 return false;
926 }
927
928 QFile file(mSaveInfo.outputFilePath());
929
930 if (!file.rename(mSaveInfo.backupFilePath())) {
931 mLogStrings.append(QObject::tr("Error: could not rename existing file to %1").arg(mSaveInfo.backupFilePath()));
932 mLogStrings.append(file.errorString());
933 mSaveInfo.clearBackupFilePath();
934 return false;
935 }
936 }
937
938 return true;
939 }
940
deleteOrRestoreExisting()941 bool DkBatchProcess::deleteOrRestoreExisting() {
942
943 QFileInfo outInfo(mSaveInfo.outputFilePath());
944
945 if (outInfo.exists() && !mSaveInfo.backupFilePath().isEmpty() && mSaveInfo.backupFileInfo().exists()) {
946 QFile file(mSaveInfo.backupFilePath());
947
948 if (!file.remove()) {
949 mLogStrings.append(QObject::tr("Error: could not delete existing file"));
950 mLogStrings.append(file.errorString());
951 return false;
952 }
953 }
954 // fall-back
955 else if (!outInfo.exists()) {
956
957 QFile file(mSaveInfo.backupFilePath());
958
959 if (!file.rename(mSaveInfo.outputFilePath())) {
960 mLogStrings.append(QObject::tr("Ui - a lot of things went wrong. Your original file can be found here: %1").arg(mSaveInfo.backupFilePath()));
961 mLogStrings.append(file.errorString());
962 return false;
963 }
964 else {
965 mLogStrings.append(QObject::tr("I could not save to %1 so I restored the original file.").arg(mSaveInfo.outputFilePath()));
966 }
967 }
968
969 return true;
970 }
971
deleteOriginalFile()972 bool DkBatchProcess::deleteOriginalFile() {
973
974 if (mSaveInfo.inputFilePath() == mSaveInfo.outputFilePath())
975 return true;
976
977 if (!mFailure && mSaveInfo.isDeleteOriginal()) {
978 QFile oFile(mSaveInfo.inputFilePath());
979
980 if (oFile.remove())
981 mLogStrings.append(QObject::tr("%1 deleted.").arg(mSaveInfo.inputFilePath()));
982 else {
983 mFailure++;
984 mLogStrings.append(QObject::tr("I could not delete %1").arg(mSaveInfo.inputFilePath()));
985 return false;
986 }
987 }
988 else if (mFailure)
989 mLogStrings.append(QObject::tr("I did not delete the original because I detected %1 failure(s).").arg(mFailure));
990
991 return true;
992 }
993
994 // DkBatchConfig --------------------------------------------------------------------
DkBatchConfig(const QStringList & fileList,const QString & outputDir,const QString & fileNamePattern)995 DkBatchConfig::DkBatchConfig(const QStringList& fileList, const QString& outputDir, const QString& fileNamePattern) {
996
997 mFileList = fileList;
998 mOutputDirPath = outputDir;
999 mFileNamePattern = fileNamePattern;
1000
1001 }
1002
isOk() const1003 bool DkBatchConfig::isOk() const {
1004
1005 if (mOutputDirPath.isEmpty())
1006 return false;
1007
1008 QDir oDir(mOutputDirPath);
1009
1010 if (!oDir.exists()) {
1011 if (!oDir.mkpath("."))
1012 return false; // output dir does not exist & I cannot create it
1013 }
1014
1015 if (mFileList.empty())
1016 return false;
1017
1018 if (mFileNamePattern.isEmpty())
1019 return false;
1020
1021 return true;
1022 }
1023
1024
1025 // DkBatchProcessing --------------------------------------------------------------------
DkBatchProcessing(const DkBatchConfig & config,QWidget * parent)1026 DkBatchProcessing::DkBatchProcessing(const DkBatchConfig& config, QWidget* parent /*= 0*/) : QObject(parent) {
1027
1028 mBatchConfig = config;
1029
1030 connect(&mBatchWatcher, SIGNAL(progressValueChanged(int)), this, SIGNAL(progressValueChanged(int)));
1031 connect(&mBatchWatcher, SIGNAL(finished()), this, SIGNAL(finished()));
1032 }
1033
init()1034 void DkBatchProcessing::init() {
1035
1036 mBatchItems.clear();
1037
1038 QStringList fileList = mBatchConfig.getFileList();
1039
1040 for (int idx = 0; idx < fileList.size(); idx++) {
1041
1042 DkSaveInfo si = mBatchConfig.saveInfo();
1043
1044 QFileInfo cFileInfo = QFileInfo(fileList.at(idx));
1045 QString outDir = si.isInputDirOutputDir() ? cFileInfo.absolutePath() : mBatchConfig.getOutputDirPath();
1046
1047 DkFileNameConverter converter(cFileInfo.fileName(), mBatchConfig.getFileNamePattern(), idx);
1048 QString outputFilePath = QFileInfo(outDir, converter.getConvertedFileName()).absoluteFilePath();
1049
1050 // set input/output file path
1051 si.setInputFilePath(fileList.at(idx));
1052 si.setOutputFilePath(outputFilePath);
1053
1054 DkBatchProcess cProcess(si);
1055 cProcess.setProcessChain(mBatchConfig.getProcessFunctions());
1056
1057 mBatchItems.push_back(cProcess);
1058 }
1059 }
1060
saveSettings(QSettings & settings) const1061 void DkBatchConfig::saveSettings(QSettings & settings) const {
1062
1063 settings.beginGroup("General"); // this general group could be removed in future releases
1064 settings.setValue("FileList", mFileList.join(";"));
1065 settings.setValue("OutputDirPath", mOutputDirPath);
1066 settings.setValue("FileNamePattern", mFileNamePattern);
1067
1068 mSaveInfo.saveSettings(settings);
1069
1070 for (auto pf : mProcessFunctions)
1071 pf->saveSettings(settings);
1072
1073 settings.endGroup();
1074 }
1075
loadSettings(QSettings & settings)1076 void DkBatchConfig::loadSettings(QSettings & settings) {
1077
1078 settings.beginGroup("General");
1079 mFileList = settings.value("FileList", mFileList).toString().split(";");
1080 mOutputDirPath = settings.value("OutputDirPath", mOutputDirPath).toString();
1081 mFileNamePattern = settings.value("FileNamePattern", mFileNamePattern).toString();
1082
1083 mSaveInfo.loadSettings(settings);
1084
1085 QStringList groups = settings.childGroups();
1086
1087 for (const QString& name : groups) {
1088
1089 // known groups that are not batch processes
1090 if (name == "SaveInfo")
1091 continue;
1092
1093 QSharedPointer<DkAbstractBatch> batch = DkAbstractBatch::createFromName(name);
1094
1095 // if it is valid - append the process
1096 if (batch) {
1097 batch->loadSettings(settings);
1098 mProcessFunctions << batch;
1099 }
1100 }
1101
1102 for (auto pf : mProcessFunctions)
1103 pf->saveSettings(settings);
1104
1105 settings.endGroup();
1106 }
1107
compute()1108 void DkBatchProcessing::compute() {
1109
1110 init();
1111
1112 qDebug() << "computing...";
1113
1114 if (mBatchWatcher.isRunning())
1115 mBatchWatcher.waitForFinished();
1116
1117 QFuture<void> future = QtConcurrent::map(mBatchItems, &nmc::DkBatchProcessing::computeItem);
1118 mBatchWatcher.setFuture(future);
1119 }
1120
computeItem(DkBatchProcess & item)1121 bool DkBatchProcessing::computeItem(DkBatchProcess& item) {
1122
1123 return item.compute();
1124 }
1125
postLoad()1126 void DkBatchProcessing::postLoad() {
1127
1128 // collect batch infos
1129 QVector<QSharedPointer<DkBatchInfo> > batchInfo;
1130
1131 for (DkBatchProcess batch : mBatchItems) {
1132 batchInfo << batch.batchInfo();
1133 }
1134
1135 for (QSharedPointer<DkAbstractBatch> fun : mBatchConfig.getProcessFunctions()) {
1136 fun->postLoad(batchInfo);
1137 }
1138 }
1139
computeBatch(const QString & settingsPath,const QString & logPath)1140 void DkBatchProcessing::computeBatch(const QString& settingsPath, const QString& logPath) {
1141
1142 DkTimer dt;
1143 DkBatchConfig bc = DkBatchProfile::loadProfile(settingsPath);
1144
1145 // guarantee that the output path exists
1146 if (!QDir().mkpath(bc.getOutputDirPath())) {
1147 qCritical() << "Could not create:" << bc.getOutputDirPath();
1148 return;
1149 }
1150
1151 QSharedPointer<nmc::DkBatchProcessing> process(new nmc::DkBatchProcessing());
1152 process->setBatchConfig(bc);
1153 process->compute();
1154
1155 process->waitForFinished(); // block
1156
1157 qInfo() << "batch finished with" << process->getNumFailures() << "errors in" << dt;
1158
1159 if (!logPath.isEmpty()) {
1160
1161 QFileInfo fi(logPath);
1162
1163 QDir().mkpath(fi.absolutePath());
1164
1165 QFile file(logPath);
1166 if (!file.open(QIODevice::WriteOnly))
1167 qWarning() << "Sorry, I could not write to" << logPath;
1168 else {
1169 QStringList log = process->getLog();
1170 QTextStream s(&file);
1171 for (const QString& line : log)
1172 s << line << '\n';
1173 qInfo() << "log written to: " << logPath;
1174 }
1175 }
1176 }
1177
1178
getLog() const1179 QStringList DkBatchProcessing::getLog() const {
1180
1181 QStringList log;
1182
1183 for (DkBatchProcess batch : mBatchItems) {
1184
1185 log << batch.getLog();
1186 log << ""; // add empty line between images
1187 }
1188
1189 return log;
1190 }
1191
getNumFailures() const1192 int DkBatchProcessing::getNumFailures() const {
1193
1194 int numFailures = 0;
1195
1196 for (DkBatchProcess batch : mBatchItems) {
1197
1198 if (batch.hasFailed())
1199 numFailures++;
1200 }
1201
1202 return numFailures;
1203 }
1204
getNumProcessed() const1205 int DkBatchProcessing::getNumProcessed() const {
1206
1207 int numProcessed = 0;
1208
1209 for (DkBatchProcess batch : mBatchItems) {
1210
1211 if (batch.wasProcessed())
1212 numProcessed++;
1213 }
1214
1215 return numProcessed;
1216 }
1217
getCurrentResults()1218 QList<int> DkBatchProcessing::getCurrentResults() {
1219
1220 if (mResList.empty()) {
1221 for (int idx = 0; idx < mBatchItems.size(); idx++)
1222 mResList.append(batch_item_not_computed);
1223 }
1224
1225 for (int idx = 0; idx < mResList.size(); idx++) {
1226
1227 if (mResList.at(idx) != batch_item_not_computed)
1228 continue;
1229
1230 if (mBatchItems.at(idx).wasProcessed())
1231 mResList[idx] = mBatchItems.at(idx).hasFailed() ? batch_item_failed : batch_item_succeeded;
1232 }
1233
1234 return mResList;
1235 }
1236
getResultList() const1237 QStringList DkBatchProcessing::getResultList() const {
1238
1239 QStringList results;
1240
1241 for (DkBatchProcess batch : mBatchItems) {
1242
1243 if (batch.wasProcessed())
1244 results.append(getBatchSummary(batch));
1245 }
1246
1247 return results;
1248 }
1249
getBatchSummary(const DkBatchProcess & batch) const1250 QString DkBatchProcessing::getBatchSummary(const DkBatchProcess& batch) const {
1251
1252 QString res = batch.inputFile() + "\t";
1253
1254 if (!batch.hasFailed())
1255 res += " <span style=\" color:#00aa00;\">" + tr("[OK]") + "</span>";
1256 else
1257 res += " <span style=\" color:#aa0000;\">" + tr("[FAIL]") + "</span>";
1258
1259 return res;
1260 }
1261
waitForFinished()1262 void DkBatchProcessing::waitForFinished() {
1263 mBatchWatcher.waitForFinished();
1264 }
1265
getNumItems() const1266 int DkBatchProcessing::getNumItems() const {
1267
1268 return mBatchItems.size();
1269 }
1270
isComputing() const1271 bool DkBatchProcessing::isComputing() const {
1272
1273 return mBatchWatcher.isRunning();
1274 }
1275
cancel()1276 void DkBatchProcessing::cancel() {
1277
1278 mBatchWatcher.cancel();
1279 }
1280
1281 // DkBatchProfile --------------------------------------------------------------------
1282 QString DkBatchProfile::ext = "pnm"; // profile file extension
1283
DkBatchProfile(const QString & profileDir)1284 DkBatchProfile::DkBatchProfile(const QString& profileDir) {
1285
1286 mProfileDir = (profileDir.isEmpty()) ? defaultProfilePath() : profileDir;
1287 }
1288
loadProfile(const QString & profilePath)1289 DkBatchConfig DkBatchProfile::loadProfile(const QString & profilePath) {
1290
1291 QFileInfo fi(profilePath);
1292 if (!fi.exists() || !fi.isFile()) {
1293 qInfo() << "cannot read profile from:" << profilePath;
1294 return DkBatchConfig();
1295 }
1296
1297 QSettings s(profilePath, QSettings::IniFormat);
1298 DkBatchConfig bc;
1299 bc.loadSettings(s);
1300
1301 return bc;
1302 }
1303
saveProfile(const QString & profilePath,const DkBatchConfig & batchConfig)1304 bool DkBatchProfile::saveProfile(const QString & profilePath, const DkBatchConfig & batchConfig) {
1305
1306 QSettings s(profilePath, QSettings::IniFormat);
1307 s.clear();
1308 batchConfig.saveSettings(s);
1309
1310 return true;
1311 }
1312
defaultProfilePath()1313 QString DkBatchProfile::defaultProfilePath() {
1314
1315 return DkUtils::getAppDataPath() + QDir::separator() + "Profiles";
1316 }
1317
profileNameToPath(const QString & profileName)1318 QString DkBatchProfile::profileNameToPath(const QString & profileName) {
1319 return defaultProfilePath() + QDir::separator() + profileName + "." + ext;
1320 }
1321
profileNames()1322 QStringList DkBatchProfile::profileNames() {
1323
1324 if (mProfilePaths.empty())
1325 mProfilePaths = index(mProfileDir);
1326
1327 QStringList userNames;
1328 for (const QString& p : mProfilePaths)
1329 userNames << makeUserFriendly(p);
1330
1331 return userNames;
1332 }
1333
index(const QString & profileDir) const1334 QStringList DkBatchProfile::index(const QString & profileDir) const {
1335
1336 QStringList exts;
1337 exts << "*." + ext;
1338
1339 QDir pd(profileDir);
1340 QStringList profiles = pd.entryList(exts, QDir::Files, QDir::Name);
1341
1342 qDebug() << "I have found these profiles: " << profiles;
1343 if (profiles.empty())
1344 qInfo() << "no profiles found in" << profileDir;
1345
1346 return profiles;
1347 }
1348
makeUserFriendly(const QString & profilePath)1349 QString DkBatchProfile::makeUserFriendly(const QString & profilePath) {
1350
1351 QString pName = QFileInfo(profilePath).baseName();
1352 return pName;
1353 }
1354
extension()1355 QString DkBatchProfile::extension() {
1356 return ext;
1357 }
1358
1359 }