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