TGA: Support for TGA specification 2.0

Adds TGA 2.0 compliance:
- Support for Extension Area, Developer Area and Footer (metadata support)
- Support for 15-bit and 16-bit per pixel images (both RGB and Indexed)
- Full support for rotation on reading (we cannot use Qt transformations because only a subset is part of the TGA specification)
- When writing you can choose the supported version (subType)
- Improved writing speed (approximately 10 times) and removed whole image conversions (significant memory savings)

It pass the [TrueVision TGA 2.0 conformance suite](https://github.com/zigimg/test-suite/tree/master/fixtures/tga).

Test changes:
- Read test: added ability to skip a specific test on sequential devices (via JSON behavior file)
- Write test: added the ability to set the subType when writing (via JSON properties file)

Closes #37
This commit is contained in:
Mirco Miranda
2025-08-23 11:20:09 +02:00
committed by Albert Astals Cid
parent f933cbe12d
commit 9c6c0c01ae
31 changed files with 1054 additions and 200 deletions

View File

@ -189,7 +189,7 @@ kimageformats_write_tests(
pic-lossless
qoi-lossless
rgb-lossless
tga # fixme: the alpha images appear not to be written properly
tga-nodatacheck
)
# EPS read tests depend on the vagaries of GhostScript

View File

@ -88,30 +88,33 @@ are iterated sequentially and the first object that matches the requirements
is used for testing (successes are ignored).
Supported values for JSON objects:
- `comment`: Type string. A string shown by the test when a condition occurs.
- `description`: Type string. A description of the object. Not used by the
- `comment`: Type `string`. A string shown by the test when a condition occurs.
- `description`: Type `string`. A description of the object. Not used by the
test.
- `disableAutoTransform`: Type boolean. By default, tests are run with
- `disableAutoTransform`: Type `boolean`. By default, tests are run with
autotransform enabled (i.e. rotation is applied if the plugin supports it).
Set to `true` to disable autotransform.
- `filename`: Type string. Name of the template file to use. E.g.
- `filename`: Type `string`. Name of the template file to use. E.g.
"testRGB_Qt_6_2.png".
- `fuzziness`: Type integer. Set the fuzziness only if not already set on the
- `fuzziness`: Type `integer`. Set the fuzziness only if not already set on the
command line. The value set on the command line wins over the one in the JSON
file.
- `maxQtVersion`: Type string. Maximum Qt version this object is compatible
- `maxQtVersion`: Type `string`. Maximum Qt version this object is compatible
with (if not set means all). E.g. "6.2.99".
- `metadata`: Type Array. An array of key/value objects (string type)
- `metadata`: Type `array`. An array of key/value objects (string type)
containing the image metadata as returned by `QImage::text`.
- `minQtVersion`: Type string. Minimum Qt version this object is compatible
- `minQtVersion`: Type `string`. Minimum Qt version this object is compatible
with (if not set means all). E.g. "6.2.0".
- `perceptiveFuzziness` Type boolean. Set the perceptive fuzziness only if not
- `perceptiveFuzziness` Type `boolean`. Set the perceptive fuzziness only if not
already set on the command line. The value set on the command line wins over
the one in the JSON file.
- `resolution`: Type object. An object with the `dotsPerMeterX` and
- `resolution`: Type `object`. An object with the `dotsPerMeterX` and
`dotsPerMeterY` (integer) values of the image.
- `seeAlso`: Type string. More info about the object. Normally used to point
- `seeAlso`: Type `string`. More info about the object. Normally used to point
to bug reports. Not used by the test.
- `skipSequential`: Type `boolean`. Skip the test on sequential access device.
Some plugins may have limited functionality on sequential devices (e.g.,
not reading metadata).
- `unsupportedFormat`: Type `boolean`. When true, the test is skipped.
Some examples:
@ -169,11 +172,12 @@ See also [Add a test to CMakeLists.txt](#add-a-test-to-cmakeliststxt).
The properties file must be located in `write/basic` and must have the name
of the file format (e.g. jxl.json). It is a JSON object composed of the
following values:
- `format`: Type string. The format tested.
- `metadata`: Type Array. An array of key/value objects (string type)
- `format`: Type `string`. The format tested.
- `metadata`: Type `array`. An array of key/value objects (string type)
containing the image metadata as returned by `QImage::text`.
- `resolution`: Type object. An object with the `dotsPerMeterX` and `
- `resolution`: Type `object`. An object with the `dotsPerMeterX` and `
dotsPerMeterY` (integer) values of the image.
- `subType`: type `string`. The image writer subtype to set when testing.
[This is an example](write/basic/jxl.json) of property file.

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -0,0 +1,56 @@
[
{
"fileName" : "extarea.png",
"skipSequential" : true,
"metadata" : [
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "Comment",
"value" : "TV broadcast test image."
},
{
"key" : "Altitude",
"value" : "34"
},
{
"key" : "Copyright",
"value" : "@2025 KDE Project"
},
{
"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"
},
{
"key" : "Software",
"value" : "LIFE Pro 2.18.30 (Linux)"
}
],
"resolution" : {
"dotsPerMeterX" : 11811,
"dotsPerMeterY" : 5906
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

View File

@ -0,0 +1,32 @@
[
{
"fileName" : "extarea.png",
"skipSequential" : true,
"metadata" : [
{
"key" : "Title",
"value" : "Test Card"
},
{
"key" : "Author",
"value" : "KDE Project"
},
{
"key" : "ModificationDate",
"value" : "2025-08-21T07:32:45"
},
{
"key" : "Comment",
"value" : "TV broadcast test image."
},
{
"key" : "Software",
"value" : "LIFE Pro 2.18.31 (Linux)"
}
],
"resolution" : {
"dotsPerMeterX" : 3937,
"dotsPerMeterY" : 3937
}
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation.png"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@ -0,0 +1,5 @@
[
{
"fileName" : "orientation.png"
}
]

View File

@ -281,6 +281,12 @@ int main(int argc, char **argv)
continue;
}
if (seq && timg.skipSequentialDeviceTest()) {
QTextStream(stdout) << "SKIP : " << fi.fileName() << ": marked to be skipped on a sequential device (don't worry, it's ok)\n";
++skipped;
continue;
}
TemplateImage::TestFlags flags = TemplateImage::None;
QString comment;
QFileInfo expFileInfo = timg.compareImage(flags, comment);

View File

@ -76,6 +76,15 @@ bool TemplateImage::isLicense() const
return !m_fi.suffix().compare(QStringLiteral("license"), Qt::CaseInsensitive);
}
bool TemplateImage::skipSequentialDeviceTest() const
{
auto obj = searchObject(m_fi);
if (obj.isEmpty()) {
return false;
}
return obj.value("skipSequential").toBool();
}
QFileInfo TemplateImage::compareImage(TestFlags &flags, QString& comment) const
{
auto fi = jsonImage(flags, comment);

View File

@ -54,6 +54,12 @@ public:
*/
bool isLicense() const;
/*!
* \brief skipSequentialDeviceTest
* \return tre it the sequential test should be skipped.
*/
bool skipSequentialDeviceTest() const;
/*!
* \brief compareImage
* \param flags Flags for modifying test behavior (e.g. image format not supported by current Qt version).

View File

@ -0,0 +1,62 @@
{
"format" : "tga",
"subType" : "TGAv2E",
"metadata" : [
{
"key" : "CreationDate",
"value" : "2025-01-14T13:53:32+01:00"
},
{
"key" : "Direction",
"value" : "123.7"
},
{
"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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -70,6 +70,15 @@ void setOptionalInfo(QImage &image, const QString &suffix)
}
}
QByteArray readSubType(const QString &suffix)
{
auto obj = readOptionalInfo(suffix);
if (obj.isEmpty()) {
return {};
}
return obj.value("subType").toString().toLatin1();
}
bool checkOptionalInfo(QImage &image, const QString &suffix)
{
auto obj = readOptionalInfo(suffix);
@ -157,6 +166,9 @@ int basicTest(const QString &suffix, bool lossless, bool ignoreDataCheck, bool s
{
QBuffer buffer(&writtenData);
QImageWriter imgWriter(&buffer, format.constData());
auto subType = readSubType(suffix);
if (!subType.isEmpty())
imgWriter.setSubType(subType);
if (lossless) {
imgWriter.setQuality(100);
}