mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-06-03 17:08:08 -04:00
JXL: Resolution and metadata support via EXIF
- Added a class to read and write minimal exif metadata. - JXL plugin uses EXIF metadata to load/save the resolution of the image (like GIMP). - JXL plugin uses EXIF metadata to set/store text metadata and date/time. - Enable info display in Dolphin (JXL File -> Properties -> Details on a JXL file, see image below). - Enabled read test to check also image metadata and resolution. {width=401 height=357}
This commit is contained in:
parent
f39ca9dc9b
commit
ae00c110f2
@ -8,3 +8,4 @@ Options:
|
||||
test-before-installing: True
|
||||
require-passing-tests-on: [ 'Linux', 'FreeBSD', 'Windows' ]
|
||||
cmake-options: "-DKIMAGEFORMATS_DDS=ON -DKIMAGEFORMATS_JXR=ON"
|
||||
per-test-timeout: 90
|
||||
|
155
README.md
155
README.md
@ -113,16 +113,88 @@ clamp). This is not a plugin issue.
|
||||
|
||||
### Metadata
|
||||
|
||||
Metadata support is implemented in all formats that support it. In particular,
|
||||
in addition to the classic `"Description"`, `"Author"`, `"Copyright"`, etc... where
|
||||
possible, XMP data is supported via the `"XML:com.adobe.xmp"` key.
|
||||
Metadata support is available in formats that include it via
|
||||
`QImage::setText()` and `QImage::text()`. To ensure consistent metadata
|
||||
functionality, the following keys have been adopted.
|
||||
|
||||
Please note that only the most common metadata is supported.
|
||||
About the image:
|
||||
- `Altitude`: Floating-point number indicating the GPS altitude in meters
|
||||
above sea level (e.g. 35.4).
|
||||
- `Author`: Person who created the image.
|
||||
- `Comment`: Additional image information in human-readable form, for
|
||||
example a verbal description of the image.
|
||||
- `Copyright`: Copyright notice of the person or organization that claims
|
||||
the copyright to the image.
|
||||
- `CreationDate`: Creation date and time in ISO 8601 format without
|
||||
milliseconds (e.g. 2024-03-23T15:30:43).
|
||||
- `Description`: A string that describes the subject of the image.
|
||||
- `DocumentName`: The name of the document from which this image was
|
||||
scanned.
|
||||
- `HostComputer`: The computer and/or operating system in use at the time
|
||||
of image creation.
|
||||
- `Latitude`: Floating-point number indicating the latitude in degrees
|
||||
north of the equator (e.g. 27.717).
|
||||
- `Longitude`: Floating-point number indicating the longitude in degrees
|
||||
east of Greenwich (e.g. 85.317).
|
||||
- `Owner`: Name of the owner of the image.
|
||||
- `Software`: Name and version number of the software package(s) used to
|
||||
create the image.
|
||||
- `Title`: The title of the image.
|
||||
|
||||
About the camera:
|
||||
- `Manufacturer`: The manufacturer of the recording equipment.
|
||||
- `Model`: The model name or model number of the recording equipment.
|
||||
- `SerialNumber`: The serial number of the recording equipment.
|
||||
|
||||
About the lens:
|
||||
- `LensManufacturer`: The manufacturer of the interchangeable lens that was
|
||||
used.
|
||||
- `LensModel`: The model name or model number of the lens that was used.
|
||||
- `LensSerialNumber`: The serial number of the interchangeable lens that was
|
||||
used.
|
||||
|
||||
Complex metadata (requires a parser):
|
||||
- `XML:org.gimp.xml`: XML metadata generated by GIMP and present only in XCF
|
||||
files.
|
||||
- `XML:com.adobe.xmp`: [Extensible Metadata Platform (XMP)](https://developer.adobe.com/xmp/docs/)
|
||||
is the metadata standard used by Adobe applications and is supported by all
|
||||
common image formats. **Note that XMP metadata is read and written by
|
||||
plugins as is.** Since it may contain information present in other metadata
|
||||
(e.g. `Description`), it is the user's responsibility to ensure consistency
|
||||
between all metadata and XMP metadata when writing an image.
|
||||
|
||||
Supported metadata may vary from one plugin to another. Please note that only
|
||||
the most common metadata are supported and some plugins may return keys not
|
||||
listed here.
|
||||
|
||||
### EXIF Metadata
|
||||
|
||||
[EXIF (Exchangeable Image File Format)](https://en.wikipedia.org/wiki/Exif)
|
||||
metadata is a standard for embedding information within the image file itself.
|
||||
|
||||
Unlike the metadata described above, EXIF metadata is used internally by some
|
||||
plugins to standardize image handling. For example, the JXL plugin uses them
|
||||
to **set/get the image resolution and metadata**. They are also needed to
|
||||
make the image properties appear in the file details of some file managers
|
||||
(e.g. Dolphin).
|
||||
|
||||
When reading, EXIF metadata is converted into simple metadata (e.g.
|
||||
`Description`) and inserted into the image if and only if it is not already
|
||||
present.
|
||||
|
||||
On writing, the image metadata is converted to EXIF and saved appropriately.
|
||||
Note that, if not present in the image to be saved, the following metadata
|
||||
are created automatically:
|
||||
|
||||
- `Software`: Created using `applicationName` and `applicationVersion` methods
|
||||
of [`QCoreApplication`](https://doc.qt.io/qt-6/qcoreapplication.html).
|
||||
- `CreationDate`: Set to current time and date.
|
||||
|
||||
### ICC profile support
|
||||
|
||||
ICC support is fully implemented in all formats that support it. When saving,
|
||||
some formats convert the image using color profiles according to
|
||||
ICC profile support is implemented in all formats that handle them using
|
||||
[`QColorSpace`](https://doc.qt.io/qt-6/qcolorspace.html). When saving, some
|
||||
plugins convert the image using color profiles according to format
|
||||
specifications. In particular, HDR formats almost always convert to linear
|
||||
RGB.
|
||||
|
||||
@ -194,8 +266,10 @@ compiled with Qt 6.8+.
|
||||
**This plugin can be disabled by setting `KIMAGEFORMATS_DDS` to `OFF`
|
||||
in your cmake options.**
|
||||
|
||||
The following defines can be defined in cmake to modify the behavior of the plugin:
|
||||
- `DDS_DISABLE_STRIDE_ALIGNMENT`: disable the stride aligment based on DDS pitch: it is known that some writers do not set it correctly.
|
||||
The following defines can be defined in cmake to modify the behavior of the
|
||||
plugin:
|
||||
- `DDS_DISABLE_STRIDE_ALIGNMENT`: disable the stride aligment based on DDS
|
||||
pitch: it is known that some writers do not set it correctly.
|
||||
|
||||
### The HEIF plugin
|
||||
|
||||
@ -209,9 +283,13 @@ will compile but will fail the tests.
|
||||
|
||||
### The EXR plugin
|
||||
|
||||
The following defines can be defined in cmake to modify the behavior of the plugin:
|
||||
- `EXR_CONVERT_TO_SRGB`: the linear data is converted to sRGB on read to accommodate programs that do not support color profiles.
|
||||
- `EXR_DISABLE_XMP_ATTRIBUTE`: disables the stores XMP values in a non-standard attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable writes it as well.
|
||||
The following defines can be defined in cmake to modify the behavior of the
|
||||
plugin:
|
||||
- `EXR_CONVERT_TO_SRGB`: the linear data is converted to sRGB on read to
|
||||
accommodate programs that do not support color profiles.
|
||||
- `EXR_DISABLE_XMP_ATTRIBUTE`: disables the stores XMP values in a non-standard
|
||||
attribute named "xmp". Note that Gimp reads the "xmp" attribute and Darktable
|
||||
writes it as well.
|
||||
|
||||
### The EPS plugin
|
||||
|
||||
@ -223,8 +301,10 @@ create a temporary PDF file which is then converted to EPS. Therefore, if
|
||||
|
||||
### The HDR plugin
|
||||
|
||||
The following defines can be defined in cmake to modify the behavior of the plugin:
|
||||
- `HDR_HALF_QUALITY`: on read, a 16-bit float image is returned instead of a 32-bit float one.
|
||||
The following defines can be defined in cmake to modify the behavior of the
|
||||
plugin:
|
||||
- `HDR_HALF_QUALITY`: on read, a 16-bit float image is returned instead of a
|
||||
32-bit float one.
|
||||
|
||||
### The JP2 plugin
|
||||
|
||||
@ -232,16 +312,19 @@ The following defines can be defined in cmake to modify the behavior of the plug
|
||||
in your cmake options.**
|
||||
|
||||
JP2 plugin has the following limitations due to the lack of support by OpenJPEG:
|
||||
- Metadata are not supported
|
||||
- Image resolution is not supported
|
||||
- Metadata are not supported.
|
||||
- Image resolution is not supported.
|
||||
|
||||
### The JXL plugin
|
||||
|
||||
**The current version of the plugin limits the image size to 256 megapixels
|
||||
according to feature level 5 of the JXL stream encoding.**
|
||||
|
||||
The following defines can be defined in cmake to modify the behavior of the plugin:
|
||||
- `JXL_HDR_PRESERVATION_DISABLED`: disable floating point images (both read and write) by converting them to UINT16 images. Any HDR data is lost. Note that FP images are always disabled when compiling with libJXL less than v0.9.
|
||||
The following defines can be defined in cmake to modify the behavior of the
|
||||
plugin:
|
||||
- `JXL_HDR_PRESERVATION_DISABLED`: disable floating point images (both read
|
||||
and write) by converting them to UINT16 images. Any HDR data is lost. Note
|
||||
that FP images are always disabled when compiling with libJXL less than v0.9.
|
||||
- `JXL_DECODE_BOXES_DISABLED`: disable reading of metadata (e.g. XMP).
|
||||
|
||||
### The JXR plugin
|
||||
@ -249,11 +332,21 @@ The following defines can be defined in cmake to modify the behavior of the plug
|
||||
**This plugin is disabled by default. It can be enabled by settings
|
||||
`KIMAGEFORMATS_JXR` to `ON` in your cmake options.**
|
||||
|
||||
The following defines can be defined in cmake to modify the behavior of the plugin:
|
||||
- `JXR_DENY_FLOAT_IMAGE`: disables the use of float images and consequently any HDR data will be lost.
|
||||
- `JXR_DISABLE_DEPTH_CONVERSION`: remove the neeeds of additional memory by disabling the conversion between different color depths (e.g. RGBA64bpp to RGBA32bpp) at the cost of reduced compatibility.
|
||||
- `JXR_DISABLE_BGRA_HACK`: Windows displays and opens JXR files correctly out of the box. Unfortunately it doesn't seem to open (P)RGBA @32bpp files as it only wants (P)BGRA32bpp files (a format not supported by Qt). Only for this format an hack is activated to guarantee total compatibility of the plugin with Windows.
|
||||
- `JXR_ENABLE_ADVANCED_METADATA`: enable metadata support (e.g. XMP). Some distributions use an incomplete JXR library that does not allow reading metadata, causing compilation errors.
|
||||
The following defines can be defined in cmake to modify the behavior of the
|
||||
plugin:
|
||||
- `JXR_DENY_FLOAT_IMAGE`: disables the use of float images and consequently
|
||||
any HDR data will be lost.
|
||||
- `JXR_DISABLE_DEPTH_CONVERSION`: remove the neeeds of additional memory by
|
||||
disabling the conversion between different color depths (e.g. RGBA64bpp to
|
||||
RGBA32bpp) at the cost of reduced compatibility.
|
||||
- `JXR_DISABLE_BGRA_HACK`: Windows displays and opens JXR files correctly out
|
||||
of the box. Unfortunately it doesn't seem to open (P)RGBA @32bpp files as
|
||||
it only wants (P)BGRA32bpp files (a format not supported by Qt). Only for
|
||||
this format an hack is activated to guarantee total compatibility of the
|
||||
plugin with Windows.
|
||||
- `JXR_ENABLE_ADVANCED_METADATA`: enable metadata support (e.g. XMP). Some
|
||||
distributions use an incomplete JXR library that does not allow reading
|
||||
metadata, causing compilation errors.
|
||||
|
||||
### The KRA plugin
|
||||
|
||||
@ -270,15 +363,19 @@ image.
|
||||
### The PSD plugin
|
||||
|
||||
PSD support has the following limitations:
|
||||
- Only images saved by Photoshop using compatibility mode enabled (Photoshop default) can be decoded.
|
||||
- Only images saved by Photoshop using compatibility mode enabled (Photoshop
|
||||
default) can be decoded.
|
||||
- Multichannel images are treated as CMYK if they have 2 or more channels.
|
||||
- Multichannel images are treated as Grayscale if they have 1 channel.
|
||||
- Duotone images are treated as grayscale images.
|
||||
- Extra channels other than alpha are discarded.
|
||||
|
||||
The following defines can be defined in cmake to modify the behavior of the plugin:
|
||||
- `PSD_FAST_LAB_CONVERSION`: the LAB image is converted to linear sRGB instead of sRGB which significantly increases performance.
|
||||
- `PSD_NATIVE_CMYK_SUPPORT_DISABLED`: disable native support for CMYK images when compiled with Qt 6.8+
|
||||
The following defines can be defined in cmake to modify the behavior of the
|
||||
plugin:
|
||||
- `PSD_FAST_LAB_CONVERSION`: the LAB image is converted to linear sRGB instead
|
||||
of sRGB which significantly increases performance.
|
||||
- `PSD_NATIVE_CMYK_SUPPORT_DISABLED`: disable native support for CMYK images
|
||||
when compiled with Qt 6.8+
|
||||
|
||||
### The RAW plugin
|
||||
|
||||
@ -293,7 +390,9 @@ The default setting tries to balance quality and conversion speed.
|
||||
### The XCF plugin
|
||||
|
||||
XCF support has the following limitations:
|
||||
- XCF format up to [version 12](https://testing.developer.gimp.org/core/standards/xcf/#version-history) (no support for GIMP 3).
|
||||
- XCF format up to [version 12](https://testing.developer.gimp.org/core/standards/xcf/#version-history)
|
||||
(no support for GIMP 3).
|
||||
- The returned image is always 8-bit.
|
||||
- Cannot read zlib compressed files.
|
||||
- The rendered image may be slightly different (colors/transparencies) than in GIMP.
|
||||
- The rendered image may be slightly different (colors/transparencies) than
|
||||
in GIMP.
|
||||
|
@ -19,9 +19,15 @@ macro(kimageformats_read_tests)
|
||||
endif()
|
||||
|
||||
foreach(_testname ${KIF_RT_UNPARSED_ARGUMENTS})
|
||||
string(REGEX MATCH "-skipoptional" _is_skip_optional "${_testname}")
|
||||
unset(skip_optional_arg)
|
||||
if (_is_skip_optional)
|
||||
set(skip_optional_arg "--skip-optional-tests")
|
||||
string(REGEX REPLACE "-skipoptional$" "" _testname "${_testname}")
|
||||
endif()
|
||||
add_test(
|
||||
NAME kimageformats-read-${_testname}
|
||||
COMMAND readtest ${_fuzzarg} ${_testname}
|
||||
COMMAND readtest ${skip_optional_arg} ${_fuzzarg} ${_testname}
|
||||
)
|
||||
endforeach(_testname)
|
||||
endmacro()
|
||||
@ -132,9 +138,15 @@ if (OpenJPEG_FOUND)
|
||||
endif()
|
||||
|
||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||
if(LibJXL_VERSION VERSION_GREATER_EQUAL "0.11.0")
|
||||
kimageformats_read_tests(
|
||||
jxl
|
||||
)
|
||||
else()
|
||||
kimageformats_read_tests(
|
||||
jxl-skipoptional
|
||||
)
|
||||
endif()
|
||||
kimageformats_write_tests(
|
||||
jxl-nodatacheck-lossless
|
||||
)
|
||||
|
19
autotests/read/jxl/gimp_exif.jxl.json
Normal file
19
autotests/read/jxl/gimp_exif.jxl.json
Normal file
@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"fileName" : "gimp_exif.png",
|
||||
"metadata" : [
|
||||
{
|
||||
"Key" : "CreationDate",
|
||||
"Value" : "2025-01-05T10:18:16"
|
||||
},
|
||||
{
|
||||
"Key" : "Software" ,
|
||||
"Value" : "GIMP 3.0.0-RC2"
|
||||
}
|
||||
],
|
||||
"resolution" : {
|
||||
"dotsPerMeterX" : 5905,
|
||||
"dotsPerMeterY" : 6692
|
||||
}
|
||||
}
|
||||
]
|
@ -198,7 +198,7 @@ int main(int argc, char **argv)
|
||||
QCoreApplication::removeLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||
QCoreApplication::addLibraryPath(QStringLiteral(PLUGIN_DIR));
|
||||
QCoreApplication::setApplicationName(QStringLiteral("readtest"));
|
||||
QCoreApplication::setApplicationVersion(QStringLiteral("1.2.0"));
|
||||
QCoreApplication::setApplicationVersion(QStringLiteral("1.3.0"));
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(QStringLiteral("Performs basic image conversion checking."));
|
||||
@ -208,8 +208,11 @@ int main(int argc, char **argv)
|
||||
QCommandLineOption fuzz(QStringList() << QStringLiteral("f") << QStringLiteral("fuzz"),
|
||||
QStringLiteral("Allow for some deviation in ARGB data."),
|
||||
QStringLiteral("max"));
|
||||
parser.addOption(fuzz);
|
||||
QCommandLineOption skipOptTest({QStringLiteral("skip-optional-tests")},
|
||||
QStringLiteral("Skip optional data tests (metadata, resolution, etc.)."));
|
||||
|
||||
parser.addOption(fuzz);
|
||||
parser.addOption(skipOptTest);
|
||||
parser.process(app);
|
||||
|
||||
const QStringList args = parser.positionalArguments();
|
||||
@ -314,6 +317,7 @@ int main(int argc, char **argv)
|
||||
continue;
|
||||
}
|
||||
|
||||
// option test
|
||||
OptionTest optionTest;
|
||||
if (!optionTest.store(&inputReader)) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": error while reading options\n";
|
||||
@ -339,6 +343,17 @@ int main(int argc, char **argv)
|
||||
continue;
|
||||
}
|
||||
|
||||
// metadata checks
|
||||
if (!parser.isSet(skipOptTest)) {
|
||||
QString optError;
|
||||
if (!timg.checkOptionaInfo(inputImage, optError)) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << " : " << optError << "\n";
|
||||
++failed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// image compare
|
||||
if (expImage.width() != inputImage.width()) {
|
||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": width was " << inputImage.width() << " but " << expfilename << " width was "
|
||||
<< expImage.width() << "\n";
|
||||
|
@ -12,6 +12,49 @@
|
||||
#include <QJsonObject>
|
||||
#include <QVersionNumber>
|
||||
|
||||
static QJsonObject searchObject(const QFileInfo& file)
|
||||
{
|
||||
auto fi = QFileInfo(QStringLiteral("%1.json").arg(file.filePath()));
|
||||
if (!fi.exists()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QFile f(fi.filePath());
|
||||
if (!f.open(QFile::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError err;
|
||||
auto doc = QJsonDocument::fromJson(f.readAll(), &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isArray()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto currentQt = QVersionNumber::fromString(qVersion());
|
||||
auto arr = doc.array();
|
||||
for (auto val : arr) {
|
||||
if (!val.isObject())
|
||||
continue;
|
||||
auto obj = val.toObject();
|
||||
auto minQt = QVersionNumber::fromString(obj.value("minQtVersion").toString());
|
||||
auto maxQt = QVersionNumber::fromString(obj.value("maxQtVersion").toString());
|
||||
auto name = obj.value("fileName").toString();
|
||||
auto unsupportedFormat = obj.value("unsupportedFormat").toBool();
|
||||
|
||||
// filter
|
||||
if (name.isEmpty() && !unsupportedFormat)
|
||||
continue;
|
||||
if (!minQt.isNull() && currentQt < minQt)
|
||||
continue;
|
||||
if (!maxQt.isNull() && currentQt > maxQt)
|
||||
continue;
|
||||
return obj;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
TemplateImage::TemplateImage(const QFileInfo &fi) :
|
||||
m_fi(fi)
|
||||
{
|
||||
@ -45,6 +88,43 @@ QFileInfo TemplateImage::compareImage(TestFlags &flags, QString& comment) const
|
||||
return legacyImage();
|
||||
}
|
||||
|
||||
bool TemplateImage::checkOptionaInfo(const QImage& image, QString& error) const
|
||||
{
|
||||
auto obj = searchObject(m_fi);
|
||||
if (obj.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Test resolution
|
||||
auto res = obj.value("resolution").toObject();
|
||||
if (!res.isEmpty()) {
|
||||
auto resx = res.value("dotsPerMeterX").toInt();
|
||||
auto resy = res.value("dotsPerMeterY").toInt();
|
||||
if (resx != image.dotsPerMeterX()) {
|
||||
error = QStringLiteral("X resolution mismatch (current: %1, expected: %2)!").arg(image.dotsPerMeterX()).arg(resx);
|
||||
return false;
|
||||
}
|
||||
if (resy != image.dotsPerMeterY()) {
|
||||
error = QStringLiteral("Y resolution mismatch (current: %1, expected: %2)!").arg(image.dotsPerMeterY()).arg(resy);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Test metadata
|
||||
auto meta = obj.value("metadata").toArray();
|
||||
for (auto jv : meta) {
|
||||
auto obj = jv.toObject();
|
||||
auto key = obj.value("Key").toString();
|
||||
auto val = obj.value("Value").toString();
|
||||
auto cur = image.text(key);
|
||||
if (cur != val) {
|
||||
error = QStringLiteral("Metadata '%1' mismatch (current: '%2', expected:'%3')!").arg(key, cur, val);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList TemplateImage::suffixes()
|
||||
{
|
||||
@ -66,51 +146,25 @@ QFileInfo TemplateImage::legacyImage() const
|
||||
QFileInfo TemplateImage::jsonImage(TestFlags &flags, QString& comment) const
|
||||
{
|
||||
flags = TestFlag::None;
|
||||
auto fi = QFileInfo(QStringLiteral("%1.json").arg(m_fi.filePath()));
|
||||
if (!fi.exists()) {
|
||||
|
||||
auto obj = searchObject(m_fi);
|
||||
if (obj.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QFile f(fi.filePath());
|
||||
if (!f.open(QFile::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QJsonParseError err;
|
||||
auto doc = QJsonDocument::fromJson(f.readAll(), &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isArray()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto currentQt = QVersionNumber::fromString(qVersion());
|
||||
auto arr = doc.array();
|
||||
for (auto val : arr) {
|
||||
if (!val.isObject())
|
||||
continue;
|
||||
auto obj = val.toObject();
|
||||
auto minQt = QVersionNumber::fromString(obj.value("minQtVersion").toString());
|
||||
auto maxQt = QVersionNumber::fromString(obj.value("maxQtVersion").toString());
|
||||
auto name = obj.value("fileName").toString();
|
||||
auto unsupportedFormat = obj.value("unsupportedFormat").toBool();
|
||||
comment = obj.value("comment").toString();
|
||||
|
||||
if(obj.value("disableAutoTransform").toBool())
|
||||
if(obj.value("disableAutoTransform").toBool()) {
|
||||
flags |= TestFlag::DisableAutotransform;
|
||||
}
|
||||
|
||||
// filter
|
||||
if (name.isEmpty() && !unsupportedFormat)
|
||||
continue;
|
||||
if (!minQt.isNull() && currentQt < minQt)
|
||||
continue;
|
||||
if (!maxQt.isNull() && currentQt > maxQt)
|
||||
continue;
|
||||
if (unsupportedFormat) {
|
||||
flags |= TestFlag::SkipTest;
|
||||
break;
|
||||
}
|
||||
return QFileInfo(QStringLiteral("%1/%2").arg(fi.path(), name));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
return QFileInfo(QStringLiteral("%1/%2").arg(m_fi.path(), name));
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
#define TEMPLATEIMAGE_H
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
|
||||
/*!
|
||||
* \brief The TemplateImage class
|
||||
@ -60,6 +61,15 @@ public:
|
||||
*/
|
||||
QFileInfo compareImage(TestFlags &flags, QString& comment) const;
|
||||
|
||||
/*!
|
||||
* \brief checkOptionaInfo
|
||||
* Verify the optional information (resolution, metadata, etc.) of the image with that in the template if present.
|
||||
* \param image The image to check optional information on.
|
||||
* \param error The error message when returns false.
|
||||
* \return True on success, otherwise false.
|
||||
*/
|
||||
bool checkOptionaInfo(const QImage& image, QString& error) const;
|
||||
|
||||
/*!
|
||||
* \brief suffixes
|
||||
* \return The list of suffixes considered templates.
|
||||
|
@ -75,7 +75,7 @@ endif()
|
||||
##################################
|
||||
|
||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
||||
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp)
|
||||
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp microexif.cpp)
|
||||
target_link_libraries(kimg_jxl PRIVATE PkgConfig::LibJXL PkgConfig::LibJXLThreads)
|
||||
if(LibJXL_VERSION VERSION_GREATER_EQUAL "0.9.0")
|
||||
if(LibJXLCMS_FOUND)
|
||||
|
@ -10,6 +10,7 @@
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "jxl_p.h"
|
||||
#include "microexif_p.h"
|
||||
#include "util_p.h"
|
||||
|
||||
#include <jxl/encode.h>
|
||||
@ -461,6 +462,17 @@ bool QJpegXLHandler::decode_one_frame()
|
||||
m_current_image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(m_xmp));
|
||||
}
|
||||
|
||||
if (!m_exif.isEmpty()) {
|
||||
auto exif = MicroExif::fromByteArray(m_exif);
|
||||
// set image resolution
|
||||
if (exif.horizontalResolution() > 0)
|
||||
m_current_image.setDotsPerMeterX(qRound(exif.horizontalResolution() / 25.4 * 1000));
|
||||
if (exif.verticalResolution() > 0)
|
||||
m_current_image.setDotsPerMeterY(qRound(exif.verticalResolution() / 25.4 * 1000));
|
||||
// set image metadata
|
||||
exif.toImageMetadata(m_current_image);
|
||||
}
|
||||
|
||||
if (JxlDecoderSetImageOutBuffer(m_decoder, &m_input_pixel_format, m_current_image.bits(), m_buffer_size) != JXL_DEC_SUCCESS) {
|
||||
qWarning("ERROR: JxlDecoderSetImageOutBuffer failed");
|
||||
m_parseState = ParseJpegXLError;
|
||||
@ -639,7 +651,6 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
qWarning("Failed to create Jxl encoder");
|
||||
return false;
|
||||
}
|
||||
JxlEncoderUseBoxes(encoder);
|
||||
|
||||
if (m_quality > 100) {
|
||||
m_quality = 100;
|
||||
@ -647,8 +658,12 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
m_quality = 90;
|
||||
}
|
||||
|
||||
JxlEncoderUseContainer(encoder, JXL_TRUE);
|
||||
JxlEncoderUseBoxes(encoder);
|
||||
|
||||
JxlBasicInfo output_info;
|
||||
JxlEncoderInitBasicInfo(&output_info);
|
||||
output_info.have_container = JXL_TRUE;
|
||||
|
||||
QByteArray iccprofile;
|
||||
QColorSpace tmpcs = image.colorSpace();
|
||||
@ -668,8 +683,6 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
|| (pixel_count > FEATURE_LEVEL_5_PIXELS)
|
||||
|| (image.width() > FEATURE_LEVEL_5_WIDTH)
|
||||
|| (image.height() > FEATURE_LEVEL_5_HEIGHT)) {
|
||||
output_info.have_container = JXL_TRUE;
|
||||
JxlEncoderUseContainer(encoder, JXL_TRUE);
|
||||
JxlEncoderSetCodestreamLevel(encoder, 10);
|
||||
}
|
||||
// clang-format on
|
||||
@ -788,6 +801,7 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
auto cs = image.colorSpace();
|
||||
if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
|
||||
tmpimage = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), tmpformat);
|
||||
iccprofile.clear();
|
||||
} else {
|
||||
tmpimage = image.convertToFormat(tmpformat);
|
||||
}
|
||||
@ -820,6 +834,20 @@ bool QJpegXLHandler::write(const QImage &image)
|
||||
return false;
|
||||
}
|
||||
|
||||
auto exif_data = MicroExif::fromImage(image).toByteArray();
|
||||
if (!exif_data.isEmpty()) {
|
||||
exif_data = QByteArray::fromHex("00000000") + exif_data;
|
||||
const char *box_type = "Exif";
|
||||
status = JxlEncoderAddBox(encoder, box_type, reinterpret_cast<const uint8_t *>(exif_data.constData()), exif_data.size(), JXL_FALSE);
|
||||
if (status != JXL_ENC_SUCCESS) {
|
||||
qWarning("JxlEncoderAddBox failed!");
|
||||
if (runner) {
|
||||
JxlThreadParallelRunnerDestroy(runner);
|
||||
}
|
||||
JxlEncoderDestroy(encoder);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
auto xmp_data = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).toUtf8();
|
||||
if (!xmp_data.isEmpty()) {
|
||||
const char *box_type = "xml ";
|
||||
|
1197
src/imageformats/microexif.cpp
Normal file
1197
src/imageformats/microexif.cpp
Normal file
File diff suppressed because it is too large
Load Diff
307
src/imageformats/microexif_p.h
Normal file
307
src/imageformats/microexif_p.h
Normal file
@ -0,0 +1,307 @@
|
||||
/*
|
||||
This file is part of the KDE project
|
||||
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
#ifndef MICROEXIF_P_H
|
||||
#define MICROEXIF_P_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QColorSpace>
|
||||
#include <QDataStream>
|
||||
#include <QDateTime>
|
||||
#include <QImage>
|
||||
#include <QImageIOHandler>
|
||||
#include <QMap>
|
||||
#include <QUuid>
|
||||
#include <QVariant>
|
||||
|
||||
#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
|
||||
#define EXIF_DEFAULT_BYTEORDER QDataStream::LittleEndian
|
||||
#else
|
||||
#define EXIF_DEFAULT_BYTEORDER QDataStream::BigEndian
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* \brief The MicroExif class
|
||||
* Class to extract / write minimal EXIF data (e.g. resolution, rotation,
|
||||
* some strings).
|
||||
*
|
||||
* This class is a partial (or rather minimal) implementation and is only used
|
||||
* to avoid including external libraries when only a few tags are needed.
|
||||
*
|
||||
* It reads/writes the main IFD only.
|
||||
*/
|
||||
class MicroExif
|
||||
{
|
||||
public:
|
||||
using Tags = QMap<quint16, QVariant>;
|
||||
|
||||
/*!
|
||||
* \brief MicroExif
|
||||
* Constructs an empty class.
|
||||
* \sa isEmpty
|
||||
*/
|
||||
MicroExif();
|
||||
|
||||
MicroExif(const MicroExif &other) = default;
|
||||
MicroExif &operator=(const MicroExif &other) = default;
|
||||
|
||||
/*!
|
||||
* \brief clear
|
||||
* Removes all items.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/*!
|
||||
* \brief isEmpty
|
||||
* \return True if it contains no items, otherwise false.
|
||||
*/
|
||||
bool isEmpty() const;
|
||||
|
||||
/*!
|
||||
* \brief horizontalResolution
|
||||
* \return The horizontal resolution in DPI.
|
||||
*/
|
||||
double horizontalResolution() const;
|
||||
void setHorizontalResolution(double hres);
|
||||
|
||||
/*!
|
||||
* \brief verticalResolution
|
||||
* \return The vertical resolution in DPI.
|
||||
*/
|
||||
double verticalResolution() const;
|
||||
void setVerticalResolution(double vres);
|
||||
|
||||
/*!
|
||||
* \brief colosSpace
|
||||
* \return sRGB color space or an invalid one.
|
||||
*/
|
||||
QColorSpace colosSpace() const;
|
||||
void setColorSpace(const QColorSpace& cs);
|
||||
void setColorSpace(const QColorSpace::NamedColorSpace& csName);
|
||||
|
||||
/*!
|
||||
* \brief width
|
||||
* \return The image width.
|
||||
*/
|
||||
qint32 width() const;
|
||||
void setWidth(qint32 w);
|
||||
|
||||
/*!
|
||||
* \brief height
|
||||
* \return The image height.
|
||||
*/
|
||||
qint32 height() const;
|
||||
void setHeight(qint32 h);
|
||||
|
||||
/*!
|
||||
* \brief orientation
|
||||
* The orientation of the image with respect to the rows and columns.
|
||||
*
|
||||
* Valid orientation values:
|
||||
* - 1 = The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
|
||||
* - 2 = The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
|
||||
* - 3 = The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
|
||||
* - 4 = The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
|
||||
* - 5 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
|
||||
* - 6 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
|
||||
* - 7 = The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
|
||||
* - 8 = The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
|
||||
* \return The orientation value or 0 if none.
|
||||
* \sa transformation
|
||||
*/
|
||||
quint16 orientation() const;
|
||||
void setOrientation(quint16 orient);
|
||||
|
||||
/*!
|
||||
* \brief transformation
|
||||
* \return The orientation converted in the equvalent Qt transformation.
|
||||
* \sa orientation
|
||||
*/
|
||||
QImageIOHandler::Transformation transformation() const;
|
||||
void setTransformation(const QImageIOHandler::Transformation& t);
|
||||
|
||||
/*!
|
||||
* \brief software
|
||||
* \return Name and version number of the software package(s) used to create the image.
|
||||
*/
|
||||
QString software() const;
|
||||
void setSoftware(const QString& s);
|
||||
|
||||
/*!
|
||||
* \brief description
|
||||
* \return A string that describes the subject of the image.
|
||||
*/
|
||||
QString description() const;
|
||||
void setDescription(const QString& s);
|
||||
|
||||
/*!
|
||||
* \brief artist
|
||||
* \return Person who created the image.
|
||||
*/
|
||||
QString artist() const;
|
||||
void setArtist(const QString& s);
|
||||
|
||||
/*!
|
||||
* \brief copyright
|
||||
* \return Copyright notice of the person or organization that claims the copyright to the image.
|
||||
*/
|
||||
QString copyright() const;
|
||||
void setCopyright(const QString& s);
|
||||
|
||||
/*!
|
||||
* \brief make
|
||||
* \return The manufacturer of the recording equipment.
|
||||
*/
|
||||
QString make() const;
|
||||
void setMake(const QString& s);
|
||||
|
||||
/*!
|
||||
* \brief model
|
||||
* \return The model name or model number of the equipment.
|
||||
*/
|
||||
QString model() const;
|
||||
void setModel(const QString& s);
|
||||
|
||||
/*!
|
||||
* \brief serialNumber
|
||||
* \return The serial number of the recording equipment.
|
||||
*/
|
||||
QString serialNumber() const;
|
||||
void setSerialNumber(const QString &s);
|
||||
|
||||
/*!
|
||||
* \brief lensMake
|
||||
* \return The manufacturer of the interchangeable lens that was used.
|
||||
*/
|
||||
QString lensMake() const;
|
||||
void setLensMake(const QString &s);
|
||||
|
||||
/*!
|
||||
* \brief lensModel
|
||||
* \return The model name or model number of the lens that was used.
|
||||
*/
|
||||
QString lensModel() const;
|
||||
void setLensModel(const QString &s);
|
||||
|
||||
/*!
|
||||
* \brief lensSerialNumber
|
||||
* \return The serial number of the interchangeable lens that was used.
|
||||
*/
|
||||
QString lensSerialNumber() const;
|
||||
void setLensSerialNumber(const QString &s);
|
||||
|
||||
/*!
|
||||
* \brief dateTime
|
||||
* \return Creation date and time.
|
||||
*/
|
||||
QDateTime dateTime() const;
|
||||
void setDateTime(const QDateTime& dt);
|
||||
|
||||
/*!
|
||||
* \brief title
|
||||
* \return The title of the image.
|
||||
*/
|
||||
QString title() const;
|
||||
void setImageTitle(const QString &s);
|
||||
|
||||
/*!
|
||||
* \brief uniqueId
|
||||
* \return An identifier assigned uniquely to each image or null one if none.
|
||||
*/
|
||||
QUuid uniqueId() const;
|
||||
void setUniqueId(const QUuid &uuid);
|
||||
|
||||
/*!
|
||||
* \brief latitude
|
||||
* \return Floating-point number indicating the latitude in degrees north of the equator (e.g. 27.717) or NaN if not set.
|
||||
*/
|
||||
double latitude() const;
|
||||
void setLatitude(double degree);
|
||||
|
||||
/*!
|
||||
* \brief longitude
|
||||
* \return Floating-point number indicating the longitude in degrees east of Greenwich (e.g. 85.317) or NaN if not set.
|
||||
*/
|
||||
double longitude() const;
|
||||
void setLongitude(double degree);
|
||||
|
||||
/*!
|
||||
* \brief altitude
|
||||
* \return Floating-point number indicating the GPS altitude in meters above sea level or ellipsoidal surface (e.g. 35.4) or NaN if not set.
|
||||
* \note It makes no distinction between an 'ellipsoidal surface' and 'sea level'.
|
||||
*/
|
||||
double altitude() const;
|
||||
void setAltitude(double meters);
|
||||
|
||||
/*!
|
||||
* \brief toByteArray
|
||||
* \param byteOrder Sets the serialization byte order for EXIF data.
|
||||
* \return A byte array containing the serialized data.
|
||||
*/
|
||||
QByteArray toByteArray(const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
||||
|
||||
/*!
|
||||
* \brief write
|
||||
* Serialize the class on a device.
|
||||
* \param device A random access device.
|
||||
* \param byteOrder Sets the serialization byte order for EXIF data.
|
||||
* \return True on success, otherwise false.
|
||||
*/
|
||||
bool write(QIODevice *device, const QDataStream::ByteOrder &byteOrder = EXIF_DEFAULT_BYTEORDER) const;
|
||||
|
||||
/*!
|
||||
* \brief toImageMetadata
|
||||
* Helper to set metadata in an image.
|
||||
* \param targetImage The image to set metadata on.
|
||||
* \param replaceExisting Replaces any existing metadata.
|
||||
*/
|
||||
void toImageMetadata(QImage& targetImage, bool replaceExisting = false) const;
|
||||
|
||||
/*!
|
||||
* \brief fromByteArray
|
||||
* Creates the class from RAW EXIF data.
|
||||
* \return The created class (empty on error).
|
||||
* \sa isEmpty
|
||||
*/
|
||||
static MicroExif fromByteArray(const QByteArray &ba);
|
||||
|
||||
/*!
|
||||
* \brief fromDevice
|
||||
* Creates the class from a device.
|
||||
* \param device A random access device.
|
||||
* \return The created class (empty on error).
|
||||
* \sa isEmpty
|
||||
*/
|
||||
static MicroExif fromDevice(QIODevice *device);
|
||||
|
||||
/*!
|
||||
* \brief fromImage
|
||||
* Creates the class and fill it with image info (e.g. resolution).
|
||||
*/
|
||||
static MicroExif fromImage(const QImage &image);
|
||||
|
||||
private:
|
||||
void setTiffString(quint16 tagId, const QString &s);
|
||||
QString tiffString(quint16 tagId) const;
|
||||
void setExifString(quint16 tagId, const QString& s);
|
||||
QString exifString(quint16 tagId) const;
|
||||
void setGpsString(quint16 tagId, const QString& s);
|
||||
QString gpsString(quint16 tagId) const;
|
||||
bool writeHeader(QDataStream &ds) const;
|
||||
bool writeIfds(QDataStream &ds) const;
|
||||
void updateTags(Tags &tiffTags, Tags &exifTags, Tags &gpsTags) const;
|
||||
|
||||
static void setString(Tags &tags, quint16 tagId, const QString &s);
|
||||
static QString string(const Tags &tags, quint16 tagId);
|
||||
|
||||
private:
|
||||
Tags m_tiffTags;
|
||||
Tags m_exifTags;
|
||||
Tags m_gpsTags;
|
||||
};
|
||||
|
||||
#endif // MICROEXIF_P_H
|
Loading…
x
Reference in New Issue
Block a user