mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-06-03 00:58:15 -04:00
Metadata and image resolution write test
The goal of MR is to control the correct saving of metadata and resolution in plugins that support them. - Modified the basic write test to verify that resolution and metadata are saved correctly. - Verifies the correct functioning of MicroExif in plugins that use it to save metadata. - EXR: fixed wrong vertical resolution (error found with this MR). - Added EXR, JXR, JXL, PCX metadata test.
This commit is contained in:
parent
ac3591c7ea
commit
ebb9c1ec18
@ -49,6 +49,8 @@ macro(kimageformats_write_tests)
|
|||||||
foreach(_testname ${KIF_RT_UNPARSED_ARGUMENTS})
|
foreach(_testname ${KIF_RT_UNPARSED_ARGUMENTS})
|
||||||
string(REGEX MATCH "-lossless$" _is_lossless "${_testname}")
|
string(REGEX MATCH "-lossless$" _is_lossless "${_testname}")
|
||||||
string(REGEX MATCH "-nodatacheck" _is_no_data_check "${_testname}")
|
string(REGEX MATCH "-nodatacheck" _is_no_data_check "${_testname}")
|
||||||
|
string(REGEX MATCH "-skipoptional" _is_skip_optional "${_testname}")
|
||||||
|
unset(skip_optional_arg)
|
||||||
unset(lossless_arg)
|
unset(lossless_arg)
|
||||||
unset(no_data_check_arg)
|
unset(no_data_check_arg)
|
||||||
if (_is_lossless)
|
if (_is_lossless)
|
||||||
@ -59,9 +61,13 @@ macro(kimageformats_write_tests)
|
|||||||
set(no_data_check_arg "--no-data-check")
|
set(no_data_check_arg "--no-data-check")
|
||||||
string(REGEX REPLACE "-nodatacheck$" "" _testname "${_testname}")
|
string(REGEX REPLACE "-nodatacheck$" "" _testname "${_testname}")
|
||||||
endif()
|
endif()
|
||||||
|
if (_is_skip_optional)
|
||||||
|
set(skip_optional_arg "--skip-optional-tests")
|
||||||
|
string(REGEX REPLACE "-skipoptional$" "" _testname "${_testname}")
|
||||||
|
endif()
|
||||||
add_test(
|
add_test(
|
||||||
NAME kimageformats-write-${_testname}
|
NAME kimageformats-write-${_testname}
|
||||||
COMMAND writetest ${lossless_arg} ${no_data_check_arg} ${_fuzzarg} ${_testname}
|
COMMAND writetest ${lossless_arg} ${no_data_check_arg} ${skip_optional_arg} ${_fuzzarg} ${_testname}
|
||||||
)
|
)
|
||||||
endforeach(_testname)
|
endforeach(_testname)
|
||||||
endmacro()
|
endmacro()
|
||||||
@ -142,15 +148,18 @@ if (LibJXL_FOUND AND LibJXLThreads_FOUND)
|
|||||||
kimageformats_read_tests(
|
kimageformats_read_tests(
|
||||||
jxl
|
jxl
|
||||||
)
|
)
|
||||||
|
kimageformats_write_tests(
|
||||||
|
jxl-nodatacheck-lossless
|
||||||
|
)
|
||||||
else()
|
else()
|
||||||
kimageformats_read_tests(
|
kimageformats_read_tests(
|
||||||
jxl-skipoptional
|
jxl-skipoptional
|
||||||
)
|
)
|
||||||
endif()
|
|
||||||
kimageformats_write_tests(
|
kimageformats_write_tests(
|
||||||
jxl-nodatacheck-lossless
|
jxl-skipoptional-nodatacheck-lossless
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
if (LibJXR_FOUND)
|
if (LibJXR_FOUND)
|
||||||
kimageformats_read_tests(
|
kimageformats_read_tests(
|
||||||
@ -172,7 +181,7 @@ kimageformats_read_tests(
|
|||||||
# You can append -lossless to the format to indicate that
|
# You can append -lossless to the format to indicate that
|
||||||
# reading back the image data will result in an identical image.
|
# reading back the image data will result in an identical image.
|
||||||
kimageformats_write_tests(
|
kimageformats_write_tests(
|
||||||
pcx-lossless
|
pcx-nodatacheck
|
||||||
pic-lossless
|
pic-lossless
|
||||||
qoi-lossless
|
qoi-lossless
|
||||||
rgb-lossless
|
rgb-lossless
|
||||||
|
41
autotests/write/basic/exr.json
Normal file
41
autotests/write/basic/exr.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"format" : "exr",
|
||||||
|
"metadata" : [
|
||||||
|
{
|
||||||
|
"key" : "CreationDate",
|
||||||
|
"value" : "2025-01-14T13:53:32+01:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Altitude",
|
||||||
|
"value" : "34"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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" : 10000,
|
||||||
|
"dotsPerMeterY" : 12000
|
||||||
|
}
|
||||||
|
}
|
57
autotests/write/basic/jxl.json
Normal file
57
autotests/write/basic/jxl.json
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"format" : "jxl",
|
||||||
|
"metadata" : [
|
||||||
|
{
|
||||||
|
"key" : "CreationDate",
|
||||||
|
"value" : "2025-01-14T13:53:32+01:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Software" ,
|
||||||
|
"value" : "Adobe Photoshop 26.2 (Windows)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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" : 11812
|
||||||
|
}
|
||||||
|
}
|
33
autotests/write/basic/jxr.json
Normal file
33
autotests/write/basic/jxr.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"format" : "jxr",
|
||||||
|
"metadata" : [
|
||||||
|
{
|
||||||
|
"key" : "CreationDate",
|
||||||
|
"value" : "2025-01-14T13:53:32+01:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Author",
|
||||||
|
"value" : "KDE Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Copyright",
|
||||||
|
"value" : "@2025 KDE Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Description",
|
||||||
|
"value" : "TV broadcast test image."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Manufacturer",
|
||||||
|
"value" : "KFramework"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Model",
|
||||||
|
"value" : "KImageFormats"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution" : {
|
||||||
|
"dotsPerMeterX" : 10000,
|
||||||
|
"dotsPerMeterY" : 11000
|
||||||
|
}
|
||||||
|
}
|
7
autotests/write/basic/pcx.json
Normal file
7
autotests/write/basic/pcx.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"format" : "pcx",
|
||||||
|
"resolution" : {
|
||||||
|
"dotsPerMeterX" : 10000,
|
||||||
|
"dotsPerMeterY" : 20000
|
||||||
|
}
|
||||||
|
}
|
@ -16,16 +16,106 @@
|
|||||||
#include <QImage>
|
#include <QImage>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QImageWriter>
|
#include <QImageWriter>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonParseError>
|
||||||
#include <QMetaEnum>
|
#include <QMetaEnum>
|
||||||
#include <QTextStream>
|
#include <QTextStream>
|
||||||
|
|
||||||
#include "fuzzyeq.cpp"
|
#include "fuzzyeq.cpp"
|
||||||
|
|
||||||
|
QJsonObject readOptionalInfo(const QString &suffix)
|
||||||
|
{
|
||||||
|
auto fi = QFileInfo(QStringLiteral("%1/basic/%2.json").arg(IMAGEDIR, suffix));
|
||||||
|
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.isObject()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc.object();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOptionalInfo(QImage& image, const QString &suffix)
|
||||||
|
{
|
||||||
|
auto obj = readOptionalInfo(suffix);
|
||||||
|
if (obj.isEmpty()) {
|
||||||
|
return ;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set resolution
|
||||||
|
auto res = obj.value("resolution").toObject();
|
||||||
|
if (!res.isEmpty()) {
|
||||||
|
image.setDotsPerMeterX(res.value("dotsPerMeterX").toInt());
|
||||||
|
image.setDotsPerMeterY(res.value("dotsPerMeterY").toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 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();
|
||||||
|
image.setText(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkOptionalInfo(QImage& image, const QString &suffix)
|
||||||
|
{
|
||||||
|
auto obj = readOptionalInfo(suffix);
|
||||||
|
if (obj.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the format match
|
||||||
|
if (suffix.compare(obj.value("format").toString(), Qt::CaseInsensitive)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resy != image.dotsPerMeterY()) {
|
||||||
|
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) {
|
||||||
|
qDebug() << key;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief basicTest
|
* \brief basicTest
|
||||||
* Run a basic test on some common images.
|
* Run a basic test on some common images.
|
||||||
*/
|
*/
|
||||||
int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, uint fuzzarg)
|
int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, bool skipOptional, uint fuzzarg)
|
||||||
{
|
{
|
||||||
uchar fuzziness = uchar(fuzzarg);
|
uchar fuzziness = uchar(fuzzarg);
|
||||||
|
|
||||||
@ -70,6 +160,9 @@ int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, uint f
|
|||||||
if (lossless) {
|
if (lossless) {
|
||||||
imgWriter.setQuality(100);
|
imgWriter.setQuality(100);
|
||||||
}
|
}
|
||||||
|
if (!skipOptional) {
|
||||||
|
setOptionalInfo(pngImage, suffix);
|
||||||
|
}
|
||||||
if (!imgWriter.write(pngImage)) {
|
if (!imgWriter.write(pngImage)) {
|
||||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to write image data\n";
|
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to write image data\n";
|
||||||
++failed;
|
++failed;
|
||||||
@ -96,7 +189,6 @@ int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, uint f
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expData != writtenData) {
|
if (expData != writtenData) {
|
||||||
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n";
|
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n";
|
||||||
++failed;
|
++failed;
|
||||||
@ -113,6 +205,13 @@ int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, uint f
|
|||||||
++failed;
|
++failed;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!skipOptional) {
|
||||||
|
if (!checkOptionalInfo(reReadImage, suffix)) {
|
||||||
|
QTextStream(stdout) << "FAIL : " << fi.fileName() << ": optional information does not match\n";
|
||||||
|
++failed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (reReadImage.colorSpace().isValid()) {
|
if (reReadImage.colorSpace().isValid()) {
|
||||||
QColorSpace toColorSpace;
|
QColorSpace toColorSpace;
|
||||||
if (pngImage.colorSpace().isValid()) {
|
if (pngImage.colorSpace().isValid()) {
|
||||||
@ -486,10 +585,14 @@ int main(int argc, char **argv)
|
|||||||
QStringLiteral("max"));
|
QStringLiteral("max"));
|
||||||
QCommandLineOption createFormatTempates({QStringLiteral("create-format-templates")},
|
QCommandLineOption createFormatTempates({QStringLiteral("create-format-templates")},
|
||||||
QStringLiteral("Create template images for all formats supported by QImage."));
|
QStringLiteral("Create template images for all formats supported by QImage."));
|
||||||
|
QCommandLineOption skipOptTest({QStringLiteral("skip-optional-tests")},
|
||||||
|
QStringLiteral("Skip optional data tests (metadata, resolution, etc.)."));
|
||||||
|
|
||||||
parser.addOption(lossless);
|
parser.addOption(lossless);
|
||||||
parser.addOption(ignoreDataCheck);
|
parser.addOption(ignoreDataCheck);
|
||||||
parser.addOption(fuzz);
|
parser.addOption(fuzz);
|
||||||
parser.addOption(createFormatTempates);
|
parser.addOption(createFormatTempates);
|
||||||
|
parser.addOption(skipOptTest);
|
||||||
|
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
|
|
||||||
@ -514,7 +617,7 @@ int main(int argc, char **argv)
|
|||||||
|
|
||||||
// run test
|
// run test
|
||||||
auto suffix = args.at(0);
|
auto suffix = args.at(0);
|
||||||
auto ret = basicTest(suffix, parser.isSet(lossless), parser.isSet(ignoreDataCheck), fuzzarg);
|
auto ret = basicTest(suffix, parser.isSet(lossless), parser.isSet(ignoreDataCheck), parser.isSet(skipOptTest), fuzzarg);
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
ret = formatTest(suffix, parser.isSet(createFormatTempates));
|
ret = formatTest(suffix, parser.isSet(createFormatTempates));
|
||||||
}
|
}
|
||||||
|
@ -535,7 +535,7 @@ static void setMetadata(const QImage &image, Imf::Header &header)
|
|||||||
|
|
||||||
if (image.dotsPerMeterX() && image.dotsPerMeterY()) {
|
if (image.dotsPerMeterX() && image.dotsPerMeterY()) {
|
||||||
header.insert("xDensity", Imf::FloatAttribute(image.dotsPerMeterX() * 2.54f / 100.f));
|
header.insert("xDensity", Imf::FloatAttribute(image.dotsPerMeterX() * 2.54f / 100.f));
|
||||||
header.insert("pixelAspectRatio", Imf::FloatAttribute(float(image.dotsPerMeterX()) / float(image.dotsPerMeterY())));
|
header.insert("pixelAspectRatio", Imf::FloatAttribute(float(image.dotsPerMeterY()) / float(image.dotsPerMeterX())));
|
||||||
}
|
}
|
||||||
|
|
||||||
// set default chroma (default constructor ITU-R BT.709-3 -> sRGB)
|
// set default chroma (default constructor ITU-R BT.709-3 -> sRGB)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user