mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-07-14 02:54:19 -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:
@ -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)
|
||||
kimageformats_read_tests(
|
||||
jxl
|
||||
)
|
||||
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)) {
|
||||
auto name = obj.value("fileName").toString();
|
||||
auto unsupportedFormat = obj.value("unsupportedFormat").toBool();
|
||||
comment = obj.value("comment").toString();
|
||||
|
||||
if(obj.value("disableAutoTransform").toBool()) {
|
||||
flags |= TestFlag::DisableAutotransform;
|
||||
}
|
||||
|
||||
if (unsupportedFormat) {
|
||||
flags |= TestFlag::SkipTest;
|
||||
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())
|
||||
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.
|
||||
|
Reference in New Issue
Block a user