mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-06-03 17:08:08 -04:00
JXR: added support to EXIF metadata
Improved metadata support via EXIF metadata. Since JXR is based on a TIFF container, EXIF data is read directly from the file so it always works (even with versions of libjxr that don't have the metadata reading API). It also solves the following issues: - Incorrect date format on saved JXR files (was saved in ISO format instead of `yyyy:MM:dd HH:mm:ss`). - Incorrect date type setting in EXIF data: the `DateTime` tag should be updated on every save (verified by GIMP and Photoshop). Our `CreationDate` metadata is the equivalent of the EXIF `DateTimeOriginal` tag. Closes #22
This commit is contained in:
parent
90d4256f3d
commit
e5cf9caac5
@ -125,8 +125,9 @@ About the image:
|
|||||||
example a verbal description of the image.
|
example a verbal description of the image.
|
||||||
- `Copyright`: Copyright notice of the person or organization that claims
|
- `Copyright`: Copyright notice of the person or organization that claims
|
||||||
the copyright to the image.
|
the copyright to the image.
|
||||||
- `CreationDate`: Creation date and time in ISO 8601 format without
|
- `CreationDate`: When the image was created or captured. Date and time in
|
||||||
milliseconds (e.g. 2024-03-23T15:30:43).
|
ISO 8601 format without milliseconds (e.g. 2024-03-23T15:30:43). This value
|
||||||
|
should be kept unchanged when present.
|
||||||
- `Description`: A string that describes the subject of the image.
|
- `Description`: A string that describes the subject of the image.
|
||||||
- `DocumentName`: The name of the document from which this image was
|
- `DocumentName`: The name of the document from which this image was
|
||||||
scanned.
|
scanned.
|
||||||
@ -136,6 +137,9 @@ About the image:
|
|||||||
north of the equator (e.g. 27.717).
|
north of the equator (e.g. 27.717).
|
||||||
- `Longitude`: Floating-point number indicating the longitude in degrees
|
- `Longitude`: Floating-point number indicating the longitude in degrees
|
||||||
east of Greenwich (e.g. 85.317).
|
east of Greenwich (e.g. 85.317).
|
||||||
|
- `ModificationDate`: Last modification date and time in ISO 8601 format
|
||||||
|
without milliseconds (e.g. 2024-03-23T15:30:43). This value should be
|
||||||
|
updated every time the image is saved.
|
||||||
- `Owner`: Name of the owner of the image.
|
- `Owner`: Name of the owner of the image.
|
||||||
- `Software`: Name and version number of the software package(s) used to
|
- `Software`: Name and version number of the software package(s) used to
|
||||||
create the image.
|
create the image.
|
||||||
@ -189,6 +193,7 @@ are created automatically:
|
|||||||
- `Software`: Created using `applicationName` and `applicationVersion` methods
|
- `Software`: Created using `applicationName` and `applicationVersion` methods
|
||||||
of [`QCoreApplication`](https://doc.qt.io/qt-6/qcoreapplication.html).
|
of [`QCoreApplication`](https://doc.qt.io/qt-6/qcoreapplication.html).
|
||||||
- `CreationDate`: Set to current time and date.
|
- `CreationDate`: Set to current time and date.
|
||||||
|
- `ModificationDate`: Set to current time and date.
|
||||||
|
|
||||||
### ICC profile support
|
### ICC profile support
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"fileName" : "metadata.png",
|
"fileName" : "metadata.png",
|
||||||
"metadata" : [
|
"metadata" : [
|
||||||
{
|
{
|
||||||
"key" : "CreationDate",
|
"key" : "ModificationDate",
|
||||||
"value" : "2025-02-19T08:27:22+01:00"
|
"value" : "2025-02-19T08:27:22+01:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"fileName" : "gimp_exif.png",
|
"fileName" : "gimp_exif.png",
|
||||||
"metadata" : [
|
"metadata" : [
|
||||||
{
|
{
|
||||||
"key" : "CreationDate",
|
"key" : "ModificationDate",
|
||||||
"value" : "2025-01-05T10:18:16"
|
"value" : "2025-01-05T10:18:16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
BIN
autotests/read/jxr/metadata.jxr
Normal file
BIN
autotests/read/jxr/metadata.jxr
Normal file
Binary file not shown.
59
autotests/read/jxr/metadata.jxr.json
Normal file
59
autotests/read/jxr/metadata.jxr.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"fileName" : "metadata.png",
|
||||||
|
"metadata" : [
|
||||||
|
{
|
||||||
|
"key" : "CreationDate",
|
||||||
|
"value" : "2025-01-14T10:34:51Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Software" ,
|
||||||
|
"value" : "LIFE Pro 2.18.10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Altitude",
|
||||||
|
"value" : "34"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Author",
|
||||||
|
"value" : "KDE Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Copyright",
|
||||||
|
"value" : "@2025 KDE Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Description",
|
||||||
|
"value" : "TV broadcast test image."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Latitude",
|
||||||
|
"value" : "44.6478"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "LensManufacturer",
|
||||||
|
"value" : "KDE Glasses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "LensModel",
|
||||||
|
"value" : "A1234"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Longitude",
|
||||||
|
"value" : "10.9254"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Manufacturer",
|
||||||
|
"value" : "KFramework"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Model",
|
||||||
|
"value" : "KImageFormats"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution" : {
|
||||||
|
"dotsPerMeterX" : 11811,
|
||||||
|
"dotsPerMeterY" : 5906
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
BIN
autotests/read/jxr/metadata.png
Normal file
BIN
autotests/read/jxr/metadata.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"metadata" : [
|
"metadata" : [
|
||||||
{
|
{
|
||||||
"key" : "CreationDate",
|
"key" : "ModificationDate",
|
||||||
"value" : "2022-11-11T14:27:52"
|
"value" : "2022-11-11T14:27:52"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"metadata" : [
|
"metadata" : [
|
||||||
{
|
{
|
||||||
"key" : "CreationDate",
|
"key" : "ModificationDate",
|
||||||
"value" : "2022-11-11T14:27:39"
|
"value" : "2022-11-11T14:27:39"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"fileName" : "metadata.png",
|
"fileName" : "metadata.png",
|
||||||
"metadata" : [
|
"metadata" : [
|
||||||
{
|
{
|
||||||
"key" : "CreationDate",
|
"key" : "ModificationDate",
|
||||||
"value" : "2025-01-14T13:53:32+01:00"
|
"value" : "2025-01-14T13:53:32+01:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
"key" : "CreationDate",
|
"key" : "CreationDate",
|
||||||
"value" : "2025-01-14T13:53:32+01:00"
|
"value" : "2025-01-14T13:53:32+01:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key" : "ModificationDate",
|
||||||
|
"value" : "2025-02-14T15:58:44+01:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key" : "Software" ,
|
"key" : "Software" ,
|
||||||
"value" : "Adobe Photoshop 26.2 (Windows)"
|
"value" : "Adobe Photoshop 26.2 (Windows)"
|
||||||
|
@ -5,6 +5,10 @@
|
|||||||
"key" : "CreationDate",
|
"key" : "CreationDate",
|
||||||
"value" : "2025-01-14T13:53:32+01:00"
|
"value" : "2025-01-14T13:53:32+01:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key" : "ModificationDate",
|
||||||
|
"value" : "2025-02-14T15:58:44+01:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key" : "Software" ,
|
"key" : "Software" ,
|
||||||
"value" : "Adobe Photoshop 26.2 (Windows)"
|
"value" : "Adobe Photoshop 26.2 (Windows)"
|
||||||
|
@ -5,6 +5,14 @@
|
|||||||
"key" : "CreationDate",
|
"key" : "CreationDate",
|
||||||
"value" : "2025-01-14T13:53:32+01:00"
|
"value" : "2025-01-14T13:53:32+01:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key" : "ModificationDate",
|
||||||
|
"value" : "2025-02-14T15:58:44+01:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Altitude",
|
||||||
|
"value" : "34"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key" : "Author",
|
"key" : "Author",
|
||||||
"value" : "KDE Project"
|
"value" : "KDE Project"
|
||||||
@ -17,6 +25,22 @@
|
|||||||
"key" : "Description",
|
"key" : "Description",
|
||||||
"value" : "TV broadcast test image."
|
"value" : "TV broadcast test image."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key" : "Latitude",
|
||||||
|
"value" : "44.6478"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "LensManufacturer",
|
||||||
|
"value" : "KDE Glasses"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "LensModel",
|
||||||
|
"value" : "A1234"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Longitude",
|
||||||
|
"value" : "10.9254"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key" : "Manufacturer",
|
"key" : "Manufacturer",
|
||||||
"value" : "KFramework"
|
"value" : "KFramework"
|
||||||
|
@ -140,7 +140,7 @@ endif()
|
|||||||
##################################
|
##################################
|
||||||
|
|
||||||
if (LibJXR_FOUND)
|
if (LibJXR_FOUND)
|
||||||
kimageformats_add_plugin(kimg_jxr SOURCES jxr.cpp)
|
kimageformats_add_plugin(kimg_jxr SOURCES jxr.cpp microexif.cpp)
|
||||||
kde_enable_exceptions()
|
kde_enable_exceptions()
|
||||||
target_include_directories(kimg_jxr PRIVATE ${LIBJXR_INCLUDE_DIRS})
|
target_include_directories(kimg_jxr PRIVATE ${LIBJXR_INCLUDE_DIRS})
|
||||||
target_link_libraries(kimg_jxr PRIVATE ${LIBJXR_LIBRARIES})
|
target_link_libraries(kimg_jxr PRIVATE ${LIBJXR_LIBRARIES})
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "jxr_p.h"
|
#include "jxr_p.h"
|
||||||
|
#include "microexif_p.h"
|
||||||
#include "util_p.h"
|
#include "util_p.h"
|
||||||
|
|
||||||
#include <QColorSpace>
|
#include <QColorSpace>
|
||||||
@ -83,9 +84,10 @@ Q_LOGGING_CATEGORY(LOG_JXRPLUGIN, "kf.imageformats.plugins.jxr", QtWarningMsg)
|
|||||||
class JXRHandlerPrivate : public QSharedData
|
class JXRHandlerPrivate : public QSharedData
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
QSharedPointer<QTemporaryDir> tempDir;
|
QSharedPointer<QTemporaryDir> m_tempDir;
|
||||||
mutable QSharedPointer<QFile> jxrFile;
|
QSharedPointer<QFile> m_jxrFile;
|
||||||
mutable QHash<QString, QString> txtMeta;
|
MicroExif m_exif;
|
||||||
|
mutable QHash<QString, QString> m_txtMeta;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PKFactory *pFactory = nullptr;
|
PKFactory *pFactory = nullptr;
|
||||||
@ -95,7 +97,7 @@ public:
|
|||||||
|
|
||||||
JXRHandlerPrivate()
|
JXRHandlerPrivate()
|
||||||
{
|
{
|
||||||
tempDir = QSharedPointer<QTemporaryDir>(new QTemporaryDir);
|
m_tempDir = QSharedPointer<QTemporaryDir>(new QTemporaryDir);
|
||||||
if (PKCreateFactory(&pFactory, PK_SDK_VERSION) == WMP_errSuccess) {
|
if (PKCreateFactory(&pFactory, PK_SDK_VERSION) == WMP_errSuccess) {
|
||||||
PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION);
|
PKCreateCodecFactory(&pCodecFactory, WMP_SDK_VERSION);
|
||||||
}
|
}
|
||||||
@ -123,7 +125,7 @@ public:
|
|||||||
|
|
||||||
QString fileName() const
|
QString fileName() const
|
||||||
{
|
{
|
||||||
return jxrFile->fileName();
|
return m_jxrFile->fileName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* *** READ *** */
|
/* *** READ *** */
|
||||||
@ -318,11 +320,20 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief setTextMetadata
|
* \brief exifData
|
||||||
* Set the text metadata into \a image
|
* \return The EXIF data.
|
||||||
|
*/
|
||||||
|
MicroExif exifData() const
|
||||||
|
{
|
||||||
|
return m_exif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief setMetadata
|
||||||
|
* Set the metadata into \a image
|
||||||
* \param image Image on which to write metadata
|
* \param image Image on which to write metadata
|
||||||
*/
|
*/
|
||||||
void setTextMetadata(QImage& image)
|
void setMetadata(QImage& image)
|
||||||
{
|
{
|
||||||
auto xmp = xmpData();
|
auto xmp = xmpData();
|
||||||
if (!xmp.isEmpty()) {
|
if (!xmp.isEmpty()) {
|
||||||
@ -344,10 +355,6 @@ public:
|
|||||||
if (!model.isEmpty()) {
|
if (!model.isEmpty()) {
|
||||||
image.setText(QStringLiteral(META_KEY_MODEL), model);
|
image.setText(QStringLiteral(META_KEY_MODEL), model);
|
||||||
}
|
}
|
||||||
auto cDate = dateTime();
|
|
||||||
if (!cDate.isEmpty()) {
|
|
||||||
image.setText(QStringLiteral(META_KEY_CREATIONDATE), cDate);
|
|
||||||
}
|
|
||||||
auto author = artist();
|
auto author = artist();
|
||||||
if (!author.isEmpty()) {
|
if (!author.isEmpty()) {
|
||||||
image.setText(QStringLiteral(META_KEY_AUTHOR), author);
|
image.setText(QStringLiteral(META_KEY_AUTHOR), author);
|
||||||
@ -368,20 +375,23 @@ public:
|
|||||||
if (!docn.isEmpty()) {
|
if (!docn.isEmpty()) {
|
||||||
image.setText(QStringLiteral(META_KEY_DOCUMENTNAME), docn);
|
image.setText(QStringLiteral(META_KEY_DOCUMENTNAME), docn);
|
||||||
}
|
}
|
||||||
|
auto exif = exifData();
|
||||||
|
if (!exif.isEmpty()) {
|
||||||
|
exif.toImageMetadata(image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#define META_TEXT(name, key) \
|
#define META_TEXT(name, key) \
|
||||||
QString name() const \
|
QString name() const \
|
||||||
{ \
|
{ \
|
||||||
readTextMeta(); \
|
readTextMeta(); \
|
||||||
return txtMeta.value(QStringLiteral(key)); \
|
return m_txtMeta.value(QStringLiteral(key)); \
|
||||||
}
|
}
|
||||||
|
|
||||||
META_TEXT(description, META_KEY_DESCRIPTION)
|
META_TEXT(description, META_KEY_DESCRIPTION)
|
||||||
META_TEXT(cameraMake, META_KEY_MANUFACTURER)
|
META_TEXT(cameraMake, META_KEY_MANUFACTURER)
|
||||||
META_TEXT(cameraModel, META_KEY_MODEL)
|
META_TEXT(cameraModel, META_KEY_MODEL)
|
||||||
META_TEXT(software, META_KEY_SOFTWARE)
|
META_TEXT(software, META_KEY_SOFTWARE)
|
||||||
META_TEXT(dateTime, META_KEY_CREATIONDATE)
|
|
||||||
META_TEXT(artist, META_KEY_AUTHOR)
|
META_TEXT(artist, META_KEY_AUTHOR)
|
||||||
META_TEXT(copyright, META_KEY_COPYRIGHT)
|
META_TEXT(copyright, META_KEY_COPYRIGHT)
|
||||||
META_TEXT(caption, META_KEY_TITLE)
|
META_TEXT(caption, META_KEY_TITLE)
|
||||||
@ -400,9 +410,9 @@ public:
|
|||||||
bool initForWriting()
|
bool initForWriting()
|
||||||
{
|
{
|
||||||
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
|
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
|
||||||
auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
|
auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
|
||||||
QSharedPointer<QFile> file(new QFile(fileName));
|
QSharedPointer<QFile> file(new QFile(fileName));
|
||||||
jxrFile = file;
|
m_jxrFile = file;
|
||||||
return initEncoder();
|
return initEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +432,7 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deviceCopy(device, jxrFile.data())) {
|
if (!deviceCopy(device, m_jxrFile.data())) {
|
||||||
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while writing in the target device";
|
qCWarning(LOG_JXRPLUGIN) << "JXRHandlerPrivate::finalizeWriting() error while writing in the target device";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -580,7 +590,6 @@ public:
|
|||||||
META_CTEXT(META_KEY_MODEL, pvarCameraModel)
|
META_CTEXT(META_KEY_MODEL, pvarCameraModel)
|
||||||
META_CTEXT(META_KEY_AUTHOR, pvarArtist)
|
META_CTEXT(META_KEY_AUTHOR, pvarArtist)
|
||||||
META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright)
|
META_CTEXT(META_KEY_COPYRIGHT, pvarCopyright)
|
||||||
META_CTEXT(META_KEY_CREATIONDATE, pvarDateTime)
|
|
||||||
META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
|
META_CTEXT(META_KEY_DOCUMENTNAME, pvarDocumentName)
|
||||||
META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
|
META_CTEXT(META_KEY_HOSTCOMPUTER, pvarHostComputer)
|
||||||
META_WTEXT(META_KEY_TITLE, pvarCaption)
|
META_WTEXT(META_KEY_TITLE, pvarCaption)
|
||||||
@ -595,12 +604,33 @@ public:
|
|||||||
meta.pvarSoftware.VT.pszVal = software.data();
|
meta.pvarSoftware.VT.pszVal = software.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Date and Time (TIFF format)
|
||||||
|
auto cDate = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
|
||||||
|
auto sDate = cDate.isValid() ? cDate.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")).toLatin1() : QByteArray();
|
||||||
|
if (!sDate.isEmpty()) {
|
||||||
|
meta.pvarDateTime.vt = DPKVT_LPSTR;
|
||||||
|
meta.pvarDateTime.VT.pszVal = sDate.data();
|
||||||
|
}
|
||||||
|
|
||||||
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
|
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
|
||||||
if (!xmp.isNull()) {
|
if (!xmp.isNull()) {
|
||||||
if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<quint8 *>(xmp.data()), xmp.size())) {
|
if (auto err = PKImageEncode_SetXMPMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(xmp.constData()), xmp.size())) {
|
||||||
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err;
|
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting XMP data:" << err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto exif = MicroExif::fromImage(image);
|
||||||
|
if (!exif.isEmpty()) {
|
||||||
|
auto exifIfd = exif.exifIfdByteArray(QDataStream::LittleEndian);
|
||||||
|
if (auto err = PKImageEncode_SetEXIFMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(exifIfd.constData()), exifIfd.size())) {
|
||||||
|
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting EXIF data:" << err;
|
||||||
|
}
|
||||||
|
auto gpsIfd = exif.gpsIfdByteArray(QDataStream::LittleEndian);
|
||||||
|
if (auto err = PKImageEncode_SetGPSInfoMetadata_WMP(pEncoder, reinterpret_cast<const quint8 *>(gpsIfd.constData()), gpsIfd.size())) {
|
||||||
|
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting GPS data:" << err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) {
|
if (auto err = pEncoder->SetDescriptiveMetadata(pEncoder, &meta)) {
|
||||||
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err;
|
qCWarning(LOG_JXRPLUGIN) << "JXRHandler::write() error while setting descriptive data:" << err;
|
||||||
}
|
}
|
||||||
@ -710,11 +740,11 @@ private:
|
|||||||
if (device == nullptr) {
|
if (device == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!jxrFile.isNull()) {
|
if (!m_jxrFile.isNull()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
|
// I have to use QFile because, on Windows, the QTemporary file is locked (even if I close it)
|
||||||
auto fileName = QStringLiteral("%1.jxr").arg(tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
|
auto fileName = QStringLiteral("%1.jxr").arg(m_tempDir->filePath(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8)));
|
||||||
QSharedPointer<QFile> file(new QFile(fileName));
|
QSharedPointer<QFile> file(new QFile(fileName));
|
||||||
if (!file->open(QFile::WriteOnly)) {
|
if (!file->open(QFile::WriteOnly)) {
|
||||||
return false;
|
return false;
|
||||||
@ -724,7 +754,8 @@ private:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
file->close();
|
file->close();
|
||||||
jxrFile = file;
|
m_exif = MicroExif::fromDevice(file.data());
|
||||||
|
m_jxrFile = file;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -762,7 +793,7 @@ private:
|
|||||||
if (pDecoder == nullptr) {
|
if (pDecoder == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!txtMeta.isEmpty()) {
|
if (!m_txtMeta.isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -773,15 +804,14 @@ private:
|
|||||||
|
|
||||||
#define META_TEXT(name, field) \
|
#define META_TEXT(name, field) \
|
||||||
if (meta.field.vt == DPKVT_LPSTR) \
|
if (meta.field.vt == DPKVT_LPSTR) \
|
||||||
txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \
|
m_txtMeta.insert(QStringLiteral(name), QString::fromUtf8(meta.field.VT.pszVal)); \
|
||||||
else if (meta.field.vt == DPKVT_LPWSTR) \
|
else if (meta.field.vt == DPKVT_LPWSTR) \
|
||||||
txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));
|
m_txtMeta.insert(QStringLiteral(name), QString::fromUtf16(reinterpret_cast<char16_t *>(meta.field.VT.pwszVal)));
|
||||||
|
|
||||||
META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription)
|
META_TEXT(META_KEY_DESCRIPTION, pvarImageDescription)
|
||||||
META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake)
|
META_TEXT(META_KEY_MANUFACTURER, pvarCameraMake)
|
||||||
META_TEXT(META_KEY_MODEL, pvarCameraModel)
|
META_TEXT(META_KEY_MODEL, pvarCameraModel)
|
||||||
META_TEXT(META_KEY_SOFTWARE, pvarSoftware)
|
META_TEXT(META_KEY_SOFTWARE, pvarSoftware)
|
||||||
META_TEXT(META_KEY_CREATIONDATE, pvarDateTime)
|
|
||||||
META_TEXT(META_KEY_AUTHOR, pvarArtist)
|
META_TEXT(META_KEY_AUTHOR, pvarArtist)
|
||||||
META_TEXT(META_KEY_COPYRIGHT, pvarCopyright)
|
META_TEXT(META_KEY_COPYRIGHT, pvarCopyright)
|
||||||
META_TEXT(META_KEY_TITLE, pvarCaption)
|
META_TEXT(META_KEY_TITLE, pvarCaption)
|
||||||
@ -867,7 +897,7 @@ bool JXRHandler::read(QImage *outImage)
|
|||||||
|
|
||||||
// Metadata (e.g.: icc profile, description, etc...)
|
// Metadata (e.g.: icc profile, description, etc...)
|
||||||
img.setColorSpace(d->colorSpace());
|
img.setColorSpace(d->colorSpace());
|
||||||
d->setTextMetadata(img);
|
d->setMetadata(img);
|
||||||
|
|
||||||
#ifndef JXR_DENY_FLOAT_IMAGE
|
#ifndef JXR_DENY_FLOAT_IMAGE
|
||||||
// JXR float are stored in scRGB.
|
// JXR float are stored in scRGB.
|
||||||
|
@ -37,7 +37,12 @@
|
|||||||
// EXIF 3 specs
|
// EXIF 3 specs
|
||||||
#define EXIF_EXIFIFD 0x8769
|
#define EXIF_EXIFIFD 0x8769
|
||||||
#define EXIF_GPSIFD 0x8825
|
#define EXIF_GPSIFD 0x8825
|
||||||
|
#define EXIF_EXIFVERSION 0x9000
|
||||||
|
#define EXIF_DATETIMEORIGINAL 0x9003
|
||||||
|
#define EXIF_DATETIMEDIGITIZED 0x9004
|
||||||
#define EXIF_OFFSETTIME 0x9010
|
#define EXIF_OFFSETTIME 0x9010
|
||||||
|
#define EXIF_OFFSETTIMEORIGINAL 0x9011
|
||||||
|
#define EXIF_OFFSETTIMEDIGITIZED 0x9012
|
||||||
#define EXIF_COLORSPACE 0xA001
|
#define EXIF_COLORSPACE 0xA001
|
||||||
#define EXIF_PIXELXDIM 0xA002
|
#define EXIF_PIXELXDIM 0xA002
|
||||||
#define EXIF_PIXELYDIM 0xA003
|
#define EXIF_PIXELYDIM 0xA003
|
||||||
@ -47,7 +52,6 @@
|
|||||||
#define EXIF_LENSMODEL 0xA434
|
#define EXIF_LENSMODEL 0xA434
|
||||||
#define EXIF_LENSSERIALNUMBER 0xA435
|
#define EXIF_LENSSERIALNUMBER 0xA435
|
||||||
#define EXIF_IMAGETITLE 0xA436
|
#define EXIF_IMAGETITLE 0xA436
|
||||||
#define EXIF_EXIFVERSION 0x9000
|
|
||||||
|
|
||||||
#define EXIF_VAL_COLORSPACE_SRGB 1
|
#define EXIF_VAL_COLORSPACE_SRGB 1
|
||||||
#define EXIF_VAL_COLORSPACE_UNCAL 0xFFFF
|
#define EXIF_VAL_COLORSPACE_UNCAL 0xFFFF
|
||||||
@ -119,7 +123,11 @@ static const KnownTags staticTagTypes = {
|
|||||||
TagInfo(TIFF_COPYRIGHT, ExifTagType::Ascii),
|
TagInfo(TIFF_COPYRIGHT, ExifTagType::Ascii),
|
||||||
TagInfo(EXIF_EXIFIFD, ExifTagType::Long),
|
TagInfo(EXIF_EXIFIFD, ExifTagType::Long),
|
||||||
TagInfo(EXIF_GPSIFD, ExifTagType::Long),
|
TagInfo(EXIF_GPSIFD, ExifTagType::Long),
|
||||||
|
TagInfo(EXIF_DATETIMEORIGINAL, ExifTagType::Ascii),
|
||||||
|
TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii),
|
||||||
TagInfo(EXIF_OFFSETTIME, ExifTagType::Ascii),
|
TagInfo(EXIF_OFFSETTIME, ExifTagType::Ascii),
|
||||||
|
TagInfo(EXIF_OFFSETTIMEORIGINAL, ExifTagType::Ascii),
|
||||||
|
TagInfo(EXIF_OFFSETTIMEDIGITIZED, ExifTagType::Ascii),
|
||||||
TagInfo(EXIF_COLORSPACE, ExifTagType::Short),
|
TagInfo(EXIF_COLORSPACE, ExifTagType::Short),
|
||||||
TagInfo(EXIF_PIXELXDIM, ExifTagType::Long),
|
TagInfo(EXIF_PIXELXDIM, ExifTagType::Long),
|
||||||
TagInfo(EXIF_PIXELYDIM, ExifTagType::Long),
|
TagInfo(EXIF_PIXELYDIM, ExifTagType::Long),
|
||||||
@ -230,8 +238,8 @@ static bool checkHeader(QDataStream &ds)
|
|||||||
|
|
||||||
quint16 version;
|
quint16 version;
|
||||||
ds >> version;
|
ds >> version;
|
||||||
if (version != 0x2A)
|
if (version != 0x002A && version != 0x01BC)
|
||||||
return false;
|
return false; // not TIFF or JXR
|
||||||
|
|
||||||
quint32 offset;
|
quint32 offset;
|
||||||
ds >> offset;
|
ds >> offset;
|
||||||
@ -852,6 +860,46 @@ void MicroExif::setDateTime(const QDateTime &dt)
|
|||||||
setExifString(EXIF_OFFSETTIME, timeOffset(dt.offsetFromUtc() / 60));
|
setExifString(EXIF_OFFSETTIME, timeOffset(dt.offsetFromUtc() / 60));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QDateTime MicroExif::dateTimeOriginal() const
|
||||||
|
{
|
||||||
|
auto dt = QDateTime::fromString(exifString(EXIF_DATETIMEORIGINAL), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
|
||||||
|
auto ofTag = exifString(EXIF_OFFSETTIMEORIGINAL);
|
||||||
|
if (dt.isValid() && !ofTag.isEmpty())
|
||||||
|
dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
|
||||||
|
return(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MicroExif::setDateTimeOriginal(const QDateTime &dt)
|
||||||
|
{
|
||||||
|
if (!dt.isValid()) {
|
||||||
|
m_exifTags.remove(EXIF_DATETIMEORIGINAL);
|
||||||
|
m_exifTags.remove(EXIF_OFFSETTIMEORIGINAL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExifString(EXIF_DATETIMEORIGINAL, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
|
||||||
|
setExifString(EXIF_OFFSETTIMEORIGINAL, timeOffset(dt.offsetFromUtc() / 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
QDateTime MicroExif::dateTimeDigitized() const
|
||||||
|
{
|
||||||
|
auto dt = QDateTime::fromString(exifString(EXIF_DATETIMEDIGITIZED), QStringLiteral("yyyy:MM:dd HH:mm:ss"));
|
||||||
|
auto ofTag = exifString(EXIF_OFFSETTIMEDIGITIZED);
|
||||||
|
if (dt.isValid() && !ofTag.isEmpty())
|
||||||
|
dt.setTimeZone(QTimeZone::fromSecondsAheadOfUtc(timeOffset(ofTag) * 60));
|
||||||
|
return(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MicroExif::setDateTimeDigitized(const QDateTime &dt)
|
||||||
|
{
|
||||||
|
if (!dt.isValid()) {
|
||||||
|
m_exifTags.remove(EXIF_DATETIMEDIGITIZED);
|
||||||
|
m_exifTags.remove(EXIF_OFFSETTIMEDIGITIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExifString(EXIF_DATETIMEDIGITIZED, dt.toString(QStringLiteral("yyyy:MM:dd HH:mm:ss")));
|
||||||
|
setExifString(EXIF_OFFSETTIMEDIGITIZED, timeOffset(dt.offsetFromUtc() / 60));
|
||||||
|
}
|
||||||
|
|
||||||
QString MicroExif::title() const
|
QString MicroExif::title() const
|
||||||
{
|
{
|
||||||
return exifString(EXIF_IMAGETITLE);
|
return exifString(EXIF_IMAGETITLE);
|
||||||
@ -956,6 +1004,50 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder) const
|
|||||||
return ba;
|
return ba;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QByteArray MicroExif::exifIfdByteArray(const QDataStream::ByteOrder &byteOrder) const
|
||||||
|
{
|
||||||
|
QByteArray ba;
|
||||||
|
{
|
||||||
|
QDataStream ds(&ba, QIODevice::WriteOnly);
|
||||||
|
ds.setByteOrder(byteOrder);
|
||||||
|
auto exifTags = m_exifTags;
|
||||||
|
exifTags.insert(EXIF_EXIFVERSION, QByteArray("0300"));
|
||||||
|
TagPos positions;
|
||||||
|
if (!writeIfd(ds, exifTags, positions))
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return ba;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MicroExif::setExifIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder)
|
||||||
|
{
|
||||||
|
QDataStream ds(ba);
|
||||||
|
ds.setByteOrder(byteOrder);
|
||||||
|
return readIfd(ds, m_exifTags, 0, staticTagTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray MicroExif::gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder) const
|
||||||
|
{
|
||||||
|
QByteArray ba;
|
||||||
|
{
|
||||||
|
QDataStream ds(&ba, QIODevice::WriteOnly);
|
||||||
|
ds.setByteOrder(byteOrder);
|
||||||
|
auto gpsTags = m_gpsTags;
|
||||||
|
gpsTags.insert(GPS_GPSVERSION, QByteArray("2400"));
|
||||||
|
TagPos positions;
|
||||||
|
if (!writeIfd(ds, gpsTags, positions, 0, staticGpsTagTypes))
|
||||||
|
return {};
|
||||||
|
return ba;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MicroExif::setGpsIfdByteArray(const QByteArray &ba, const QDataStream::ByteOrder &byteOrder)
|
||||||
|
{
|
||||||
|
QDataStream ds(ba);
|
||||||
|
ds.setByteOrder(byteOrder);
|
||||||
|
return readIfd(ds, m_gpsTags, 0, staticGpsTagTypes);
|
||||||
|
}
|
||||||
|
|
||||||
bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder) const
|
bool MicroExif::write(QIODevice *device, const QDataStream::ByteOrder &byteOrder) const
|
||||||
{
|
{
|
||||||
if (device == nullptr || device->isSequential() || isEmpty())
|
if (device == nullptr || device->isSequential() || isEmpty())
|
||||||
@ -993,8 +1085,13 @@ void MicroExif::toImageMetadata(QImage &targetImage, bool replaceExisting) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set date and time
|
// set date and time
|
||||||
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_CREATIONDATE)).isEmpty()) {
|
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_MODIFICATIONDATE)).isEmpty()) {
|
||||||
auto dt = dateTime();
|
auto dt = dateTime();
|
||||||
|
if (dt.isValid())
|
||||||
|
targetImage.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), dt.toString(Qt::ISODate));
|
||||||
|
}
|
||||||
|
if (replaceExisting || targetImage.text(QStringLiteral(META_KEY_CREATIONDATE)).isEmpty()) {
|
||||||
|
auto dt = dateTimeOriginal();
|
||||||
if (dt.isValid())
|
if (dt.isValid())
|
||||||
targetImage.setText(QStringLiteral(META_KEY_CREATIONDATE), dt.toString(Qt::ISODate));
|
targetImage.setText(QStringLiteral(META_KEY_CREATIONDATE), dt.toString(Qt::ISODate));
|
||||||
}
|
}
|
||||||
@ -1095,12 +1192,18 @@ MicroExif MicroExif::fromImage(const QImage &image)
|
|||||||
exif.setSoftware(sw.trimmed());
|
exif.setSoftware(sw.trimmed());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TIFF Creation date and time
|
// TIFF date and time
|
||||||
auto dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_CREATIONDATE)), Qt::ISODate);
|
auto dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_MODIFICATIONDATE)), Qt::ISODate);
|
||||||
if (!dt.isValid())
|
if (!dt.isValid())
|
||||||
dt = QDateTime::currentDateTime();
|
dt = QDateTime::currentDateTime();
|
||||||
exif.setDateTime(dt);
|
exif.setDateTime(dt);
|
||||||
|
|
||||||
|
// EXIF original date and time
|
||||||
|
dt = QDateTime::fromString(image.text(QStringLiteral(META_KEY_CREATIONDATE)), Qt::ISODate);
|
||||||
|
if (!dt.isValid())
|
||||||
|
dt = QDateTime::currentDateTime();
|
||||||
|
exif.setDateTimeOriginal(dt);
|
||||||
|
|
||||||
// GPS Info
|
// GPS Info
|
||||||
auto ok = false;
|
auto ok = false;
|
||||||
auto alt = image.text(QStringLiteral(META_KEY_ALTITUDE)).toDouble(&ok);
|
auto alt = image.text(QStringLiteral(META_KEY_ALTITUDE)).toDouble(&ok);
|
||||||
|
@ -201,6 +201,20 @@ public:
|
|||||||
QDateTime dateTime() const;
|
QDateTime dateTime() const;
|
||||||
void setDateTime(const QDateTime& dt);
|
void setDateTime(const QDateTime& dt);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief dateTimeOriginal
|
||||||
|
* \return The date and time when the original image data was generated.
|
||||||
|
*/
|
||||||
|
QDateTime dateTimeOriginal() const;
|
||||||
|
void setDateTimeOriginal(const QDateTime& dt);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief dateTimeDigitized
|
||||||
|
* \return The date and time when the image was stored as digital data.
|
||||||
|
*/
|
||||||
|
QDateTime dateTimeDigitized() const;
|
||||||
|
void setDateTimeDigitized(const QDateTime& dt);
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief title
|
* \brief title
|
||||||
* \return The title of the image.
|
* \return The title of the image.
|
||||||
@ -239,17 +253,58 @@ public:
|
|||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief toByteArray
|
* \brief toByteArray
|
||||||
|
* Converts the class to RAW data. The raw data contains:
|
||||||
|
* - TIFF header
|
||||||
|
* - MAIN IFD
|
||||||
|
* - EXIF IFD
|
||||||
|
* - GPS IFD
|
||||||
* \param byteOrder Sets the serialization byte order for EXIF data.
|
* \param byteOrder Sets the serialization byte order for EXIF data.
|
||||||
* \return A byte array containing the serialized data.
|
* \return A byte array containing the serialized data.
|
||||||
|
* \sa write
|
||||||
*/
|
*/
|
||||||
QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief exifIfdByteArray
|
||||||
|
* Convert the EXIF IFD only to RAW data. Useful when you want to add EXIF data to an existing TIFF container.
|
||||||
|
* \param byteOrder Sets the serialization byte order for the data.
|
||||||
|
* \return A byte array containing the serialized data.
|
||||||
|
*/
|
||||||
|
QByteArray exifIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
||||||
|
/*!
|
||||||
|
* \brief setExifIfdByteArray
|
||||||
|
* \param ba The RAW data of EXIF IFD.
|
||||||
|
* \param byteOrder Sets the serialization byte order of the data.
|
||||||
|
* \return True on success, otherwise false.
|
||||||
|
*/
|
||||||
|
bool setExifIfdByteArray(const QByteArray& ba, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER);
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief gpsIfdByteArray
|
||||||
|
* Convert the GPS IFD only to RAW data. Useful when you want to add GPS data to an existing TIFF container.
|
||||||
|
* \param byteOrder Sets the serialization byte order for the data.
|
||||||
|
* \return A byte array containing the serialized data.
|
||||||
|
*/
|
||||||
|
QByteArray gpsIfdByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
||||||
|
/*!
|
||||||
|
* \brief setGpsIfdByteArray
|
||||||
|
* \param ba The RAW data of GPS IFD.
|
||||||
|
* \param byteOrder Sets the serialization byte order of the data.
|
||||||
|
* \return True on success, otherwise false.
|
||||||
|
*/
|
||||||
|
bool setGpsIfdByteArray(const QByteArray& ba, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER);
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief write
|
* \brief write
|
||||||
* Serialize the class on a device.
|
* Serialize the class on a device. The serialized data contains:
|
||||||
|
* - TIFF header
|
||||||
|
* - MAIN IFD
|
||||||
|
* - EXIF IFD
|
||||||
|
* - GPS IFD
|
||||||
* \param device A random access device.
|
* \param device A random access device.
|
||||||
* \param byteOrder Sets the serialization byte order for EXIF data.
|
* \param byteOrder Sets the serialization byte order for EXIF data.
|
||||||
* \return True on success, otherwise false.
|
* \return True on success, otherwise false.
|
||||||
|
* \sa toByteArray
|
||||||
*/
|
*/
|
||||||
bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
#define META_KEY_HOSTCOMPUTER "HostComputer"
|
#define META_KEY_HOSTCOMPUTER "HostComputer"
|
||||||
#define META_KEY_LATITUDE "Latitude"
|
#define META_KEY_LATITUDE "Latitude"
|
||||||
#define META_KEY_LONGITUDE "Longitude"
|
#define META_KEY_LONGITUDE "Longitude"
|
||||||
|
#define META_KEY_MODIFICATIONDATE "ModificationDate"
|
||||||
#define META_KEY_OWNER "Owner"
|
#define META_KEY_OWNER "Owner"
|
||||||
#define META_KEY_SOFTWARE "Software"
|
#define META_KEY_SOFTWARE "Software"
|
||||||
#define META_KEY_TITLE "Title"
|
#define META_KEY_TITLE "Title"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user