File indexing completed on 2025-03-09 03:52:56

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2007-09-19
0007  * Description : Scanning a single item - photo metadata helper.
0008  *
0009  * SPDX-FileCopyrightText: 2007-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0010  * SPDX-FileCopyrightText: 2013-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "itemscanner_p.h"
0017 
0018 namespace Digikam
0019 {
0020 
0021 QString ItemScanner::iptcCorePropertyName(MetadataInfo::Field field)
0022 {
0023     // These strings are specified in DBSCHEMA.ods
0024 
0025     switch (field)
0026     {
0027         // Copyright table                                     krazy:exclude=copyright
0028 
0029         case MetadataInfo::IptcCoreCopyrightNotice:
0030             return QLatin1String("copyrightNotice");        // krazy:exclude=copyright
0031         case MetadataInfo::IptcCoreCreator:
0032             return QLatin1String("creator");
0033         case MetadataInfo::IptcCoreProvider:
0034             return QLatin1String("provider");
0035         case MetadataInfo::IptcCoreRightsUsageTerms:
0036             return QLatin1String("rightsUsageTerms");
0037         case MetadataInfo::IptcCoreSource:
0038             return QLatin1String("source");
0039         case MetadataInfo::IptcCoreCreatorJobTitle:
0040             return QLatin1String("creatorJobTitle");
0041         case MetadataInfo::IptcCoreInstructions:
0042             return QLatin1String("instructions");
0043 
0044         // ImageProperties table
0045 
0046         case MetadataInfo::IptcCoreCountryCode:
0047             return QLatin1String("countryCode");
0048         case MetadataInfo::IptcCoreCountry:
0049             return QLatin1String("country");
0050         case MetadataInfo::IptcCoreCity:
0051             return QLatin1String("city");
0052         case MetadataInfo::IptcCoreLocation:
0053             return QLatin1String("location");
0054         case MetadataInfo::IptcCoreProvinceState:
0055             return QLatin1String("provinceState");
0056         case MetadataInfo::IptcCoreIntellectualGenre:
0057             return QLatin1String("intellectualGenre");
0058         case MetadataInfo::IptcCoreJobID:
0059             return QLatin1String("jobId");
0060         case MetadataInfo::IptcCoreScene:
0061             return QLatin1String("scene");
0062         case MetadataInfo::IptcCoreSubjectCode:
0063             return QLatin1String("subjectCode");
0064         case MetadataInfo::IptcCoreContactInfoCity:
0065             return QLatin1String("creatorContactInfo.city");
0066         case MetadataInfo::IptcCoreContactInfoCountry:
0067             return QLatin1String("creatorContactInfo.country");
0068         case MetadataInfo::IptcCoreContactInfoAddress:
0069             return QLatin1String("creatorContactInfo.address");
0070         case MetadataInfo::IptcCoreContactInfoPostalCode:
0071             return QLatin1String("creatorContactInfo.postalCode");
0072         case MetadataInfo::IptcCoreContactInfoProvinceState:
0073             return QLatin1String("creatorContactInfo.provinceState");
0074         case MetadataInfo::IptcCoreContactInfoEmail:
0075             return QLatin1String("creatorContactInfo.email");
0076         case MetadataInfo::IptcCoreContactInfoPhone:
0077             return QLatin1String("creatorContactInfo.phone");
0078         case MetadataInfo::IptcCoreContactInfoWebUrl:
0079             return QLatin1String("creatorContactInfo.webUrl");
0080         default:
0081             return QString();
0082     }
0083 }
0084 
0085 QString ItemScanner::detectImageFormat() const
0086 {
0087     DImg::FORMAT dimgFormat = d->img.detectedFormat();
0088 
0089     switch (dimgFormat)
0090     {
0091         case DImg::JPEG:
0092             return QLatin1String("JPG");
0093         case DImg::PNG:
0094             return QLatin1String("PNG");
0095         case DImg::TIFF:
0096             return QLatin1String("TIFF");
0097         case DImg::JP2K:
0098             return QLatin1String("JP2");
0099         case DImg::PGF:
0100             return QLatin1String("PGF");
0101         case DImg::HEIF:
0102             return QLatin1String("HEIF");
0103         case DImg::RAW:
0104         {
0105             QString format = QLatin1String("RAW-");
0106             format += d->fileInfo.suffix().toUpper();
0107             return format;
0108         }
0109         case DImg::NONE:
0110         case DImg::QIMAGE:
0111         {
0112             QString ext = d->fileInfo.suffix().toUpper();
0113 
0114             if (
0115                 (ext == QLatin1String("AVIF")) ||       // See bug #109060
0116                 (ext == QLatin1String("JPX"))
0117                )
0118             {
0119                 return ext;
0120             }
0121 
0122             QByteArray format = QImageReader::imageFormat(d->fileInfo.filePath());
0123 
0124             if (!format.isEmpty())
0125             {
0126                 return QString::fromUtf8(format).toUpper();
0127             }
0128 
0129             break;
0130         }
0131     }
0132 
0133     // See BUG #339341: Take file name suffix instead type mime analyze.
0134 
0135     return d->fileInfo.suffix().toUpper();
0136 }
0137 
0138 void ItemScanner::scanImageMetadata()
0139 {
0140     QVariantList metadataInfos = d->metadata->getMetadataFields(allImageMetadataFields());
0141 
0142     if (hasValidField(metadataInfos))
0143     {
0144         d->commit.commitImageMetadata = true;
0145         d->commit.imageMetadataInfos  = metadataInfos;
0146     }
0147 }
0148 
0149 void ItemScanner::commitImageMetadata()
0150 {
0151     CoreDbAccess().db()->addImageMetadata(d->scanInfo.id, d->commit.imageMetadataInfos);
0152 }
0153 
0154 void ItemScanner::scanItemPosition()
0155 {
0156     // This list must reflect the order required by CoreDB::addItemPosition
0157 
0158     MetadataFields fields;
0159     fields << MetadataInfo::Latitude
0160            << MetadataInfo::LatitudeNumber
0161            << MetadataInfo::Longitude
0162            << MetadataInfo::LongitudeNumber
0163            << MetadataInfo::Altitude
0164            << MetadataInfo::PositionOrientation
0165            << MetadataInfo::PositionTilt
0166            << MetadataInfo::PositionRoll
0167            << MetadataInfo::PositionAccuracy
0168            << MetadataInfo::PositionDescription;
0169 
0170     QVariantList metadataInfos = d->metadata->getMetadataFields(fields);
0171 
0172     if (hasValidField(metadataInfos))
0173     {
0174         d->commit.commitItemPosition = true;
0175         d->commit.imagePositionInfos = metadataInfos;
0176     }
0177 }
0178 
0179 void ItemScanner::commitItemPosition()
0180 {
0181     CoreDbAccess().db()->addItemPosition(d->scanInfo.id, d->commit.imagePositionInfos);
0182 }
0183 
0184 void ItemScanner::scanItemComments()
0185 {
0186     MetadataFields fields;
0187     fields << MetadataInfo::Headline;
0188 
0189     QVariantList metadataInfos = d->metadata->getMetadataFields(fields);
0190 
0191     // handles all possible fields, multi-language, author, date
0192 
0193     CaptionsMap captions = d->metadata->getItemComments();
0194     CaptionsMap titles   = d->metadata->getItemTitles();
0195 
0196     if (titles.isEmpty()            &&
0197         captions.isEmpty()          &&
0198         !hasValidField(metadataInfos))
0199     {
0200         return;
0201     }
0202 
0203     d->commit.commitItemComments = true;
0204     d->commit.captions           = captions;
0205     d->commit.titles             = titles;
0206 
0207     // Headline
0208 
0209     if (!metadataInfos.at(0).isNull())
0210     {
0211         d->commit.headline = metadataInfos.at(0).toString();
0212     }
0213 }
0214 
0215 void ItemScanner::commitItemComments()
0216 {
0217     CoreDbAccess access;
0218     ItemComments comments(access, d->scanInfo.id);
0219 
0220     // Description
0221 
0222     if (!d->commit.captions.isEmpty())
0223     {
0224         CaptionsMap::const_iterator it;
0225 
0226         for (it = d->commit.captions.constBegin() ; it != d->commit.captions.constEnd() ; ++it)
0227         {
0228             CaptionValues val = it.value();
0229             comments.addComment(val.caption, it.key(), val.author, val.date);
0230         }
0231     }
0232 
0233     // Headline
0234 
0235     if (!d->commit.headline.isNull())
0236     {
0237         comments.addHeadline(d->commit.headline);
0238     }
0239 
0240     // Title
0241 
0242     if (!d->commit.titles.isEmpty())
0243     {
0244         CaptionsMap::const_iterator it;
0245 
0246         for (it = d->commit.titles.constBegin() ; it != d->commit.titles.constEnd() ; ++it)
0247         {
0248             CaptionValues val = it.value();
0249             comments.addTitle(val.caption, it.key(), val.author, val.date);
0250         }
0251     }
0252 }
0253 
0254 void ItemScanner::scanItemCopyright()
0255 {
0256     Template t;
0257 
0258     if (!d->metadata->getCopyrightInformation(t))
0259     {
0260         return;
0261     }
0262 
0263     d->commit.commitItemCopyright = true;
0264     d->commit.copyrightTemplate   = t;
0265 }
0266 
0267 void ItemScanner::commitItemCopyright()
0268 {
0269     ItemCopyright copyright(d->scanInfo.id);
0270 
0271     // It is not clear if removeAll() should be called if d->scanMode == Rescan
0272 
0273     copyright.removeAll();
0274     copyright.setFromTemplate(d->commit.copyrightTemplate);
0275 }
0276 
0277 void ItemScanner::scanIPTCCore()
0278 {
0279     MetadataFields fields;
0280     fields << MetadataInfo::IptcCoreLocationInfo
0281            << MetadataInfo::IptcCoreIntellectualGenre
0282            << MetadataInfo::IptcCoreJobID
0283            << MetadataInfo::IptcCoreScene
0284            << MetadataInfo::IptcCoreSubjectCode;
0285 
0286     QVariantList metadataInfos = d->metadata->getMetadataFields(fields);
0287 
0288     if (!hasValidField(metadataInfos))
0289     {
0290         return;
0291     }
0292 
0293     d->commit.commitIPTCCore        = true;
0294     d->commit.iptcCoreMetadataInfos = metadataInfos;
0295 }
0296 
0297 void ItemScanner::commitIPTCCore()
0298 {
0299     ItemExtendedProperties props(d->scanInfo.id);
0300 
0301     if (!d->commit.iptcCoreMetadataInfos.at(0).isNull())
0302     {
0303         IptcCoreLocationInfo loc = d->commit.iptcCoreMetadataInfos.at(0).value<IptcCoreLocationInfo>();
0304 
0305         if (!loc.isNull())
0306         {
0307             props.setLocation(loc);
0308         }
0309     }
0310 
0311     if (!d->commit.iptcCoreMetadataInfos.at(1).isNull())
0312     {
0313         props.setIntellectualGenre(d->commit.iptcCoreMetadataInfos.at(1).toString());
0314     }
0315 
0316     if (!d->commit.iptcCoreMetadataInfos.at(2).isNull())
0317     {
0318         props.setJobId(d->commit.iptcCoreMetadataInfos.at(2).toString());
0319     }
0320 
0321     if (!d->commit.iptcCoreMetadataInfos.at(3).isNull())
0322     {
0323         props.setScene(d->commit.iptcCoreMetadataInfos.at(3).toStringList());
0324     }
0325 
0326     if (!d->commit.iptcCoreMetadataInfos.at(4).isNull())
0327     {
0328         props.setSubjectCode(d->commit.iptcCoreMetadataInfos.at(4).toStringList());
0329     }
0330 }
0331 
0332 void ItemScanner::scanTags()
0333 {
0334     // Check Keywords tag paths.
0335 
0336     QVariant var         = d->metadata->getMetadataField(MetadataInfo::Keywords);
0337     QStringList keywords = var.toStringList();
0338     QStringList filteredKeywords;
0339 
0340     // Extra empty tags check, empty tag = root tag which is not asignable
0341 
0342     for (int index = 0 ; index < keywords.size() ; ++index)
0343     {
0344         QString keyword = keywords.at(index);
0345 
0346         if (!keyword.isEmpty())
0347         {
0348 
0349             // _Digikam_root_tag_ is present in some photos tagged with older
0350             // version of digiKam, must be removed
0351 
0352             if (keyword.contains(QRegularExpression(QLatin1String("(_Digikam_root_tag_/|/_Digikam_root_tag_|_Digikam_root_tag_)"))))
0353             {
0354                 keyword = keyword.replace(QRegularExpression(QLatin1String("(_Digikam_root_tag_/|/_Digikam_root_tag_|_Digikam_root_tag_)")),
0355                                           QLatin1String(""));
0356             }
0357 
0358             filteredKeywords.append(keyword);
0359         }
0360     }
0361 
0362     if (!filteredKeywords.isEmpty())
0363     {
0364         // get tag ids, create if necessary
0365 
0366         QList<int> tagIds = TagsCache::instance()->getOrCreateTags(filteredKeywords);
0367         d->commit.tagIds += tagIds;
0368     }
0369 
0370     // Check Pick Label tag.
0371 
0372     int pickId = d->metadata->getItemPickLabel();
0373 
0374     if (pickId != -1)
0375     {
0376         qCDebug(DIGIKAM_DATABASE_LOG) << "Pick Label found :" << pickId;
0377 
0378         int tagId = TagsCache::instance()->tagForPickLabel((PickLabel)pickId);
0379 
0380         if (tagId)
0381         {
0382             d->commit.tagIds << tagId;
0383             d->commit.hasPickTag = true;
0384             qCDebug(DIGIKAM_DATABASE_LOG) << "Assigned Pick Label Tag :" << tagId;
0385         }
0386         else
0387         {
0388             qCDebug(DIGIKAM_DATABASE_LOG) << "Cannot find Pick Label Tag for :" << pickId;
0389         }
0390     }
0391 
0392     // Check Color Label tag.
0393 
0394     int colorId = d->metadata->getItemColorLabel();
0395 
0396     if (colorId != -1)
0397     {
0398         qCDebug(DIGIKAM_DATABASE_LOG) << "Color Label found :" << colorId;
0399 
0400         int tagId = TagsCache::instance()->tagForColorLabel((ColorLabel)colorId);
0401 
0402         if (tagId)
0403         {
0404             d->commit.tagIds << tagId;
0405             d->commit.hasColorTag = true;
0406             qCDebug(DIGIKAM_DATABASE_LOG) << "Assigned Color Label Tag :" << tagId;
0407         }
0408         else
0409         {
0410             qCDebug(DIGIKAM_DATABASE_LOG) << "Cannot find Color Label Tag for :" << colorId;
0411         }
0412     }
0413 }
0414 
0415 void ItemScanner::commitTags()
0416 {
0417     const QList<int>& currentTags = CoreDbAccess().db()->getItemTagIDs(d->scanInfo.id);
0418     const QVector<int>& colorTags = TagsCache::instance()->colorLabelTags();
0419     const QVector<int>& pickTags  = TagsCache::instance()->pickLabelTags();
0420     QList<int> removeTags;
0421 
0422     if (d->commit.hasColorTag || d->commit.hasPickTag)
0423     {
0424         Q_FOREACH (int tag, currentTags)
0425         {
0426             if (colorTags.contains(tag) || pickTags.contains(tag))
0427             {
0428                 removeTags << tag;
0429             }
0430         }
0431 
0432     }
0433 
0434     if (!removeTags.isEmpty())
0435     {
0436         CoreDbAccess().db()->removeTagsFromItems(QList<qlonglong>() << d->scanInfo.id, removeTags);
0437     }
0438 
0439     CoreDbAccess().db()->addTagsToItems(QList<qlonglong>() << d->scanInfo.id, d->commit.tagIds);
0440 }
0441 
0442 void ItemScanner::scanFaces()
0443 {
0444     QSize size = d->img.size();
0445 
0446     if (!size.isValid())
0447     {
0448         return;
0449     }
0450 
0451     QMultiMap<QString, QVariant> metadataFacesMap;
0452 
0453     if (!d->metadata->getItemFacesMap(metadataFacesMap))
0454     {
0455         return;
0456     }
0457 
0458     d->commit.commitFaces      = true;
0459     d->commit.metadataFacesMap = metadataFacesMap;
0460 }
0461 
0462 void ItemScanner::commitFaces()
0463 {
0464     FaceTagsEditor editor;
0465     QList<QRect> assignedRects;
0466     QMultiMap<QString, QVariant>::const_iterator it;
0467     QSize size                         = d->img.size();
0468     int orientation                    = d->img.orientation();
0469     QList<FaceTagsIface> databaseFaces = editor.databaseFaces(d->scanInfo.id);
0470 
0471     for (it = d->commit.metadataFacesMap.constBegin() ; it != d->commit.metadataFacesMap.constEnd() ; ++it)
0472     {
0473         QString name = it.key();
0474         QRectF rectF = it.value().toRectF();
0475 
0476         if (!rectF.isValid())
0477         {
0478             int tagId = FaceTags::getOrCreateTagForPerson(name);
0479 
0480             if (tagId)
0481             {
0482                 ItemInfo(d->scanInfo.id).setTag(tagId);
0483             }
0484             else
0485             {
0486                 qCDebug(DIGIKAM_DATABASE_LOG) << "Failed to create a person tag for name" << name;
0487             }
0488 
0489             continue;
0490         }
0491 
0492         QRect rect = TagRegion::relativeToAbsolute(rectF, size);
0493         TagRegion::adjustToOrientation(rect, orientation, size);
0494         TagRegion newRegion(rect);
0495 
0496         if (assignedRects.contains(rect))
0497         {
0498             continue;
0499         }
0500 
0501         assignedRects << rect;
0502         QList<FaceTagsIface>::iterator it1;
0503 
0504         for (it1 = databaseFaces.begin() ; it1 != databaseFaces.end() ; )
0505         {
0506              double minOverlap = (*it1).isConfirmedName() ? 0.25 : 0.5;
0507 
0508             if ((*it1).region().intersects(newRegion, minOverlap))
0509             {
0510                 // Remove the duplicate face in the database.
0511 
0512                 editor.removeFace((*it1));
0513                 it1 = databaseFaces.erase(it1);
0514 
0515                 continue;
0516             }
0517 
0518             ++it1;
0519         }
0520 
0521         if (name.isEmpty())
0522         {
0523             int tagId = FaceTags::unknownPersonTagId();
0524             FaceTagsIface face(FaceTagsIface::UnknownName, d->scanInfo.id, tagId, newRegion);
0525 
0526             editor.addManually(face);
0527         }
0528         else
0529         {
0530             int tagId = FaceTags::getOrCreateTagForPerson(name);
0531 
0532             if (tagId)
0533             {
0534                 editor.add(d->scanInfo.id, tagId, newRegion, false);
0535             }
0536             else
0537             {
0538                 qCDebug(DIGIKAM_DATABASE_LOG) << "Failed to create a person tag for name" << name;
0539             }
0540         }
0541     }
0542 }
0543 
0544 void ItemScanner::checkCreationDateFromMetadata(QVariant& dateFromMetadata) const
0545 {
0546     // creation date: fall back to file system property
0547 
0548     if (dateFromMetadata.isNull() || !dateFromMetadata.toDateTime().isValid())
0549     {
0550         dateFromMetadata = creationDateFromFilesystem(d->fileInfo);
0551     }
0552 }
0553 
0554 bool ItemScanner::checkRatingFromMetadata(const QVariant& ratingFromMetadata) const
0555 {
0556     // should only be overwritten if set in metadata
0557 
0558     if ((d->scanMode == Rescan) || (d->scanMode == CleanScan))
0559     {
0560         if (ratingFromMetadata.isNull() || (ratingFromMetadata.toInt() == -1))
0561         {
0562             return false;
0563         }
0564     }
0565 
0566     return true;
0567 }
0568 
0569 MetadataFields ItemScanner::allImageMetadataFields()
0570 {
0571     // This list must reflect the order required by CoreDB::addImageMetadata
0572 
0573     MetadataFields fields;
0574     fields << MetadataInfo::Make
0575            << MetadataInfo::Model
0576            << MetadataInfo::Lens
0577            << MetadataInfo::Aperture
0578            << MetadataInfo::FocalLength
0579            << MetadataInfo::FocalLengthIn35mm
0580            << MetadataInfo::ExposureTime
0581            << MetadataInfo::ExposureProgram
0582            << MetadataInfo::ExposureMode
0583            << MetadataInfo::Sensitivity
0584            << MetadataInfo::FlashMode
0585            << MetadataInfo::WhiteBalance
0586            << MetadataInfo::WhiteBalanceColorTemperature
0587            << MetadataInfo::MeteringMode
0588            << MetadataInfo::SubjectDistance
0589            << MetadataInfo::SubjectDistanceCategory;
0590     return fields;
0591 }
0592 
0593 } // namespace Digikam