1 /* ============================================================
2  *
3  * Date        : 2008-02-10
4  * Description : a tool to fix automatically camera lens aberrations
5  *
6  * Copyright (C) 2008      by Adrian Schroeter <adrian at suse dot de>
7  * Copyright (C) 2008-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
8  *
9  * This program is free software; you can redistribute it
10  * and/or modify it under the terms of the GNU General
11  * Public License as published by the Free Software Foundation;
12  * either version 2, or (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * ============================================================ */
20 
21 #include "lensfuniface.h"
22 
23 // Qt includes
24 
25 #include <QStandardPaths>
26 #include <QFile>
27 #include <QDir>
28 
29 // Local includes
30 
31 #include "digikam_debug.h"
32 
33 // Disable deprecated API from Lensfun.
34 #if defined(Q_CC_GNU)
35 #   pragma GCC diagnostic push
36 #   pragma GCC diagnostic ignored "-Wdeprecated-declarations"
37 #endif
38 
39 #if defined(Q_CC_CLANG)
40 #   pragma clang diagnostic push
41 #   pragma clang diagnostic ignored "-Wdeprecated-declarations"
42 #endif
43 
44 namespace Digikam
45 {
46 
47 class Q_DECL_HIDDEN LensFunIface::Private
48 {
49 public:
50 
Private()51     explicit Private()
52       : lfDb      (nullptr),
53         lfCameras (nullptr),
54         usedLens  (nullptr),
55         usedCamera(nullptr)
56     {
57     }
58 
59     // To be used for modification
60     LensFunContainer       settings;
61 
62     // Database items
63     lfDatabase*            lfDb;
64     const lfCamera* const* lfCameras;
65 
66     QString                makeDescription;
67     QString                modelDescription;
68     QString                lensDescription;
69 
70     LensPtr                usedLens;
71     DevicePtr              usedCamera;
72 };
73 
LensFunIface()74 LensFunIface::LensFunIface()
75     : d(new Private)
76 {
77     d->lfDb          = lf_db_new();
78 
79     // Lensfun host XML files in a dedicated sub-directory.
80 
81     QString lensPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
82                                               QLatin1String("lensfun"),
83                                               QStandardPaths::LocateDirectory);
84 
85     QDir lensDir;
86 
87     // In first try to use last Lensfun version data dir.
88 
89     lensDir = QDir(lensPath + QLatin1String("/version_2"), QLatin1String("*.xml"));
90 
91     if (lensDir.entryList().isEmpty())
92     {
93         // Fail-back to revision 1.
94 
95         lensDir = QDir(lensPath + QLatin1String("/version_1"), QLatin1String("*.xml"));
96 
97         if (lensDir.entryList().isEmpty())
98         {
99            // Fail-back to revision 0 which host XML data in root data directory.
100 
101            lensDir = QDir(lensPath, QLatin1String("*.xml"));
102         }
103     }
104 
105     qCDebug(DIGIKAM_DIMG_LOG) << "Using root lens database dir: " << lensPath;
106 
107     foreach (const QString& lens, lensDir.entryList())
108     {
109         qCDebug(DIGIKAM_DIMG_LOG) << "Load lens database file: " << lens;
110         d->lfDb->Load(QFile::encodeName(lensDir.absoluteFilePath(lens)).constData());
111     }
112 
113     d->lfDb->Load();
114 
115     d->lfCameras = d->lfDb->GetCameras();
116 }
117 
~LensFunIface()118 LensFunIface::~LensFunIface()
119 {
120     lf_db_destroy(d->lfDb);
121     delete d;
122 }
123 
setSettings(const LensFunContainer & other)124 void LensFunIface::setSettings(const LensFunContainer& other)
125 {
126     d->settings   = other;
127     d->usedCamera = findCamera(d->settings.cameraMake, d->settings.cameraModel);
128     d->usedLens   = findLens(d->settings.lensModel);
129 }
130 
settings() const131 LensFunContainer LensFunIface::settings() const
132 {
133     return d->settings;
134 }
135 
usedCamera() const136 LensFunIface::DevicePtr LensFunIface::usedCamera() const
137 {
138     return d->usedCamera;
139 }
140 
setUsedCamera(DevicePtr cam)141 void LensFunIface::setUsedCamera(DevicePtr cam)
142 {
143     d->usedCamera           = cam;
144     d->settings.cameraMake  = d->usedCamera ? QLatin1String(d->usedCamera->Maker) : QString();
145     d->settings.cameraModel = d->usedCamera ? QLatin1String(d->usedCamera->Model) : QString();
146     d->settings.cropFactor  = d->usedCamera ? d->usedCamera->CropFactor           : -1.0;
147 }
148 
usedLens() const149 LensFunIface::LensPtr LensFunIface::usedLens() const
150 {
151     return d->usedLens;
152 }
153 
setUsedLens(LensPtr lens)154 void LensFunIface::setUsedLens(LensPtr lens)
155 {
156     d->usedLens            = lens;
157     d->settings.lensModel  = d->usedLens ? QLatin1String(d->usedLens->Model) : QString();
158 }
159 
lensFunDataBase() const160 lfDatabase* LensFunIface::lensFunDataBase() const
161 {
162     return d->lfDb;
163 }
164 
makeDescription() const165 QString LensFunIface::makeDescription() const
166 {
167     return d->makeDescription;
168 }
169 
modelDescription() const170 QString LensFunIface::modelDescription() const
171 {
172     return d->modelDescription;
173 }
174 
lensDescription() const175 QString LensFunIface::lensDescription() const
176 {
177     return d->lensDescription;
178 }
179 
lensFunCameras() const180 const lfCamera* const* LensFunIface::lensFunCameras() const
181 {
182     return d->lfCameras;
183 }
184 
setFilterSettings(const LensFunContainer & other)185 void LensFunIface::setFilterSettings(const LensFunContainer& other)
186 {
187     d->settings.filterCCA = other.filterCCA;
188     d->settings.filterVIG = other.filterVIG;
189     d->settings.filterDST = other.filterDST;
190     d->settings.filterGEO = other.filterGEO;
191 }
192 
findCamera(const QString & make,const QString & model) const193 LensFunIface::DevicePtr LensFunIface::findCamera(const QString& make, const QString& model) const
194 {
195     const lfCamera* const* cameras = d->lfDb->GetCameras();
196 
197     while (cameras && *cameras)
198     {
199         DevicePtr cam = *cameras;
200 //      qCDebug(DIGIKAM_DIMG_LOG) << "Query camera:" << cam->Maker << "-" << cam->Model;
201 
202         if ((QString::fromLatin1(cam->Maker).toLower() == make.toLower()) &&
203             (QString::fromLatin1(cam->Model).toLower() == model.toLower()))
204         {
205             qCDebug(DIGIKAM_DIMG_LOG) << "Search for camera " << make << "-" << model << " ==> true";
206             return cam;
207         }
208 
209         ++cameras;
210     }
211 
212     qCDebug(DIGIKAM_DIMG_LOG) << "Search for camera " << make << "-" << model << " ==> false";
213     return nullptr;
214 }
215 
findLens(const QString & model) const216 LensFunIface::LensPtr LensFunIface::findLens(const QString& model) const
217 {
218     const lfLens* const* lenses = d->lfDb->GetLenses();
219 
220     while (lenses && *lenses)
221     {
222         LensPtr lens = *lenses;
223 
224         if (QString::fromLatin1(lens->Model) == model)
225         {
226             qCDebug(DIGIKAM_DIMG_LOG) << "Search for lens " << model << " ==> true";
227             return lens;
228         }
229 
230         ++lenses;
231     }
232 
233     qCDebug(DIGIKAM_DIMG_LOG) << "Search for lens " << model << " ==> false";
234     return nullptr;
235 }
236 
findLenses(const lfCamera * const lfCamera,const QString & lensDesc,const QString & lensMaker) const237 LensFunIface::LensList LensFunIface::findLenses(const lfCamera* const lfCamera,
238                                                 const QString& lensDesc,
239                                                 const QString& lensMaker) const
240 {
241     LensList lensList;
242 
243     if (lfCamera)
244     {
245         const char* const maker     = lensMaker.isEmpty() ? nullptr : lensMaker.toLatin1().constData();
246         const char* const model     = lensDesc.isEmpty()  ? nullptr : lensDesc.toLatin1().constData();
247         const lfLens* const *lfLens = d->lfDb->FindLenses(lfCamera, maker, model);
248 
249         while (lfLens && *lfLens)
250         {
251             lensList << (*lfLens);
252             ++lfLens;
253         }
254     }
255 
256     return lensList;
257 }
258 
findFromMetadata(DMetadata * const meta)259 LensFunIface::MetadataMatch LensFunIface::findFromMetadata(DMetadata* const meta)
260 {
261     MetadataMatch ret  = MetadataNoMatch;
262     d->settings        = LensFunContainer();
263     d->usedCamera      = nullptr;
264     d->usedLens        = nullptr;
265     d->lensDescription.clear();
266 
267     if (!meta || meta->isEmpty())
268     {
269         qCDebug(DIGIKAM_DIMG_LOG) << "No metadata available";
270         return LensFunIface::MetadataUnavailable;
271     }
272 
273     PhotoInfoContainer photoInfo = meta->getPhotographInformation();
274     d->makeDescription           = photoInfo.make.trimmed();
275     d->modelDescription          = photoInfo.model.trimmed();
276     bool exactMatch              = true;
277 
278     if (d->makeDescription.isEmpty())
279     {
280         qCDebug(DIGIKAM_DIMG_LOG) << "No camera maker info available";
281         exactMatch = false;
282     }
283     else
284     {
285         // NOTE: see bug #184156:
286         // Some rules to wrap unknown camera device from Lensfun database, which have equivalent in fact.
287 
288         if (d->makeDescription == QLatin1String("Canon"))
289         {
290             if (d->modelDescription == QLatin1String("Canon EOS Kiss Digital X"))
291             {
292                 d->modelDescription = QLatin1String("Canon EOS 400D DIGITAL");
293             }
294 
295             if (d->modelDescription == QLatin1String("G1 X"))
296             {
297                 d->modelDescription = QLatin1String("G1X");
298             }
299         }
300 
301         if (d->makeDescription.contains(QLatin1String("olympus"), Qt::CaseInsensitive))
302         {
303             if (!findCamera(d->makeDescription, d->modelDescription))
304             {
305                 QStringList olympusList;
306                 olympusList << QLatin1String("Olympus Imaging Corp.");
307                 olympusList << QLatin1String("Olympus Corporation");
308                 olympusList << QLatin1String("Olympus");
309 
310                 while (!olympusList.isEmpty())
311                 {
312                     if (findCamera(olympusList.first(), d->modelDescription))
313                     {
314                         d->makeDescription = olympusList.first();
315                         break;
316                     }
317 
318                     olympusList.removeFirst();
319                 }
320             }
321         }
322 
323         d->lensDescription = photoInfo.lens.trimmed();
324 
325         // ------------------------------------------------------------------------------------------------
326 
327         DevicePtr lfCamera = findCamera(d->makeDescription, d->modelDescription);
328 
329         if (lfCamera)
330         {
331             setUsedCamera(lfCamera);
332 
333             qCDebug(DIGIKAM_DIMG_LOG) << "Camera maker   : " << d->settings.cameraMake;
334             qCDebug(DIGIKAM_DIMG_LOG) << "Camera model   : " << d->settings.cameraModel;
335 
336             // ------------------------------------------------------------------------------------------------
337             // -- Performing lens description searches.
338 
339             LensList lensMatches;
340 
341             if (!d->lensDescription.isEmpty())
342             {
343                 QString  lensCutted;
344                 LensList lensList;
345 
346                 // STAGE 1, search in LensFun database as well.
347 
348                 lensList = findLenses(d->usedCamera, d->lensDescription);
349                 qCDebug(DIGIKAM_DIMG_LOG) << "* Check for lens by direct query (" << d->lensDescription << " : " << lensList.count() << ")";
350                 lensMatches.append(lensList);
351 
352                 // STAGE 2, Adapt exiv2 strings to lensfun strings for Nikon.
353 
354                 lensCutted = d->lensDescription;
355 
356                 if (lensCutted.contains(QLatin1String("Nikon")))
357                 {
358                     lensCutted.remove(QLatin1String("Nikon "));
359                     lensCutted.remove(QLatin1String("Zoom-"));
360                     lensCutted.replace(QLatin1String("IF-ID"), QLatin1String("ED-IF"));
361                     lensList = findLenses(d->usedCamera, lensCutted);
362                     qCDebug(DIGIKAM_DIMG_LOG) << "* Check for Nikon lens (" << lensCutted << " : " << lensList.count() << ")";
363                     lensMatches.append(lensList);
364                 }
365 
366                 // TODO : Add here more specific lens maker rules.
367 
368                 // LAST STAGE, Adapt exiv2 strings to lensfun strings. Some lens description use something like that :
369                 // "10.0 - 20.0 mm". This must be adapted like this : "10-20mm"
370 
371                 lensCutted = d->lensDescription;
372                 lensCutted.replace(QRegExp(QLatin1String("\\.[0-9]")), QLatin1String("")); //krazy:exclude=doublequote_chars
373                 lensCutted.replace(QLatin1String(" - "), QLatin1String("-"));
374                 lensCutted.replace(QLatin1String(" mm"), QLatin1String("mn"));
375                 lensList   = findLenses(d->usedCamera, lensCutted);
376                 qCDebug(DIGIKAM_DIMG_LOG) << "* Check for no maker lens (" << lensCutted << " : " << lensList.count() << ")";
377                 lensMatches.append(lensList);
378 
379                 // Remove all duplicate lenses in the list by using QSet.
380 
381                 lensMatches = lensMatches.toSet().toList();
382             }
383             else
384             {
385                 qCDebug(DIGIKAM_DIMG_LOG) << "Lens description string is empty";
386 
387                 const LensList lensList = findLenses(d->usedCamera, QString());
388 
389                 if (lensList.count() == 1)
390                 {
391                     // NOTE: see bug #407157
392 
393                     qCDebug(DIGIKAM_DIMG_LOG) << "For the camera " << d->settings.cameraModel
394                                               << " there is exactly one lens in the database: "
395                                               << lensList.first()->Model;
396                     lensMatches.append(lensList);
397                 }
398                 else
399                 {
400                     exactMatch = false;
401                 }
402             }
403 
404             // Display the results.
405 
406             if      (lensMatches.isEmpty())
407             {
408                 qCDebug(DIGIKAM_DIMG_LOG) << "lens matches   : NOT FOUND";
409                 exactMatch = false;
410             }
411             else if (lensMatches.count() == 1)
412             {
413                 // Best case for an exact match is to have only one item returned by Lensfun searches.
414 
415                 setUsedLens(lensMatches.first());
416                 qCDebug(DIGIKAM_DIMG_LOG) << "Lens found     : " << d->settings.lensModel;
417                 qCDebug(DIGIKAM_DIMG_LOG) << "Crop Factor    : " << d->settings.cropFactor;
418             }
419             else
420             {
421                 qCDebug(DIGIKAM_DIMG_LOG) << "lens matches   : more than one...";
422                 const lfLens* similar = nullptr;
423                 double percent        = 0.0;
424 
425                 foreach (const lfLens* const l, lensMatches)
426                 {
427                     double result = checkSimilarity(d->lensDescription, QLatin1String(l->Model));
428 
429                     if (result > percent)
430                     {
431                         percent = result;
432                         similar = l;
433                     }
434                 }
435 
436                 if (similar)
437                 {
438                     qCDebug(DIGIKAM_DIMG_LOG) << "found similary match from" << lensMatches.count()
439                                               << "possibilities:" << similar->Model
440                                               << "similarity:" << percent;
441                     setUsedLens(similar);
442                 }
443                 else
444                 {
445                     exactMatch = false;
446                 }
447             }
448         }
449         else
450         {
451             qCDebug(DIGIKAM_DIMG_LOG) << "Cannot find Lensfun camera device for (" << d->makeDescription << " - " << d->modelDescription << ")";
452             exactMatch = false;
453         }
454     }
455 
456     // ------------------------------------------------------------------------------------------------
457     // Performing Lens settings searches.
458 
459     QString temp = photoInfo.focalLength;
460 
461     if (temp.isEmpty())
462     {
463         qCDebug(DIGIKAM_DIMG_LOG) << "Focal Length   : NOT FOUND";
464         exactMatch = false;
465     }
466 
467     d->settings.focalLength = temp.mid(0, temp.length() - 3).toDouble(); // HACK: strip the " mm" at the end ...
468     qCDebug(DIGIKAM_DIMG_LOG) << "Focal Length   : " << d->settings.focalLength;
469 
470     // ------------------------------------------------------------------------------------------------
471 
472     temp = photoInfo.aperture;
473 
474     if (temp.isEmpty())
475     {
476         qCDebug(DIGIKAM_DIMG_LOG) << "Aperture       : NOT FOUND";
477         exactMatch = false;
478     }
479 
480     d->settings.aperture = temp.mid(1).toDouble();
481     qCDebug(DIGIKAM_DIMG_LOG) << "Aperture       : " << d->settings.aperture;
482 
483     // ------------------------------------------------------------------------------------------------
484     // Try to get subject distance value.
485 
486     // From standard Exif.
487 
488     temp = meta->getExifTagString("Exif.Photo.SubjectDistance");
489 
490     if (temp.isEmpty())
491     {
492         // From standard XMP.
493 
494         temp = meta->getXmpTagString("Xmp.exif.SubjectDistance");
495     }
496 
497     if (temp.isEmpty())
498     {
499         // From Canon Makernote.
500 
501         temp = meta->getExifTagString("Exif.CanonSi.SubjectDistance");
502     }
503 
504     if (temp.isEmpty())
505     {
506         // From Nikon Makernote.
507 
508         temp = meta->getExifTagString("Exif.NikonLd2.FocusDistance");
509     }
510 
511     if (temp.isEmpty())
512     {
513         // From Nikon Makernote.
514 
515         temp = meta->getExifTagString("Exif.NikonLd3.FocusDistance");
516     }
517 
518     if (temp.isEmpty())
519     {
520         // From Olympus Makernote.
521 
522         temp = meta->getExifTagString("Exif.OlympusFi.FocusDistance");
523     }
524 
525     // TODO: Add here others Makernotes tags.
526 
527     if (temp.isEmpty())
528     {
529         qCDebug(DIGIKAM_DIMG_LOG) << "Subject dist.  : NOT FOUND : Use default value.";
530         temp = QLatin1String("1000");
531     }
532 
533     temp                        = temp.remove(QLatin1String(" m"));
534     bool ok;
535     d->settings.subjectDistance = temp.toDouble(&ok);
536 
537     if (!ok)
538     {
539         d->settings.subjectDistance = -1.0;
540     }
541 
542     qCDebug(DIGIKAM_DIMG_LOG) << "Subject dist.  : " << d->settings.subjectDistance;
543 
544     // ------------------------------------------------------------------------------------------------
545 
546     ret = exactMatch ? MetadataExactMatch : MetadataPartialMatch;
547 
548     qCDebug(DIGIKAM_DIMG_LOG) << "Metadata match : " << metadataMatchDebugStr(ret);
549 
550     return ret;
551 }
552 
metadataMatchDebugStr(MetadataMatch val) const553 QString LensFunIface::metadataMatchDebugStr(MetadataMatch val) const
554 {
555     QString ret;
556 
557     switch (val)
558     {
559         case MetadataNoMatch:
560             ret = QLatin1String("No Match");
561             break;
562 
563         case MetadataPartialMatch:
564             ret = QLatin1String("Partial Match");
565             break;
566 
567         default:
568             ret = QLatin1String("Exact Match");
569             break;
570     }
571 
572     return ret;
573 }
574 
supportsDistortion() const575 bool LensFunIface::supportsDistortion() const
576 {
577     if (!d->usedLens)
578     {
579         return false;
580     }
581 
582     lfLensCalibDistortion res;
583 
584     return d->usedLens->InterpolateDistortion(d->settings.focalLength, res);
585 }
586 
supportsCCA() const587 bool LensFunIface::supportsCCA() const
588 {
589     if (!d->usedLens)
590     {
591         return false;
592     }
593 
594     lfLensCalibTCA res;
595 
596     return d->usedLens->InterpolateTCA(d->settings.focalLength, res);
597 }
598 
supportsVig() const599 bool LensFunIface::supportsVig() const
600 {
601     if (!d->usedLens)
602     {
603         return false;
604     }
605 
606     lfLensCalibVignetting res;
607 
608     return d->usedLens->InterpolateVignetting(d->settings.focalLength,
609                                               d->settings.aperture,
610                                               d->settings.subjectDistance, res);
611 }
612 
supportsGeometry() const613 bool LensFunIface::supportsGeometry() const
614 {
615     return supportsDistortion();
616 }
617 
lensFunVersion()618 QString LensFunIface::lensFunVersion()
619 {
620     return QString::fromLatin1("%1.%2.%3-%4").arg(LF_VERSION_MAJOR)
621            .arg(LF_VERSION_MINOR)
622            .arg(LF_VERSION_MICRO)
623            .arg(LF_VERSION_BUGFIX);
624 }
625 
626 // Inspired by https://www.qtcentre.org/threads/49601-String-similarity-check
627 
checkSimilarity(const QString & a,const QString & b) const628 double LensFunIface::checkSimilarity(const QString& a, const QString& b) const
629 {
630     if (a.isEmpty() || b.isEmpty())
631     {
632         return 0.0;
633     }
634 
635     const int chars = 3;
636     int counter     = 0;
637 
638     QString spaces  = QString::fromLatin1(" ").repeated(chars - 1);
639     QString aa      = spaces + a + spaces;
640     QString bb      = spaces + b + spaces;
641 
642     for (int i = 0 ; i < (aa.count() - (chars - 1)) ; ++i)
643     {
644         QString part = aa.mid(i, chars);
645 
646         if (bb.contains(part, Qt::CaseInsensitive))
647         {
648             ++counter;
649         }
650     }
651 
652     QString s = (aa.length() < bb.length()) ? aa : bb;
653 
654     return (100.0 * counter / (s.length() - (chars - 1)));
655 }
656 
657 // Restore warnings
658 
659 #if defined(Q_CC_GNU)
660 #   pragma GCC diagnostic pop
661 #endif
662 
663 #if defined(Q_CC_CLANG)
664 #   pragma clang diagnostic pop
665 #endif
666 
667 } // namespace Digikam
668