1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2005-06-14
7  * Description : digiKam 8/16 bits image management API.
8  *               Metadata operations.
9  *
10  * Copyright (C) 2005-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11  * Copyright (C) 2006-2013 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
12  *
13  * This program is free software; you can redistribute it
14  * and/or modify it under the terms of the GNU General
15  * Public License as published by the Free Software Foundation;
16  * either version 2, or (at your option)
17  * any later version.
18  *
19  * This program is distributed in the hope that it will be useful,
20  * but WITHOUT ANY WARRANTY; without even the implied warranty of
21  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22  * GNU General Public License for more details.
23  *
24  * ============================================================ */
25 
26 #include "dimg_p.h"
27 
28 namespace Digikam
29 {
30 
getUniqueHash()31 QByteArray DImg::getUniqueHash()
32 {
33     if (hasAttribute(QLatin1String("uniqueHash")))
34     {
35         return attribute(QLatin1String("uniqueHash")).toByteArray();
36     }
37 
38     if (!hasAttribute(QLatin1String("originalFilePath")))
39     {
40         qCWarning(DIGIKAM_DIMG_LOG) << "DImg::getUniqueHash called without originalFilePath property set!";
41         return QByteArray();
42     }
43 
44     QString filePath = attribute(QLatin1String("originalFilePath")).toString();
45 
46     if (filePath.isEmpty())
47     {
48         return QByteArray();
49     }
50 
51     FileReadLocker lock(filePath);
52 
53     QScopedPointer<DMetadata> meta(new DMetadata(getMetadata()));
54     QByteArray ba   = meta->getExifEncoded();
55 
56     QByteArray hash = DImg::createUniqueHash(filePath, ba);
57     setAttribute(QLatin1String("uniqueHash"), hash);
58 
59     return hash;
60 }
61 
getUniqueHash(const QString & filePath)62 QByteArray DImg::getUniqueHash(const QString& filePath)
63 {
64     QScopedPointer<DMetadata> meta(new DMetadata(filePath));
65     QByteArray ba = meta->getExifEncoded();
66 
67     return DImg::createUniqueHash(filePath, ba);
68 }
69 
getUniqueHashV2()70 QByteArray DImg::getUniqueHashV2()
71 {
72     if (hasAttribute(QLatin1String("uniqueHashV2")))
73     {
74         return attribute(QLatin1String("uniqueHashV2")).toByteArray();
75     }
76 
77     if (!hasAttribute(QLatin1String("originalFilePath")))
78     {
79         qCWarning(DIGIKAM_DIMG_LOG) << "DImg::getUniqueHash called without originalFilePath property set!";
80         return QByteArray();
81     }
82 
83     QString filePath = attribute(QLatin1String("originalFilePath")).toString();
84 
85     if (filePath.isEmpty())
86     {
87         return QByteArray();
88     }
89 
90     FileReadLocker lock(filePath);
91 
92     QByteArray hash = DImg::createUniqueHashV2(filePath);
93     setAttribute(QLatin1String("uniqueHashV2"), hash);
94 
95     return hash;
96 }
97 
getUniqueHashV2(const QString & filePath)98 QByteArray DImg::getUniqueHashV2(const QString& filePath)
99 {
100     return DImg::createUniqueHashV2(filePath);
101 }
102 
createImageUniqueId()103 QByteArray DImg::createImageUniqueId()
104 {
105     NonDeterministicRandomData randomData(16);
106     QByteArray imageUUID = randomData.toHex();
107     imageUUID           += getUniqueHashV2();
108 
109     return imageUUID;
110 }
111 
prepareMetadataToSave(const QString & intendedDestPath,const QString & destMimeType,bool resetExifOrientationTag)112 void DImg::prepareMetadataToSave(const QString& intendedDestPath, const QString& destMimeType,
113                                  bool resetExifOrientationTag)
114 {
115     PrepareMetadataFlags flags = PrepareMetadataFlagsAll;
116 
117     if (!resetExifOrientationTag)
118     {
119         flags &= ~ResetExifOrientationTag;
120     }
121 
122     QUrl url = QUrl::fromLocalFile(originalFilePath());
123     prepareMetadataToSave(intendedDestPath, destMimeType, url.fileName(), flags);
124 }
125 
prepareMetadataToSave(const QString & intendedDestPath,const QString & destMimeType,const QString & originalFileName,PrepareMetadataFlags flags)126 void DImg::prepareMetadataToSave(const QString& intendedDestPath, const QString& destMimeType,
127                                  const QString& originalFileName, PrepareMetadataFlags flags)
128 {
129     if (isNull())
130     {
131         return;
132     }
133 
134     // Get image Exif/IPTC data.
135 
136     QScopedPointer<DMetadata> meta(new DMetadata(getMetadata()));
137 
138     qCDebug(DIGIKAM_DIMG_LOG) << "Prepare Metadata to save for" << intendedDestPath;
139 
140     if (flags & RemoveOldMetadataPreviews || flags & CreateNewMetadataPreview)
141     {
142         // Clear IPTC preview
143 
144         meta->removeIptcTag("Iptc.Application2.Preview");
145         meta->removeIptcTag("Iptc.Application2.PreviewFormat");
146         meta->removeIptcTag("Iptc.Application2.PreviewVersion");
147 
148         // Clear Exif thumbnail
149 
150         meta->removeExifThumbnail();
151 
152         // Clear Tiff thumbnail
153 
154         MetaEngine::MetaDataMap tiffThumbTags = meta->getExifTagsDataList(QStringList() << QLatin1String("SubImage1"));
155 
156         for (MetaEngine::MetaDataMap::iterator it = tiffThumbTags.begin() ; it != tiffThumbTags.end() ; ++it)
157         {
158             meta->removeExifTag(it.key().toLatin1().constData());
159         }
160 
161         // Clear Xmp preview from digiKam namespace
162 
163         meta->removeXmpTag("Xmp.digiKam.Preview");
164     }
165 
166     bool createNewPreview    = false;
167     QSize previewSize;
168 
169     // Refuse preview creation for images with transparency
170     // as long as we have no format to support this. See bug 286127
171 
172     bool skipPreviewCreation = hasTransparentPixels();
173 
174     if (flags & CreateNewMetadataPreview && !skipPreviewCreation)
175     {
176         const QSize standardPreviewSize(1280, 1280);
177         previewSize = size();
178 
179         // Scale to standard preview size. Only scale down, not up
180 
181         if (width() > (uint)standardPreviewSize.width() && height() > (uint)standardPreviewSize.height())
182         {
183             previewSize.scale(standardPreviewSize, Qt::KeepAspectRatio);
184         }
185 
186         // Only store a new preview if it is worth it - the original should be significantly larger than the preview
187 
188         createNewPreview = (2 * (uint)previewSize.width() <= width());
189     }
190 
191     if (createNewPreview)
192     {
193         // Create the preview QImage
194 
195         QImage preview;
196         {
197             if (!IccManager::isSRGB(*this))
198             {
199                 DImg previewDImg;
200 
201                 if (previewSize.width() >= (int)width())
202                 {
203                     previewDImg = copy();
204                 }
205                 else
206                 {
207                     previewDImg = smoothScale(previewSize.width(), previewSize.height(), Qt::IgnoreAspectRatio);
208                 }
209 
210                 IccManager manager(previewDImg);
211                 manager.transformToSRGB();
212                 preview = previewDImg.copyQImage();
213             }
214             else
215             {
216                 // Ensure that preview is not upscaled
217 
218                 if (previewSize.width() >= (int)width())
219                 {
220                     preview = copyQImage();
221                 }
222                 else
223                 {
224                     preview = smoothScale(previewSize.width(), previewSize.height(), Qt::IgnoreAspectRatio).copyQImage();
225                 }
226             }
227         }
228 
229         // Update IPTC preview.
230         // see bug #130525. a JPEG segment is limited to 64K. If the IPTC byte array is
231         // bigger than 64K during of image preview tag size, the target JPEG image will be
232         // broken. Note that IPTC image preview tag is limited to 256K!!!
233         // There is no limitation with TIFF and PNG about IPTC byte array size.
234         // So for a JPEG file, we don't store the IPTC preview.
235 
236         if (((destMimeType.toUpper() != QLatin1String("JPG"))  &&
237              (destMimeType.toUpper() != QLatin1String("JPEG")) &&
238              (destMimeType.toUpper() != QLatin1String("JPE")))
239            )
240         {
241             // Non JPEG file, we update IPTC and XMP preview
242 
243             meta->setItemPreview(preview);
244         }
245 
246         if ((destMimeType.toUpper() == QLatin1String("TIFF")) ||
247             (destMimeType.toUpper() == QLatin1String("TIF")))
248         {
249             // With TIFF file, we don't store JPEG thumbnail, we even need to erase it and store
250             // a thumbnail at a special location. See bug #211758
251 
252             QImage thumb = preview.scaled(160, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation);
253             meta->setTiffThumbnail(thumb);
254         }
255         else
256         {
257             // Update Exif thumbnail.
258 
259             QImage thumb = preview.scaled(160, 120, Qt::KeepAspectRatio, Qt::SmoothTransformation);
260             meta->setExifThumbnail(thumb);
261         }
262     }
263 
264     // Update Exif Image dimensions.
265 
266     meta->setItemDimensions(size());
267 
268     // Update Exif Document Name tag with the original file name.
269 
270     if (!originalFileName.isEmpty())
271     {
272         meta->setExifTagString("Exif.Image.DocumentName", originalFileName);
273     }
274 
275     // Update Exif Orientation tag if necessary.
276 
277     if (flags & ResetExifOrientationTag)
278     {
279         meta->setItemOrientation(DMetadata::ORIENTATION_NORMAL);
280     }
281 
282     if (!m_priv->imageHistory.isEmpty())
283     {
284         DImageHistory forSaving(m_priv->imageHistory);
285         forSaving.adjustReferredImages();
286 
287         QUrl url         = QUrl::fromLocalFile(intendedDestPath);
288         QString filePath = url.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile() + QLatin1Char('/');
289         QString fileName = url.fileName();
290 
291         if (!filePath.isEmpty() && !fileName.isEmpty())
292         {
293             forSaving.purgePathFromReferredImages(filePath, fileName);
294         }
295 
296         QString imageHistoryXml = forSaving.toXml();
297         meta->setItemHistory(imageHistoryXml);
298     }
299 
300     if (flags & CreateNewImageHistoryUUID)
301     {
302         meta->setItemUniqueId(QString::fromUtf8(createImageUniqueId()));
303     }
304 
305     // Store new Exif/IPTC/XMP data into image.
306 
307     setMetadata(meta->data());
308 }
309 
createHistoryImageId(const QString & filePath,HistoryImageId::Type type)310 HistoryImageId DImg::createHistoryImageId(const QString& filePath, HistoryImageId::Type type)
311 {
312     QFileInfo fileInfo(filePath);
313 
314     if (!fileInfo.exists())
315     {
316         return HistoryImageId();
317     }
318 
319     QScopedPointer<DMetadata> metadata(new DMetadata(getMetadata()));
320     HistoryImageId id(metadata->getItemUniqueId());
321 
322     QDateTime dt = metadata->getItemDateTime();
323 
324     if (dt.isNull())
325     {
326         dt = creationDateFromFilesystem(fileInfo);
327     }
328 
329     id.setCreationDate(dt);
330     id.setFileName(fileInfo.fileName());
331     id.setPath(fileInfo.path());
332     id.setUniqueHash(QString::fromUtf8(getUniqueHashV2()), fileInfo.size());
333     id.setType(type);
334 
335     return id;
336 }
337 
addAsReferredImage(const QString & filePath,HistoryImageId::Type type)338 HistoryImageId DImg::addAsReferredImage(const QString& filePath, HistoryImageId::Type type)
339 {
340     HistoryImageId id = createHistoryImageId(filePath, type);
341     m_priv->imageHistory.purgePathFromReferredImages(id.path(), id.fileName());
342     addAsReferredImage(id);
343 
344     return id;
345 }
346 
addAsReferredImage(const HistoryImageId & id)347 void DImg::addAsReferredImage(const HistoryImageId& id)
348 {
349     m_priv->imageHistory << id;
350 }
351 
insertAsReferredImage(int afterHistoryStep,const HistoryImageId & id)352 void DImg::insertAsReferredImage(int afterHistoryStep, const HistoryImageId& id)
353 {
354     m_priv->imageHistory.insertReferredImage(afterHistoryStep, id);
355 }
356 
addCurrentUniqueImageId(const QString & uuid)357 void DImg::addCurrentUniqueImageId(const QString& uuid)
358 {
359     m_priv->imageHistory.adjustCurrentUuid(uuid);
360 }
361 
addFilterAction(const Digikam::FilterAction & action)362 void DImg::addFilterAction(const Digikam::FilterAction& action)
363 {
364     m_priv->imageHistory << action;
365 }
366 
getItemHistory() const367 const DImageHistory& DImg::getItemHistory() const
368 {
369     return m_priv->imageHistory;
370 }
371 
getItemHistory()372 DImageHistory& DImg::getItemHistory()
373 {
374     return m_priv->imageHistory;
375 }
376 
setItemHistory(const DImageHistory & history)377 void DImg::setItemHistory(const DImageHistory& history)
378 {
379     m_priv->imageHistory = history;
380 }
381 
hasImageHistory() const382 bool DImg::hasImageHistory() const
383 {
384     if (m_priv->imageHistory.isEmpty())
385     {
386         return false;
387     }
388     else
389     {
390         return true;
391     }
392 }
393 
getOriginalImageHistory() const394 DImageHistory DImg::getOriginalImageHistory() const
395 {
396     return attribute(QLatin1String("originalImageHistory")).value<DImageHistory>();
397 }
398 
setHistoryBranch(bool isBranch)399 void DImg::setHistoryBranch(bool isBranch)
400 {
401     setHistoryBranchAfter(getOriginalImageHistory(), isBranch);
402 }
403 
setHistoryBranchAfter(const DImageHistory & historyBeforeBranch,bool isBranch)404 void DImg::setHistoryBranchAfter(const DImageHistory& historyBeforeBranch, bool isBranch)
405 {
406     int addedSteps = m_priv->imageHistory.size() - historyBeforeBranch.size();
407     setHistoryBranchForLastSteps(addedSteps, isBranch);
408 }
409 
setHistoryBranchForLastSteps(int numberOfLastHistorySteps,bool isBranch)410 void DImg::setHistoryBranchForLastSteps(int numberOfLastHistorySteps, bool isBranch)
411 {
412     int firstStep = m_priv->imageHistory.size() - numberOfLastHistorySteps;
413 
414     if (firstStep < m_priv->imageHistory.size())
415     {
416         if (isBranch)
417         {
418             m_priv->imageHistory[firstStep].action.addFlag(FilterAction::ExplicitBranch);
419         }
420         else
421         {
422             m_priv->imageHistory[firstStep].action.removeFlag(FilterAction::ExplicitBranch);
423         }
424     }
425 }
426 
colorModelToString(COLORMODEL colorModel)427 QString DImg::colorModelToString(COLORMODEL colorModel)
428 {
429     switch (colorModel)
430     {
431         case RGB:
432         {
433             return i18nc("Color Model: RGB", "RGB");
434         }
435 
436         case GRAYSCALE:
437         {
438             return i18nc("Color Model: Grayscale", "Grayscale");
439         }
440 
441         case MONOCHROME:
442         {
443             return i18nc("Color Model: Monochrome", "Monochrome");
444         }
445 
446         case INDEXED:
447         {
448             return i18nc("Color Model: Indexed", "Indexed");
449         }
450 
451         case YCBCR:
452         {
453             return i18nc("Color Model: YCbCr", "YCbCr");
454         }
455 
456         case CMYK:
457         {
458             return i18nc("Color Model: CMYK", "CMYK");
459         }
460 
461         case CIELAB:
462         {
463             return i18nc("Color Model: CIE L*a*b*", "CIE L*a*b*");
464         }
465 
466         case COLORMODELRAW:
467         {
468             return i18nc("Color Model: Uncalibrated (RAW)", "Uncalibrated (RAW)");
469         }
470 
471         case COLORMODELUNKNOWN:
472         default:
473         {
474             return i18nc("Color Model: Unknown", "Unknown");
475         }
476     }
477 }
478 
isAnimatedImage(const QString & filePath)479 bool DImg::isAnimatedImage(const QString& filePath)
480 {
481     QImageReader reader(filePath);
482     reader.setDecideFormatFromContent(true);
483 
484     if (reader.supportsAnimation() &&
485        (reader.imageCount() > 1))
486     {
487         qCDebug(DIGIKAM_DIMG_LOG) << "File \"" << filePath << "\" is an animated image";
488         return true;
489     }
490 
491     return false;
492 }
493 
exifOrientation(const QString & filePath)494 int DImg::exifOrientation(const QString& filePath)
495 {
496     QVariant preview(attribute(QLatin1String("fromRawEmbeddedPreview")));
497 
498     return LoadSaveThread::exifOrientation(filePath,
499                                            DMetadata(getMetadata()),
500                                            (detectedFormat() == DImg::RAW),
501                                            (preview.isValid() && preview.toBool()));
502 }
503 
createUniqueHash(const QString & filePath,const QByteArray & ba)504 QByteArray DImg::createUniqueHash(const QString& filePath, const QByteArray& ba)
505 {
506     // Create the unique ID
507 
508     QCryptographicHash md5(QCryptographicHash::Md5);
509 
510     // First, read the Exif data into the hash
511 
512     md5.addData(ba);
513 
514     // Second, read in the first 8KB of the file
515 
516     QFile qfile(filePath);
517 
518     char databuf[8192];
519     QByteArray hash;
520 
521     if (qfile.open(QIODevice::Unbuffered | QIODevice::ReadOnly))
522     {
523         int readlen = 0;
524 
525         if ((readlen = qfile.read(databuf, 8192)) > 0)
526         {
527             QByteArray size;
528             md5.addData(databuf, readlen);
529             md5.addData(size.setNum(qfile.size()));
530             hash = md5.result().toHex();
531         }
532 
533         qfile.close();
534     }
535 
536     return hash;
537 }
538 
createUniqueHashV2(const QString & filePath)539 QByteArray DImg::createUniqueHashV2(const QString& filePath)
540 {
541     QFile file(filePath);
542 
543     if (!file.open(QIODevice::Unbuffered | QIODevice::ReadOnly))
544     {
545         return QByteArray();
546     }
547 
548     QCryptographicHash md5(QCryptographicHash::Md5);
549 
550     // Specified size: 100 kB; but limit to file size
551 
552     const qint64 specifiedSize = 100 * 1024; // 100 kB
553     qint64 size                = qMin(file.size(), specifiedSize);
554 
555     if (size)
556     {
557         QScopedArrayPointer<char> databuf(new char[size]);
558         int read;
559 
560         // Read first 100 kB
561 
562         if ((read = file.read(databuf.data(), size)) > 0)
563         {
564             md5.addData(databuf.data(), read);
565         }
566 
567         // Read last 100 kB
568 
569         file.seek(file.size() - size);
570 
571         if ((read = file.read(databuf.data(), size)) > 0)
572         {
573             md5.addData(databuf.data(), read);
574         }
575     }
576 
577     file.close();
578 
579     return md5.result().toHex();
580 }
581 
582 } // namespace Digikam
583