diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index adaab43..0fe117a 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -49,6 +49,8 @@ macro(kimageformats_write_tests) foreach(_testname ${KIF_RT_UNPARSED_ARGUMENTS}) string(REGEX MATCH "-lossless$" _is_lossless "${_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(no_data_check_arg) if (_is_lossless) @@ -59,9 +61,13 @@ macro(kimageformats_write_tests) set(no_data_check_arg "--no-data-check") string(REGEX REPLACE "-nodatacheck$" "" _testname "${_testname}") endif() + if (_is_skip_optional) + set(skip_optional_arg "--skip-optional-tests") + string(REGEX REPLACE "-skipoptional$" "" _testname "${_testname}") + endif() add_test( 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) endmacro() @@ -142,14 +148,17 @@ if (LibJXL_FOUND AND LibJXLThreads_FOUND) kimageformats_read_tests( jxl ) + kimageformats_write_tests( + jxl-nodatacheck-lossless + ) else() kimageformats_read_tests( jxl-skipoptional ) + kimageformats_write_tests( + jxl-skipoptional-nodatacheck-lossless + ) endif() - kimageformats_write_tests( - jxl-nodatacheck-lossless - ) endif() if (LibJXR_FOUND) @@ -172,7 +181,7 @@ kimageformats_read_tests( # You can append -lossless to the format to indicate that # reading back the image data will result in an identical image. kimageformats_write_tests( - pcx-lossless + pcx-nodatacheck pic-lossless qoi-lossless rgb-lossless diff --git a/autotests/write/basic/exr.json b/autotests/write/basic/exr.json new file mode 100644 index 0000000..6feee72 --- /dev/null +++ b/autotests/write/basic/exr.json @@ -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 + } +} diff --git a/autotests/write/basic/jxl.json b/autotests/write/basic/jxl.json new file mode 100644 index 0000000..c9c840f --- /dev/null +++ b/autotests/write/basic/jxl.json @@ -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 + } +} diff --git a/autotests/write/basic/jxr.json b/autotests/write/basic/jxr.json new file mode 100644 index 0000000..872aff1 --- /dev/null +++ b/autotests/write/basic/jxr.json @@ -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 + } +} diff --git a/autotests/write/basic/pcx.json b/autotests/write/basic/pcx.json new file mode 100644 index 0000000..c6511b7 --- /dev/null +++ b/autotests/write/basic/pcx.json @@ -0,0 +1,7 @@ +{ + "format" : "pcx", + "resolution" : { + "dotsPerMeterX" : 10000, + "dotsPerMeterY" : 20000 + } +} diff --git a/autotests/writetest.cpp b/autotests/writetest.cpp index 9739d82..03f576c 100644 --- a/autotests/writetest.cpp +++ b/autotests/writetest.cpp @@ -16,16 +16,106 @@ #include #include #include +#include +#include +#include +#include #include #include #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 * 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); @@ -70,6 +160,9 @@ int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, uint f if (lossless) { imgWriter.setQuality(100); } + if (!skipOptional) { + setOptionalInfo(pngImage, suffix); + } if (!imgWriter.write(pngImage)) { QTextStream(stdout) << "FAIL : " << fi.fileName() << ": failed to write image data\n"; ++failed; @@ -96,7 +189,6 @@ int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, uint f continue; } } - if (expData != writtenData) { QTextStream(stdout) << "FAIL : " << fi.fileName() << ": written data differs from " << fi.fileName() << "\n"; ++failed; @@ -113,6 +205,13 @@ int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, uint f ++failed; continue; } + if (!skipOptional) { + if (!checkOptionalInfo(reReadImage, suffix)) { + QTextStream(stdout) << "FAIL : " << fi.fileName() << ": optional information does not match\n"; + ++failed; + continue; + } + } if (reReadImage.colorSpace().isValid()) { QColorSpace toColorSpace; if (pngImage.colorSpace().isValid()) { @@ -486,10 +585,14 @@ int main(int argc, char **argv) QStringLiteral("max")); QCommandLineOption createFormatTempates({QStringLiteral("create-format-templates")}, 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(ignoreDataCheck); parser.addOption(fuzz); parser.addOption(createFormatTempates); + parser.addOption(skipOptTest); parser.process(app); @@ -514,7 +617,7 @@ int main(int argc, char **argv) // run test 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) { ret = formatTest(suffix, parser.isSet(createFormatTempates)); } diff --git a/src/imageformats/exr.cpp b/src/imageformats/exr.cpp index 6f806d8..45a755a 100644 --- a/src/imageformats/exr.cpp +++ b/src/imageformats/exr.cpp @@ -535,7 +535,7 @@ static void setMetadata(const QImage &image, Imf::Header &header) if (image.dotsPerMeterX() && image.dotsPerMeterY()) { 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)