diff --git a/README.md b/README.md index f65819c..8a9654a 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,17 @@ submit the plugin directly to the Qt Project. To be accepted, contributions must: - Contain the test images needed to verify that the changes work correctly. - Pass the tests successfully. +- Use Qt logging categories for Debug messages. For more info about tests, see also [Autotests README](autotests/README.md). ## Duplicated Plugins +> [!important] +> To ensure you are using the correct plugin, the unwanted one should be +renamed or deleted. If several plugins support the same capability, Qt will +select one arbitrarily. + ### The TGA plugin The TGA plugin supports more formats than Qt's own TGA plugin; @@ -340,10 +346,6 @@ The plugin supports the following image data: - FOR4 CIMG (Maya Image File Format): It supports 24/48-bit RGB and 32/64-bit RGBA images. -The plugin does not load images with non-standard SHAM/CTBL chunks due to the -lack of clear specifications. - - ### The JP2 plugin **This plugin can be disabled by setting `KIMAGEFORMATS_JP2` to `OFF` @@ -426,6 +428,25 @@ selectively change the conversion (see also [raw_p.h](./src/imageformats/raw_p.h The default setting tries to balance quality and conversion speed. +### The TGA plugin + +TGA plugin supports both version 1 and version 2 of TGA files. When writing, +it is possible to force which version to use by setting the following subtypes: +- `TGAv1`: force TGA v1.0. No metadata. +- `TGAv2` (default): force TGA v2.0 (strict). Adds the TGA Extension Area. +- `TGAv2E`: force TGA v2.0 (enhanced). Same as TGA v2.0 (strict) but with the + addition of the TGA v2.0 Developer Area with info like, for e.g., Exif data, + XMP packet and the ICC profile. + +They are all TGA specs compliant. While for versions 1 and 2 (strict) it is +possible to decode all the information with the TGA specification alone, for +version 2 (enhanced) it is necessary to know how the additional data is +encoded. + +The following defines can be defined in cmake to modify the behavior of the +plugin: +- `TGA_V2E_AS_DEFAULT`: change the default version of the plugin to `TGAv2E`. + ### The XCF plugin XCF support has the following limitations: diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index e871752..465e411 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -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 diff --git a/autotests/README.md b/autotests/README.md index 3f68bbb..11df784 100644 --- a/autotests/README.md +++ b/autotests/README.md @@ -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. diff --git a/autotests/read/tga/bottom_left.tga b/autotests/read/tga/bottom_left.tga new file mode 100644 index 0000000..049e4d4 Binary files /dev/null and b/autotests/read/tga/bottom_left.tga differ diff --git a/autotests/read/tga/bottom_left.tga.json b/autotests/read/tga/bottom_left.tga.json new file mode 100644 index 0000000..cd45ed6 --- /dev/null +++ b/autotests/read/tga/bottom_left.tga.json @@ -0,0 +1,5 @@ +[ + { + "fileName" : "orientation.png" + } +] diff --git a/autotests/read/tga/bottom_right.tga b/autotests/read/tga/bottom_right.tga new file mode 100644 index 0000000..1784a8e Binary files /dev/null and b/autotests/read/tga/bottom_right.tga differ diff --git a/autotests/read/tga/bottom_right.tga.json b/autotests/read/tga/bottom_right.tga.json new file mode 100644 index 0000000..cd45ed6 --- /dev/null +++ b/autotests/read/tga/bottom_right.tga.json @@ -0,0 +1,5 @@ +[ + { + "fileName" : "orientation.png" + } +] diff --git a/autotests/read/tga/devarea.tga b/autotests/read/tga/devarea.tga new file mode 100644 index 0000000..c0fe832 Binary files /dev/null and b/autotests/read/tga/devarea.tga differ diff --git a/autotests/read/tga/devarea.tga.json b/autotests/read/tga/devarea.tga.json new file mode 100644 index 0000000..6423a99 --- /dev/null +++ b/autotests/read/tga/devarea.tga.json @@ -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 + } + } +] diff --git a/autotests/read/tga/extarea.png b/autotests/read/tga/extarea.png new file mode 100644 index 0000000..bb26682 Binary files /dev/null and b/autotests/read/tga/extarea.png differ diff --git a/autotests/read/tga/extarea.tga b/autotests/read/tga/extarea.tga new file mode 100644 index 0000000..ff3a718 Binary files /dev/null and b/autotests/read/tga/extarea.tga differ diff --git a/autotests/read/tga/extarea.tga.json b/autotests/read/tga/extarea.tga.json new file mode 100644 index 0000000..6becb8a --- /dev/null +++ b/autotests/read/tga/extarea.tga.json @@ -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 + } + } +] diff --git a/autotests/read/tga/orientation.png b/autotests/read/tga/orientation.png new file mode 100644 index 0000000..92614d0 Binary files /dev/null and b/autotests/read/tga/orientation.png differ diff --git a/autotests/read/tga/rgb16.png b/autotests/read/tga/rgb16.png new file mode 100644 index 0000000..d4f210f Binary files /dev/null and b/autotests/read/tga/rgb16.png differ diff --git a/autotests/read/tga/rgb16.tga b/autotests/read/tga/rgb16.tga new file mode 100644 index 0000000..ad6bbea Binary files /dev/null and b/autotests/read/tga/rgb16.tga differ diff --git a/autotests/read/tga/top_left.tga b/autotests/read/tga/top_left.tga new file mode 100644 index 0000000..18bdc46 Binary files /dev/null and b/autotests/read/tga/top_left.tga differ diff --git a/autotests/read/tga/top_left.tga.json b/autotests/read/tga/top_left.tga.json new file mode 100644 index 0000000..cd45ed6 --- /dev/null +++ b/autotests/read/tga/top_left.tga.json @@ -0,0 +1,5 @@ +[ + { + "fileName" : "orientation.png" + } +] diff --git a/autotests/read/tga/top_right.tga b/autotests/read/tga/top_right.tga new file mode 100644 index 0000000..98af8bf Binary files /dev/null and b/autotests/read/tga/top_right.tga differ diff --git a/autotests/read/tga/top_right.tga.json b/autotests/read/tga/top_right.tga.json new file mode 100644 index 0000000..cd45ed6 --- /dev/null +++ b/autotests/read/tga/top_right.tga.json @@ -0,0 +1,5 @@ +[ + { + "fileName" : "orientation.png" + } +] diff --git a/autotests/readtest.cpp b/autotests/readtest.cpp index 061da8e..639db0c 100644 --- a/autotests/readtest.cpp +++ b/autotests/readtest.cpp @@ -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); diff --git a/autotests/templateimage.cpp b/autotests/templateimage.cpp index 65b4fff..15ad33c 100644 --- a/autotests/templateimage.cpp +++ b/autotests/templateimage.cpp @@ -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); diff --git a/autotests/templateimage.h b/autotests/templateimage.h index 847689e..d2bb504 100644 --- a/autotests/templateimage.h +++ b/autotests/templateimage.h @@ -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). diff --git a/autotests/write/basic/tga.json b/autotests/write/basic/tga.json new file mode 100644 index 0000000..86d6497 --- /dev/null +++ b/autotests/write/basic/tga.json @@ -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 + } +} diff --git a/autotests/write/format/tga/Format_RGB16.tga b/autotests/write/format/tga/Format_RGB16.tga index 9ef437c..2c00745 100644 Binary files a/autotests/write/format/tga/Format_RGB16.tga and b/autotests/write/format/tga/Format_RGB16.tga differ diff --git a/autotests/write/format/tga/Format_RGB444.tga b/autotests/write/format/tga/Format_RGB444.tga index 4987d23..a4069c7 100644 Binary files a/autotests/write/format/tga/Format_RGB444.tga and b/autotests/write/format/tga/Format_RGB444.tga differ diff --git a/autotests/write/format/tga/Format_RGB555.tga b/autotests/write/format/tga/Format_RGB555.tga index 10353f9..2c00745 100644 Binary files a/autotests/write/format/tga/Format_RGB555.tga and b/autotests/write/format/tga/Format_RGB555.tga differ diff --git a/autotests/write/format/tga/Format_RGBA16FPx4.tga b/autotests/write/format/tga/Format_RGBA16FPx4.tga index 2f85e0a..ef7959f 100644 Binary files a/autotests/write/format/tga/Format_RGBA16FPx4.tga and b/autotests/write/format/tga/Format_RGBA16FPx4.tga differ diff --git a/autotests/writetest.cpp b/autotests/writetest.cpp index 91238b7..456a364 100644 --- a/autotests/writetest.cpp +++ b/autotests/writetest.cpp @@ -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); } diff --git a/src/imageformats/CMakeLists.txt b/src/imageformats/CMakeLists.txt index 2a29795..3bda87f 100644 --- a/src/imageformats/CMakeLists.txt +++ b/src/imageformats/CMakeLists.txt @@ -137,7 +137,7 @@ kimageformats_add_plugin(kimg_sct SOURCES sct.cpp) ################################## -kimageformats_add_plugin(kimg_tga SOURCES tga.cpp) +kimageformats_add_plugin(kimg_tga SOURCES tga.cpp microexif.cpp scanlineconverter.cpp) ################################## diff --git a/src/imageformats/tga.cpp b/src/imageformats/tga.cpp index ad109ef..e386ab3 100644 --- a/src/imageformats/tga.cpp +++ b/src/imageformats/tga.cpp @@ -12,13 +12,15 @@ * uncompressed and run length encoded indexed, grey and color tga files. * image types 1, 2, 3, 9, 10 and 11. * only RGB color maps with no more than 256 colors. - * pixel formats 8, 16, 24 and 32. + * pixel formats 8, 15, 16, 24 and 32. * writing: - * uncompressed true color tga files + * uncompressed rgb color tga files * uncompressed grayscale tga files * uncompressed indexed tga files */ +#include "microexif_p.h" +#include "scanlineconverter_p.h" #include "tga_p.h" #include "util_p.h" @@ -26,18 +28,30 @@ #include #include -#include +#include #include +#include typedef quint32 uint; typedef quint16 ushort; typedef quint8 uchar; +#ifdef QT_DEBUG +Q_LOGGING_CATEGORY(LOG_TGAPLUGIN, "kf.imageformats.plugins.tga", QtDebugMsg) +#else +Q_LOGGING_CATEGORY(LOG_TGAPLUGIN, "kf.imageformats.plugins.tga", QtWarningMsg) +#endif + +#ifndef TGA_V2E_AS_DEFAULT +/* + * Uncomment to change the default version of the plugin to `TGAv2E`. + */ +// #define TGA_V2E_AS_DEFAULT +#endif // TGA_V2E_AS_DEFAULT + namespace // Private. { // Header format of saved files. -uchar targaMagic[12] = {0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - enum TGAType { TGA_TYPE_INDEXED = 1, TGA_TYPE_RGB = 2, @@ -58,6 +72,16 @@ enum TGAType { #define TGA_ORIGIN_LOWER 0x00 #define TGA_ORIGIN_UPPER 0x20 +/* + * Each TAG is a SHORT value in the range of 0 to 65535. Values from 0 - 32767 are available + * for developer use, while values from 32768 - 65535 are reserved for Truevision. + * Truevision will maintain a list of tags assigned to companies. + * In any case, there's no public "list of tag" and Truevision no longer exists. + */ +#define TGA_EXIF_TAGID 0x7001 // Exif data preceded by "eXif" string +#define TGA_XMPP_TAGID 0x7002 // Xmp packet preceded by "xMPP" string +#define TGA_ICCP_TAGID 0x7003 // Icc profile preceded by "iCCP" string + /** Tga Header. */ struct TgaHeader { uchar id_length = 0; @@ -78,6 +102,149 @@ struct TgaHeader { }; // const static int SIZE = 18; }; +/** Tga 2.0 Footer */ +struct TgaFooter { + TgaFooter() + : extensionOffset(0) + , developerOffset(0) + { + std::memcpy(signature, "TRUEVISION-XFILE.\0", 18); + } + bool isValid() const + { + return std::memcmp(signature, "TRUEVISION-XFILE.\0", 18) == 0; + } + + quint32 extensionOffset; // Extension Area Offset + quint32 developerOffset; // Developer Directory Offset + char signature[18]; // TGA Signature +}; + +/** Tga 2.0 extension area */ +struct TgaExtension { + enum AttributeType : quint16 { + NoAlpha = 0, // no Alpha data included (bits 3-0 of TgaHeader::flags should also be set to zero). + IgnoreAlpha = 1, // undefined data in the Alpha field, can be ignored + RetainAlpha = 2, // undefined data in the Alpha field, but should be retained + Alpha = 3, // useful Alpha channel data is present + PremultipliedAlpha = 4 // pre-multiplied Alpha (see description below) + }; + + TgaExtension() + { + std::memset(this, 0, sizeof(TgaExtension)); + size = 495; // TGA 2.0 specs + + // If you do not use Software Version field, set the SHORT to binary + // zero, and the BYTE to a space (' '). + versionLetter = 0x20; + } + + bool isValid() const + { + return size == 495; + } + + void setDateTime(const QDateTime &dt) + { + if (dt.isValid()) { + auto date = dt.date(); + stampMonth = date.month(); + stampDay = date.day(); + stampYear = date.year(); + auto time = dt.time(); + stampHour = time.hour(); + stampMinute = time.minute(); + stampSecond = time.second(); + } + } + QDateTime dateTime() const + { + auto date = QDate(stampYear, stampMonth, stampDay); + auto time = QTime(stampHour, stampMinute, stampSecond); + if (!date.isValid() || !time.isValid()) + return {}; + return QDateTime(date, time); + } + + void setAuthor(const QString &str) + { + auto ba = str.toLatin1(); + std::memcpy(authorName, ba.data(), std::min(sizeof(authorName) - 1, size_t(ba.size()))); + } + QString author() const + { + if (authorName[sizeof(authorName) - 1] != char(0)) + return {}; + return QString::fromLatin1(authorName); + } + + void setComment(const QString &str) + { + auto ba = str.toLatin1(); + std::memcpy(authorComment, ba.data(), std::min(sizeof(authorComment) - 1, size_t(ba.size()))); + } + QString comment() const + { + if (authorComment[sizeof(authorComment) - 1] != char(0)) + return {}; + return QString::fromLatin1(authorComment); + } + + void setSoftware(const QString &str) + { + auto ba = str.toLatin1(); + std::memcpy(softwareId, ba.data(), std::min(sizeof(softwareId) - 1, size_t(ba.size()))); + } + QString software() const + { + if (softwareId[sizeof(softwareId) - 1] != char(0)) + return {}; + return QString::fromLatin1(softwareId); + } + + quint16 size; // Extension Size + char authorName[41]; // Author Name + char authorComment[324]; // Author Comment + quint16 stampMonth; // Date/Time Stamp: Month + quint16 stampDay; // Date/Time Stamp: Day + quint16 stampYear; // Date/Time Stamp: Year + quint16 stampHour; // Date/Time Stamp: Hour + quint16 stampMinute; // Date/Time Stamp: Minute + quint16 stampSecond; // Date/Time Stamp: Second + char jobName[41]; // Job Name/ID + quint16 jobHour; // Job Time: Hours + quint16 jobMinute; // Job Time: Minutes + quint16 jobSecond; // Job Time: Seconds + char softwareId[41]; // Software ID + quint16 versionNumber; // Software Version Number + quint8 versionLetter; // Software Version Letter + quint32 keyColor; // Key Color + quint16 pixelNumerator; // Pixel Aspect Ratio + quint16 pixelDenominator; // Pixel Aspect Ratio + quint16 gammaNumerator; // Gamma Value + quint16 gammaDenominator; // Gamma Value + quint32 colorOffset; // Color Correction Offset + quint32 stampOffset; // Postage Stamp Offset + quint32 scanOffset; // Scan-Line Table Offset + quint8 attributesType; // Attributes Types +}; + +struct TgaDeveloperDirectory { + struct Field { + quint16 tagId; + quint32 offset; + quint32 size; + }; + + bool isEmpty() const + { + return fields.isEmpty(); + } + + QList fields; +}; + static QDataStream &operator>>(QDataStream &s, TgaHeader &head) { s >> head.id_length; @@ -95,6 +262,107 @@ static QDataStream &operator>>(QDataStream &s, TgaHeader &head) return s; } +static QDataStream &operator>>(QDataStream &s, TgaFooter &footer) +{ + s >> footer.extensionOffset; + s >> footer.developerOffset; + s.readRawData(footer.signature, sizeof(footer.signature)); + return s; +} + +static QDataStream &operator<<(QDataStream &s, const TgaFooter &footer) +{ + s << footer.extensionOffset; + s << footer.developerOffset; + s.writeRawData(footer.signature, sizeof(footer.signature)); + return s; +} + +static QDataStream &operator>>(QDataStream &s, TgaDeveloperDirectory &dir) +{ + quint16 n; + s >> n; + for (auto i = n; i > 0; --i) { + TgaDeveloperDirectory::Field f; + s >> f.tagId; + s >> f.offset; + s >> f.size; + dir.fields << f; + } + return s; +} + +static QDataStream &operator<<(QDataStream &s, const TgaDeveloperDirectory &dir) +{ + s << quint16(dir.fields.size()); + for (auto &&f : dir.fields) { + s << f.tagId; + s << f.offset; + s << f.size; + } + return s; +} + +static QDataStream &operator>>(QDataStream &s, TgaExtension &ext) +{ + s >> ext.size; + s.readRawData(ext.authorName, sizeof(ext.authorName)); + s.readRawData(ext.authorComment, sizeof(ext.authorComment)); + s >> ext.stampMonth; + s >> ext.stampDay; + s >> ext.stampYear; + s >> ext.stampHour; + s >> ext.stampMinute; + s >> ext.stampSecond; + s.readRawData(ext.jobName, sizeof(ext.jobName)); + s >> ext.jobHour; + s >> ext.jobMinute; + s >> ext.jobSecond; + s.readRawData(ext.softwareId, sizeof(ext.softwareId)); + s >> ext.versionNumber; + s >> ext.versionLetter; + s >> ext.keyColor; + s >> ext.pixelNumerator; + s >> ext.pixelDenominator; + s >> ext.gammaNumerator; + s >> ext.gammaDenominator; + s >> ext.colorOffset; + s >> ext.stampOffset; + s >> ext.scanOffset; + s >> ext.attributesType; + return s; +} + +static QDataStream &operator<<(QDataStream &s, const TgaExtension &ext) +{ + s << ext.size; + s.writeRawData(ext.authorName, sizeof(ext.authorName)); + s.writeRawData(ext.authorComment, sizeof(ext.authorComment)); + s << ext.stampMonth; + s << ext.stampDay; + s << ext.stampYear; + s << ext.stampHour; + s << ext.stampMinute; + s << ext.stampSecond; + s.writeRawData(ext.jobName, sizeof(ext.jobName)); + s << ext.jobHour; + s << ext.jobMinute; + s << ext.jobSecond; + s.writeRawData(ext.softwareId, sizeof(ext.softwareId)); + s << ext.versionNumber; + s << ext.versionLetter; + s << ext.keyColor; + s << ext.pixelNumerator; + s << ext.pixelDenominator; + s << ext.gammaNumerator; + s << ext.gammaDenominator; + s << ext.colorOffset; + s << ext.stampOffset; + s << ext.scanOffset; + s << ext.attributesType; + return s; +} + static bool IsSupported(const TgaHeader &head) { if (head.image_type != TGA_TYPE_INDEXED && head.image_type != TGA_TYPE_RGB && head.image_type != TGA_TYPE_GREY && head.image_type != TGA_TYPE_RLE_INDEXED @@ -106,8 +374,7 @@ static bool IsSupported(const TgaHeader &head) if (head.pixel_size > 8 || head.colormap_type != 1) { return false; } - // colormap_size == 16 would be ARRRRRGG GGGBBBBB but we don't support that. - if (head.colormap_size != 24 && head.colormap_size != 32) { + if (head.colormap_size != 15 && head.colormap_size != 16 && head.colormap_size != 24 && head.colormap_size != 32) { return false; } } @@ -119,7 +386,7 @@ static bool IsSupported(const TgaHeader &head) if (head.width == 0 || head.height == 0) { return false; } - if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) { + if (head.pixel_size != 8 && head.pixel_size != 15 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) { return false; } // If the colormap_type field is set to zero, indicating that no color map exists, then colormap_index and colormap_length should be set to zero. @@ -130,11 +397,17 @@ static bool IsSupported(const TgaHeader &head) return true; } -struct Color555 { - ushort b : 5; - ushort g : 5; - ushort r : 5; -}; +/*! + * \brief imageId + * Create the TGA imageId from the image TITLE metadata + */ +static QByteArray imageId(const QImage &img) +{ + auto ba = img.text(QStringLiteral(META_KEY_TITLE)).trimmed().toLatin1(); + if (ba.size() > 255) + ba = ba.left(255); + return ba; +} struct TgaHeaderInfo { bool rle; @@ -202,6 +475,8 @@ static QImage::Format imageFormat(const TgaHeader &head) format = QImage::Format_Grayscale8; } else if (info.pal) { format = QImage::Format_Indexed8; + } else if (info.rgb && (head.pixel_size == 15 || head.pixel_size == 16)) { + format = QImage::Format_RGB555; } else { format = QImage::Format_RGB32; } @@ -305,23 +580,30 @@ static QByteArray readTgaLine(QIODevice *dev, qint32 pixel_size, qint32 size, bo return data; } +inline QRgb rgb555ToRgb(char c0, char c1) +{ + // c0 = GGGBBBBB + // c1 = IRRRRRGG (I = interrupt control of VDA(D) -> ignore it) + return qRgb(int((c1 >> 2) & 0x1F) * 255 / 31, int(((c1 & 3) << 3) | ((c0 >> 5) & 7)) * 255 / 31, int(c0 & 0x1F) * 255 / 31); +} + static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) { img = imageAlloc(tga.width, tga.height, imageFormat(tga)); if (img.isNull()) { - qWarning() << "LoadTGA: Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height); + qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height); return false; } TgaHeaderInfo info(tga); - const int numAlphaBits = tga.flags & 0xf; - bool hasAlpha = img.hasAlphaChannel(); - qint32 pixel_size = (tga.pixel_size / 8); + const int numAlphaBits = qBound(0, tga.flags & 0xf, 8); + bool hasAlpha = img.hasAlphaChannel() && numAlphaBits > 0; + qint32 pixel_size = (tga.pixel_size == 15 ? 16 : tga.pixel_size) / 8; qint32 line_size = qint32(tga.width) * pixel_size; qint64 size = qint64(tga.height) * line_size; if (size < 1) { - // qDebug() << "This TGA file is broken with size " << size; + // qCDebug(LOG_TGAPLUGIN) << "This TGA file is broken with size " << size; return false; } @@ -334,8 +616,8 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) colorTable.resizeForOverwrite(tga.colormap_length); #endif - if (tga.colormap_size == 32) { // BGRA. - char data[4]; + if (tga.colormap_size == 32) { + char data[4]; // BGRA for (QRgb &rgb : colorTable) { const auto dataRead = dev->read(data, 4); if (dataRead < 4) { @@ -344,8 +626,8 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) // BGRA. rgb = qRgba(data[2], data[1], data[0], data[3]); } - } else if (tga.colormap_size == 24) { // BGR. - char data[3]; + } else if (tga.colormap_size == 24) { + char data[3]; // BGR for (QRgb &rgb : colorTable) { const auto dataRead = dev->read(data, 3); if (dataRead < 3) { @@ -354,7 +636,15 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) // BGR. rgb = qRgb(data[2], data[1], data[0]); } - // TODO tga.colormap_size == 16 ARRRRRGG GGGBBBBB + } else if (tga.colormap_size == 16 || tga.colormap_size == 15) { + char data[2]; + for (QRgb &rgb : colorTable) { + const auto dataRead = dev->read(data, 2); + if (dataRead < 2) { + return false; + } + rgb = rgb555ToRgb(data[0], data[1]); + } } else { return false; } @@ -364,24 +654,28 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) // Convert image to internal format. bool valid = true; - int y_start; - int y_step; - int y_end; + int y_start = tga.height - 1; + int y_step = -1; + int y_end = -1; if (tga.flags & TGA_ORIGIN_UPPER) { y_start = 0; y_step = 1; y_end = tga.height; - } else { - y_start = tga.height - 1; - y_step = -1; - y_end = -1; + } + int x_start = 0; + int x_step = 1; + int x_end = tga.width; + if (tga.flags & TGA_ORIGIN_RIGHT) { + x_start = tga.width - 1; + x_step = -1; + x_end = -1; } QByteArray cache; for (int y = y_start; y != y_end; y += y_step) { auto tgaLine = readTgaLine(dev, pixel_size, line_size, info.rle, cache); if (tgaLine.size() != qsizetype(line_size)) { - qWarning() << "LoadTGA: Error while decoding a TGA raw line"; + qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Error while decoding a TGA raw line"; valid = false; break; } @@ -389,7 +683,7 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) if (info.pal && img.depth() == 8) { // Paletted. auto scanline = img.scanLine(y); - for (int x = 0; x < tga.width; x++) { + for (int x = x_start; x != x_end; x += x_step) { uchar idx = *src++; if (Q_UNLIKELY(idx >= tga.colormap_length)) { valid = false; @@ -400,13 +694,13 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) } else if (info.grey) { if (tga.pixel_size == 16 && img.depth() == 32) { // Greyscale with alpha. auto scanline = reinterpret_cast(img.scanLine(y)); - for (int x = 0; x < tga.width; x++) { + for (int x = x_start; x != x_end; x += x_step) { scanline[x] = qRgba(*src, *src, *src, *(src + 1)); src += 2; } } else if (tga.pixel_size == 8 && img.depth() == 8) { // Greyscale. auto scanline = img.scanLine(y); - for (int x = 0; x < tga.width; x++) { + for (int x = x_start; x != x_end; x += x_step) { scanline[x] = *src; src++; } @@ -415,25 +709,25 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) break; } } else { - auto scanline = reinterpret_cast(img.scanLine(y)); // True Color. - if (tga.pixel_size == 16 && img.depth() == 16) { - for (int x = 0; x < tga.width; x++) { - Color555 c = *reinterpret_cast(src); - scanline[x] = qRgb((c.r << 3) | (c.r >> 2), (c.g << 3) | (c.g >> 2), (c.b << 3) | (c.b >> 2)); + if ((tga.pixel_size == 15 || tga.pixel_size == 16) && img.depth() == 16) { + auto scanline = reinterpret_cast(img.scanLine(y)); + for (int x = x_start; x != x_end; x += x_step) { + scanline[x] = ((quint16(src[1] & 0x7f) << 8) | quint8(src[0])); src += 2; } } else if (tga.pixel_size == 24 && img.depth() == 32) { - for (int x = 0; x < tga.width; x++) { + auto scanline = reinterpret_cast(img.scanLine(y)); + for (int x = x_start; x != x_end; x += x_step) { scanline[x] = qRgb(src[2], src[1], src[0]); src += 3; } } else if (tga.pixel_size == 32 && img.depth() == 32) { + auto scanline = reinterpret_cast(img.scanLine(y)); auto div = (1 << numAlphaBits) - 1; if (div == 0) hasAlpha = false; - for (int x = 0; x < tga.width; x++) { - // ### TODO: verify with images having really some alpha data + for (int x = x_start; x != x_end; x += x_step) { const int alpha = hasAlpha ? int((src[3]) << (8 - numAlphaBits)) * 255 / div : 255; scanline[x] = qRgba(src[2], src[1], src[0], alpha); src += 4; @@ -445,11 +739,9 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) } } -#ifdef QT_DEBUG if (!cache.isEmpty() && valid) { - qDebug() << "LoadTGA: Found unused image data"; + qCDebug(LOG_TGAPLUGIN) << "LoadTGA: Found unused image data"; } -#endif return valid; } @@ -459,10 +751,32 @@ static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img) class TGAHandlerPrivate { public: - TGAHandlerPrivate() {} + TGAHandlerPrivate() +#ifdef TGA_V2E_AS_DEFAULT + : m_subType(subTypeTGA_V2E()) +#else + : m_subType(subTypeTGA_V2S()) +#endif + { + } ~TGAHandlerPrivate() {} + static QByteArray subTypeTGA_V1() + { + return QByteArrayLiteral("TGAv1"); + } + static QByteArray subTypeTGA_V2S() + { + return QByteArrayLiteral("TGAv2"); + } + static QByteArray subTypeTGA_V2E() + { + return QByteArrayLiteral("TGAv2E"); + } + TgaHeader m_header; + + QByteArray m_subType; }; TGAHandler::TGAHandler() @@ -482,31 +796,43 @@ bool TGAHandler::canRead() const bool TGAHandler::read(QImage *outImage) { - // qDebug() << "Loading TGA file!"; + // qCDebug(LOG_TGAPLUGIN) << "Loading TGA file!"; auto dev = device(); auto&& tga = d->m_header; if (!peekHeader(dev, tga) || !IsSupported(tga)) { - // qDebug() << "This TGA file is not valid."; + // qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid."; return false; } + QByteArray imageId; if (dev->isSequential()) { - dev->read(TgaHeader::SIZE + tga.id_length); + auto tmp = dev->read(TgaHeader::SIZE); + if (tmp.size() != TgaHeader::SIZE) + return false; } else { - dev->seek(TgaHeader::SIZE + tga.id_length); + if (!dev->seek(TgaHeader::SIZE)) + return false; + } + if (tga.id_length > 0) { + imageId = dev->read(tga.id_length); } // Check image file format. if (dev->atEnd()) { - // qDebug() << "This TGA file is not valid."; + // qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid."; return false; } QImage img; if (!LoadTGA(dev, tga, img)) { - // qDebug() << "Error loading TGA file."; + // qCDebug(LOG_TGAPLUGIN) << "Error loading TGA file."; return false; + } else if (!imageId.isEmpty()) { + img.setText(QStringLiteral(META_KEY_TITLE), QString::fromLatin1(imageId)); + } + if (!readMetadata(img)) { + qCDebug(LOG_TGAPLUGIN) << "read: error while reading metadata"; } *outImage = img; @@ -515,163 +841,434 @@ bool TGAHandler::read(QImage *outImage) bool TGAHandler::write(const QImage &image) { - if (image.format() == QImage::Format_Indexed8) - return writeIndexed(image); - if (image.format() == QImage::Format_Grayscale8 || image.format() == QImage::Format_Grayscale16) - return writeGrayscale(image); - return writeRGBA(image); + auto ok = false; + if (image.format() == QImage::Format_Indexed8) { + ok = writeIndexed(image); + } else if (image.format() == QImage::Format_Grayscale8 || image.format() == QImage::Format_Grayscale16) { + ok = writeGrayscale(image); + } else if (image.format() == QImage::Format_RGB555 || image.format() == QImage::Format_RGB16 || image.format() == QImage::Format_RGB444) { + ok = writeRGB555(image); + } else { + ok = writeRGBA(image); + } + return (ok && writeMetadata(image)); } bool TGAHandler::writeIndexed(const QImage &image) { - QDataStream s(device()); - s.setByteOrder(QDataStream::LittleEndian); + auto dev = device(); + { // write header and palette + QDataStream s(dev); + s.setByteOrder(QDataStream::LittleEndian); - QImage img(image); - auto ct = img.colorTable(); + auto ct = image.colorTable(); + auto iid = imageId(image); + s << quint8(iid.size()); // ID Length + s << quint8(1); // Color Map Type + s << quint8(TGA_TYPE_INDEXED); // Image Type + s << quint16(0); // First Entry Index + s << quint16(ct.size()); // Color Map Length + s << quint8(32); // Color map Entry Size + s << quint16(0); // X-origin of Image + s << quint16(0); // Y-origin of Image - s << quint8(0); // ID Length - s << quint8(1); // Color Map Type - s << quint8(TGA_TYPE_INDEXED); // Image Type - s << quint16(0); // First Entry Index - s << quint16(ct.size()); // Color Map Length - s << quint8(32); // Color map Entry Size - s << quint16(0); // X-origin of Image - s << quint16(0); // Y-origin of Image + s << quint16(image.width()); // Image Width + s << quint16(image.height()); // Image Height + s << quint8(8); // Pixel Depth + s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor - s << quint16(img.width()); // Image Width - s << quint16(img.height()); // Image Height - s << quint8(8); // Pixel Depth - s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor + if (!iid.isEmpty()) + s.writeRawData(iid.data(), iid.size()); - for (auto &&rgb : ct) { - s << quint8(qBlue(rgb)); - s << quint8(qGreen(rgb)); - s << quint8(qRed(rgb)); - s << quint8(qAlpha(rgb)); - } - - if (s.status() != QDataStream::Ok) { - return false; - } - - for (int y = 0; y < img.height(); y++) { - auto ptr = img.constScanLine(y); - for (int x = 0; x < img.width(); x++) { - s << *(ptr + x); + for (auto &&rgb : ct) { + s << quint8(qBlue(rgb)); + s << quint8(qGreen(rgb)); + s << quint8(qRed(rgb)); + s << quint8(qAlpha(rgb)); } + if (s.status() != QDataStream::Ok) { return false; } } + for (int y = 0, h = image.height(), w = image.width(); y < h; y++) { + auto ptr = reinterpret_cast(image.constScanLine(y)); + if (dev->write(ptr, w) != w) { + return false; + } + } + return true; } bool TGAHandler::writeGrayscale(const QImage &image) { - QDataStream s(device()); - s.setByteOrder(QDataStream::LittleEndian); + auto dev = device(); + { // write header + QDataStream s(dev); + s.setByteOrder(QDataStream::LittleEndian); - QImage img(image); - if (img.format() != QImage::Format_Grayscale8) { - img = img.convertToFormat(QImage::Format_Grayscale8); - } - if (img.isNull()) { - qCritical() << "TGAHandler::writeGrayscale: image conversion to 8 bits grayscale failed!"; - return false; - } + auto iid = imageId(image); + s << quint8(iid.size()); // ID Length + s << quint8(0); // Color Map Type + s << quint8(TGA_TYPE_GREY); // Image Type + s << quint16(0); // First Entry Index + s << quint16(0); // Color Map Length + s << quint8(0); // Color map Entry Size + s << quint16(0); // X-origin of Image + s << quint16(0); // Y-origin of Image - s << quint8(0); // ID Length - s << quint8(0); // Color Map Type - s << quint8(TGA_TYPE_GREY); // Image Type - s << quint16(0); // First Entry Index - s << quint16(0); // Color Map Length - s << quint8(0); // Color map Entry Size - s << quint16(0); // X-origin of Image - s << quint16(0); // Y-origin of Image + s << quint16(image.width()); // Image Width + s << quint16(image.height()); // Image Height + s << quint8(8); // Pixel Depth + s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor - s << quint16(img.width()); // Image Width - s << quint16(img.height()); // Image Height - s << quint8(8); // Pixel Depth - s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor + if (!iid.isEmpty()) + s.writeRawData(iid.data(), iid.size()); - if (s.status() != QDataStream::Ok) { - return false; - } - - for (int y = 0; y < img.height(); y++) { - auto ptr = img.constScanLine(y); - for (int x = 0; x < img.width(); x++) { - s << *(ptr + x); - } if (s.status() != QDataStream::Ok) { return false; } } + ScanLineConverter scl(QImage::Format_Grayscale8); + for (int y = 0, h = image.height(), w = image.width(); y < h; y++) { + auto ptr = reinterpret_cast(scl.convertedScanLine(image, y)); + if (dev->write(ptr, w) != w) { + return false; + } + } + + return true; +} + +bool TGAHandler::writeRGB555(const QImage &image) +{ + auto dev = device(); + { // write header + QDataStream s(dev); + s.setByteOrder(QDataStream::LittleEndian); + + auto iid = imageId(image); + for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) { + s << c; + } + s << quint16(image.width()); // width + s << quint16(image.height()); // height + s << quint8(16); // depth + s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); + + if (!iid.isEmpty()) + s.writeRawData(iid.data(), iid.size()); + + if (s.status() != QDataStream::Ok) { + return false; + } + } + + ScanLineConverter scl(QImage::Format_RGB555); + QByteArray ba(image.width() * 2, char()); + for (int y = 0, h = image.height(); y < h; y++) { + auto ptr = reinterpret_cast(scl.convertedScanLine(image, y)); + for (int x = 0, w = image.width(); x < w; x++) { + auto color = *(ptr + x); + ba[x * 2] = char(color); + ba[x * 2 + 1] = char(color >> 8); + } + if (dev->write(ba.data(), ba.size()) != qint64(ba.size())) { + return false; + } + } + return true; } bool TGAHandler::writeRGBA(const QImage &image) { - QDataStream s(device()); - s.setByteOrder(QDataStream::LittleEndian); - - QImage img(image); - const bool hasAlpha = img.hasAlphaChannel(); + auto format = image.format(); + const bool hasAlpha = image.hasAlphaChannel(); #if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) auto cs = image.colorSpace(); + auto tcs = QColorSpace(); if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) { - img = image.convertedToColorSpace(QColorSpace(QColorSpace::SRgb), QImage::Format_RGB32); - } else if (hasAlpha && img.format() != QImage::Format_ARGB32) { + format = QImage::Format_RGB32; + tcs = QColorSpace(QColorSpace::SRgb); + } else if (hasAlpha && image.format() != QImage::Format_ARGB32) { #else - if (hasAlpha && img.format() != QImage::Format_ARGB32) { + if (hasAlpha && image.format() != QImage::Format_ARGB32) { #endif - img = img.convertToFormat(QImage::Format_ARGB32); - } else if (!hasAlpha && img.format() != QImage::Format_RGB32) { - img = img.convertToFormat(QImage::Format_RGB32); - } - if (img.isNull()) { - qCritical() << "TGAHandler::writeRGBA: image conversion to 32 bits failed!"; - return false; - } - static constexpr quint8 originTopLeft = TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT; // 0x20 - static constexpr quint8 alphaChannel8Bits = 0x08; - - for (int i = 0; i < 12; i++) { - s << targaMagic[i]; + format = QImage::Format_ARGB32; + } else if (!hasAlpha && image.format() != QImage::Format_RGB32) { + format = QImage::Format_RGB32; } - // write header - s << quint16(img.width()); // width - s << quint16(img.height()); // height - s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha) - s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8) + auto dev = device(); + { // write header + QDataStream s(dev); + s.setByteOrder(QDataStream::LittleEndian); - if (s.status() != QDataStream::Ok) { - return false; - } + const quint8 originTopLeft = TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT; // 0x20 + const quint8 alphaChannel8Bits = 0x08; - for (int y = 0; y < img.height(); y++) { - auto ptr = reinterpret_cast(img.constScanLine(y)); - for (int x = 0; x < img.width(); x++) { - auto color = *(ptr + x); - s << quint8(qBlue(color)); - s << quint8(qGreen(color)); - s << quint8(qRed(color)); - if (hasAlpha) { - s << quint8(qAlpha(color)); - } + auto iid = imageId(image); + for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) { + s << c; } + s << quint16(image.width()); // width + s << quint16(image.height()); // height + s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha) + s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8) + + if (!iid.isEmpty()) + s.writeRawData(iid.data(), iid.size()); + if (s.status() != QDataStream::Ok) { return false; } } + ScanLineConverter scl(format); + if (tcs.isValid()) { + scl.setTargetColorSpace(tcs); + } + auto mul = hasAlpha ? 4 : 3; + QByteArray ba(image.width() * mul, char()); + for (int y = 0, h = image.height(); y < h; y++) { + auto ptr = reinterpret_cast(scl.convertedScanLine(image, y)); + for (int x = 0, w = image.width(); x < w; x++) { + auto color = *(ptr + x); + auto xmul = x * mul; + ba[xmul] = char(qBlue(color)); + ba[xmul + 1] = char(qGreen(color)); + ba[xmul + 2] = char(qRed(color)); + if (hasAlpha) { + ba[xmul + 3] = char(qAlpha(color)); + } + } + if (dev->write(ba.data(), ba.size()) != qint64(ba.size())) { + return false; + } + } + return true; } +bool TGAHandler::writeMetadata(const QImage &image) +{ + if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V1()) { + return true; // TGA V1 does not have these data + } + + auto dev = device(); + if (dev == nullptr) { + return false; + } + if (dev->isSequential()) { + qCInfo(LOG_TGAPLUGIN) << "writeMetadata: unable to save metadata on a sequential device"; + return true; + } + + QDataStream s(dev); + s.setByteOrder(QDataStream::LittleEndian); + + // TGA 2.0 footer + TgaFooter foot; + + // 32-bit overflow check (rough check) + // I need at least 495 (extension) + 26 (footer) bytes -> 1024 bytes. + // for the development area I roughly estimate 4096 KiB (profile, exif and xmp) they should always be less. + auto reqBytes = qint64(d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E() ? 4096 * 1024 : 1024); + if (dev->pos() > std::numeric_limits::max() - reqBytes) { + qCInfo(LOG_TGAPLUGIN) << "writeMetadata: there is no enough space for metadata"; + return true; + } + + // TGA 2.0 developer area + TgaDeveloperDirectory dir; + if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E()) { + auto exif = MicroExif::fromImage(image); + if (!exif.isEmpty()) { + auto ba = QByteArray("eXif").append(exif.toByteArray(s.byteOrder())); + TgaDeveloperDirectory::Field f; + f.tagId = TGA_EXIF_TAGID; + f.offset = dev->pos(); + f.size = ba.size(); + if (s.writeRawData(ba.data(), ba.size()) != ba.size()) { + return false; + } + dir.fields << f; + } + auto icc = image.colorSpace().iccProfile(); + if (!icc.isEmpty()) { + auto ba = QByteArray("iCCP").append(icc); + TgaDeveloperDirectory::Field f; + f.tagId = TGA_ICCP_TAGID; + f.offset = dev->pos(); + f.size = ba.size(); + if (s.writeRawData(ba.data(), ba.size()) != ba.size()) { + return false; + } + dir.fields << f; + } + auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).trimmed(); + if (!xmp.isEmpty()) { + auto ba = QByteArray("xMPP").append(xmp.toUtf8()); + TgaDeveloperDirectory::Field f; + f.tagId = TGA_XMPP_TAGID; + f.offset = dev->pos(); + f.size = ba.size(); + if (s.writeRawData(ba.data(), ba.size()) != ba.size()) { + return false; + } + dir.fields << f; + } + } + + // TGA 2.0 extension area + TgaExtension ext; + ext.setDateTime(QDateTime::currentDateTimeUtc()); + if (image.hasAlphaChannel()) { + ext.attributesType = TgaExtension::Alpha; + } + auto keys = image.textKeys(); + for (auto &&key : keys) { + if (!key.compare(QStringLiteral(META_KEY_AUTHOR), Qt::CaseInsensitive)) { + ext.setAuthor(image.text(key)); + continue; + } + if (!key.compare(QStringLiteral(META_KEY_COMMENT), Qt::CaseInsensitive)) { + ext.setComment(image.text(key)); + continue; + } + if (!key.compare(QStringLiteral(META_KEY_DESCRIPTION), Qt::CaseInsensitive)) { + if (ext.comment().isEmpty()) + ext.setComment(image.text(key)); + continue; + } + if (!key.compare(QStringLiteral(META_KEY_SOFTWARE), Qt::CaseInsensitive)) { + ext.setSoftware(image.text(key)); + continue; + } + } + + // write developer area + if (!dir.isEmpty()) { + foot.developerOffset = dev->pos(); + s << dir; + } + + // write extension area (date time is always set) + foot.extensionOffset = dev->pos(); + s << ext; + s << foot; + + return s.status() == QDataStream::Ok; +} + +bool TGAHandler::readMetadata(QImage &image) +{ + auto dev = device(); + if (dev == nullptr) { + return false; + } + if (dev->isSequential()) { + qCInfo(LOG_TGAPLUGIN) << "readMetadata: unable to load metadata on a sequential device"; + return true; + } + + // read TGA footer + if (!dev->seek(dev->size() - 26)) { + return false; + } + + QDataStream s(dev); + s.setByteOrder(QDataStream::LittleEndian); + + TgaFooter foot; + s >> foot; + if (s.status() != QDataStream::Ok) { + return false; + } + if (!foot.isValid()) { + return true; // not a TGA 2.0 -> no metadata are present + } + + if (foot.extensionOffset > 0) { + // read the extension area + if (!dev->seek(foot.extensionOffset)) { + return false; + } + + TgaExtension ext; + s >> ext; + if (s.status() != QDataStream::Ok || !ext.isValid()) { + return false; + } + + auto dt = ext.dateTime(); + if (dt.isValid()) { + image.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), dt.toString(Qt::ISODate)); + } + auto au = ext.author(); + if (!au.isEmpty()) { + image.setText(QStringLiteral(META_KEY_AUTHOR), au); + } + auto cm = ext.comment(); + if (!cm.isEmpty()) { + image.setText(QStringLiteral(META_KEY_COMMENT), cm); + } + auto sw = ext.software(); + if (!sw.isEmpty()) { + image.setText(QStringLiteral(META_KEY_SOFTWARE), sw); + } + } + + if (foot.developerOffset > 0) { + // read developer area + if (!dev->seek(foot.developerOffset)) { + return false; + } + + TgaDeveloperDirectory dir; + s >> dir; + if (s.status() != QDataStream::Ok) { + return false; + } + + for (auto &&f : dir.fields) { + if (!dev->seek(f.offset)) { + return false; + } + if (f.tagId == TGA_EXIF_TAGID) { + auto ba = dev->read(f.size); + if (ba.startsWith(QByteArray("eXif"))) { + auto exif = MicroExif::fromByteArray(ba.mid(4)); + exif.updateImageMetadata(image, true); + exif.updateImageResolution(image); + } + continue; + } + if (f.tagId == TGA_ICCP_TAGID) { + auto ba = dev->read(f.size); + if (ba.startsWith(QByteArray("iCCP"))) { + image.setColorSpace(QColorSpace::fromIccProfile(ba.mid(4))); + } + continue; + } + if (f.tagId == TGA_XMPP_TAGID) { + auto ba = dev->read(f.size); + if (ba.startsWith(QByteArray("xMPP"))) { + image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(ba.mid(4))); + } + continue; + } + } + } + + return s.status() == QDataStream::Ok; +} + bool TGAHandler::supportsOption(ImageOption option) const { if (option == QImageIOHandler::Size) { @@ -680,48 +1277,74 @@ bool TGAHandler::supportsOption(ImageOption option) const if (option == QImageIOHandler::ImageFormat) { return true; } + if (option == QImageIOHandler::SubType) { + return true; + } + if (option == QImageIOHandler::SupportedSubTypes) { + return true; + } return false; } +void TGAHandler::setOption(ImageOption option, const QVariant &value) +{ + if (option == QImageIOHandler::SubType) { + auto subType = value.toByteArray(); + auto list = TGAHandler::option(QImageIOHandler::SupportedSubTypes).value>(); + if (list.contains(subType)) { + d->m_subType = subType; + } else { + d->m_subType = TGAHandlerPrivate::subTypeTGA_V2S(); + } + } +} + QVariant TGAHandler::option(ImageOption option) const { - QVariant v; + if (!supportsOption(option)) { + return {}; + } + + if (option == QImageIOHandler::SupportedSubTypes) { + return QVariant::fromValue(QList() + << TGAHandlerPrivate::subTypeTGA_V1() << TGAHandlerPrivate::subTypeTGA_V2S() << TGAHandlerPrivate::subTypeTGA_V2E()); + } + + if (option == QImageIOHandler::SubType) { + return QVariant::fromValue(d->m_subType); + } + + auto &&header = d->m_header; + if (!IsSupported(header)) { + if (auto dev = device()) + if (!peekHeader(dev, header) && IsSupported(header)) + return {}; + if (!IsSupported(header)) { + return {}; + } + } if (option == QImageIOHandler::Size) { - auto&& header = d->m_header; - if (IsSupported(header)) { - v = QVariant::fromValue(QSize(header.width, header.height)); - } else if (auto dev = device()) { - if (peekHeader(dev, header) && IsSupported(header)) { - v = QVariant::fromValue(QSize(header.width, header.height)); - } - } + return QVariant::fromValue(QSize(header.width, header.height)); } if (option == QImageIOHandler::ImageFormat) { - auto&& header = d->m_header; - if (IsSupported(header)) { - v = QVariant::fromValue(imageFormat(header)); - } else if (auto dev = device()) { - if (peekHeader(dev, header) && IsSupported(header)) { - v = QVariant::fromValue(imageFormat(header)); - } - } + return QVariant::fromValue(imageFormat(header)); } - return v; + return {}; } bool TGAHandler::canRead(QIODevice *device) { if (!device) { - qWarning("TGAHandler::canRead() called with no device"); + qCWarning(LOG_TGAPLUGIN) << "TGAHandler::canRead() called with no device"; return false; } TgaHeader tga; if (!peekHeader(device, tga)) { - qWarning("TGAHandler::canRead() error while reading the header"); + qCWarning(LOG_TGAPLUGIN) << "TGAHandler::canRead() error while reading the header"; return false; } diff --git a/src/imageformats/tga_p.h b/src/imageformats/tga_p.h index aaafbf0..ca083fb 100644 --- a/src/imageformats/tga_p.h +++ b/src/imageformats/tga_p.h @@ -22,17 +22,20 @@ public: bool write(const QImage &image) override; bool supportsOption(QImageIOHandler::ImageOption option) const override; + void setOption(ImageOption option, const QVariant &value) override; QVariant option(QImageIOHandler::ImageOption option) const override; static bool canRead(QIODevice *device); private: bool writeIndexed(const QImage &image); - bool writeGrayscale(const QImage &image); - + bool writeRGB555(const QImage &image); bool writeRGBA(const QImage &image); + bool writeMetadata(const QImage &image); + bool readMetadata(QImage &image); + const QScopedPointer d; };