1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 2003 Dominik Seichter <domseichter@web.de>
4 SPDX-FileCopyrightText: 2004 Ignacio CastaƱo <castano@ludicon.com>
5 SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10/* this code supports:
11 * reading:
12 * uncompressed and run length encoded indexed, grey and color tga files.
13 * image types 1, 2, 3, 9, 10 and 11.
14 * only RGB color maps with no more than 256 colors.
15 * pixel formats 8, 15, 16, 24 and 32.
16 * writing:
17 * uncompressed rgb color tga files
18 * uncompressed grayscale tga files
19 * uncompressed indexed tga files
20 */
21
22#include "microexif_p.h"
23#include "scanlineconverter_p.h"
24#include "tga_p.h"
25#include "util_p.h"
26
27#include <assert.h>
28
29#include <QColorSpace>
30#include <QDataStream>
31#include <QDateTime>
32#include <QImage>
33#include <QLoggingCategory>
34
35typedef quint32 uint;
36typedef quint16 ushort;
37typedef quint8 uchar;
38
39#ifdef QT_DEBUG
40Q_LOGGING_CATEGORY(LOG_TGAPLUGIN, "kf.imageformats.plugins.tga", QtDebugMsg)
41#else
42Q_LOGGING_CATEGORY(LOG_TGAPLUGIN, "kf.imageformats.plugins.tga", QtWarningMsg)
43#endif
44
45#ifndef TGA_V2E_AS_DEFAULT
46/*
47 * Uncomment to change the default version of the plugin to `TGAv2E`.
48 */
49// #define TGA_V2E_AS_DEFAULT
50#endif // TGA_V2E_AS_DEFAULT
51
52namespace // Private.
53{
54// Header format of saved files.
55enum TGAType {
56 TGA_TYPE_INDEXED = 1,
57 TGA_TYPE_RGB = 2,
58 TGA_TYPE_GREY = 3,
59 TGA_TYPE_RLE_INDEXED = 9,
60 TGA_TYPE_RLE_RGB = 10,
61 TGA_TYPE_RLE_GREY = 11,
62};
63
64#define TGA_INTERLEAVE_MASK 0xc0
65#define TGA_INTERLEAVE_NONE 0x00
66#define TGA_INTERLEAVE_2WAY 0x40
67#define TGA_INTERLEAVE_4WAY 0x80
68
69#define TGA_ORIGIN_MASK 0x30
70#define TGA_ORIGIN_LEFT 0x00
71#define TGA_ORIGIN_RIGHT 0x10
72#define TGA_ORIGIN_LOWER 0x00
73#define TGA_ORIGIN_UPPER 0x20
74
75/*
76 * Each TAG is a SHORT value in the range of 0 to 65535. Values from 0 - 32767 are available
77 * for developer use, while values from 32768 - 65535 are reserved for Truevision.
78 * Truevision will maintain a list of tags assigned to companies.
79 * In any case, there's no public "list of tag" and Truevision no longer exists.
80 */
81#define TGA_EXIF_TAGID 0x7001 // Exif data preceded by "eXif" string
82#define TGA_XMPP_TAGID 0x7002 // Xmp packet preceded by "xMPP" string
83#define TGA_ICCP_TAGID 0x7003 // Icc profile preceded by "iCCP" string
84
85/** Tga Header. */
86struct TgaHeader {
87 uchar id_length = 0;
88 uchar colormap_type = 0;
89 uchar image_type = 0;
90 ushort colormap_index = 0;
91 ushort colormap_length = 0;
92 uchar colormap_size = 0;
93 ushort x_origin = 0;
94 ushort y_origin = 0;
95 ushort width = 0;
96 ushort height = 0;
97 uchar pixel_size = 0;
98 uchar flags = 0;
99
100 enum {
101 SIZE = 18,
102 }; // const static int SIZE = 18;
103};
104
105/** Tga 2.0 Footer */
106struct TgaFooter {
107 TgaFooter()
108 : extensionOffset(0)
109 , developerOffset(0)
110 {
111 std::memcpy(dest: signature, src: "TRUEVISION-XFILE.\0", n: 18);
112 }
113 bool isValid() const
114 {
115 return std::memcmp(s1: signature, s2: "TRUEVISION-XFILE.\0", n: 18) == 0;
116 }
117
118 quint32 extensionOffset; // Extension Area Offset
119 quint32 developerOffset; // Developer Directory Offset
120 char signature[18]; // TGA Signature
121};
122
123/** Tga 2.0 extension area */
124struct TgaExtension {
125 enum AttributeType : quint16 {
126 NoAlpha = 0, // no Alpha data included (bits 3-0 of TgaHeader::flags should also be set to zero).
127 IgnoreAlpha = 1, // undefined data in the Alpha field, can be ignored
128 RetainAlpha = 2, // undefined data in the Alpha field, but should be retained
129 Alpha = 3, // useful Alpha channel data is present
130 PremultipliedAlpha = 4 // pre-multiplied Alpha (see description below)
131 };
132
133 TgaExtension()
134 {
135 std::memset(s: this, c: 0, n: sizeof(TgaExtension));
136 size = 495; // TGA 2.0 specs
137
138 // If you do not use Software Version field, set the SHORT to binary
139 // zero, and the BYTE to a space (' ').
140 versionLetter = 0x20;
141 }
142
143 bool isValid() const
144 {
145 return size == 495;
146 }
147
148 void setDateTime(const QDateTime &dt)
149 {
150 if (dt.isValid()) {
151 auto date = dt.date();
152 stampMonth = date.month();
153 stampDay = date.day();
154 stampYear = date.year();
155 auto time = dt.time();
156 stampHour = time.hour();
157 stampMinute = time.minute();
158 stampSecond = time.second();
159 }
160 }
161 QDateTime dateTime() const
162 {
163 auto date = QDate(stampYear, stampMonth, stampDay);
164 auto time = QTime(stampHour, stampMinute, stampSecond);
165 if (!date.isValid() || !time.isValid())
166 return {};
167 return QDateTime(date, time);
168 }
169
170 void setAuthor(const QString &str)
171 {
172 auto ba = str.toLatin1();
173 std::memcpy(dest: authorName, src: ba.data(), n: std::min(a: sizeof(authorName) - 1, b: size_t(ba.size())));
174 }
175 QString author() const
176 {
177 if (authorName[sizeof(authorName) - 1] != char(0))
178 return {};
179 return QString::fromLatin1(ba: authorName);
180 }
181
182 void setComment(const QString &str)
183 {
184 auto ba = str.toLatin1();
185 std::memcpy(dest: authorComment, src: ba.data(), n: std::min(a: sizeof(authorComment) - 1, b: size_t(ba.size())));
186 }
187 QString comment() const
188 {
189 if (authorComment[sizeof(authorComment) - 1] != char(0))
190 return {};
191 return QString::fromLatin1(ba: authorComment);
192 }
193
194 void setSoftware(const QString &str)
195 {
196 auto ba = str.toLatin1();
197 std::memcpy(dest: softwareId, src: ba.data(), n: std::min(a: sizeof(softwareId) - 1, b: size_t(ba.size())));
198 }
199 QString software() const
200 {
201 if (softwareId[sizeof(softwareId) - 1] != char(0))
202 return {};
203 return QString::fromLatin1(ba: softwareId);
204 }
205
206 quint16 size; // Extension Size
207 char authorName[41]; // Author Name
208 char authorComment[324]; // Author Comment
209 quint16 stampMonth; // Date/Time Stamp: Month
210 quint16 stampDay; // Date/Time Stamp: Day
211 quint16 stampYear; // Date/Time Stamp: Year
212 quint16 stampHour; // Date/Time Stamp: Hour
213 quint16 stampMinute; // Date/Time Stamp: Minute
214 quint16 stampSecond; // Date/Time Stamp: Second
215 char jobName[41]; // Job Name/ID
216 quint16 jobHour; // Job Time: Hours
217 quint16 jobMinute; // Job Time: Minutes
218 quint16 jobSecond; // Job Time: Seconds
219 char softwareId[41]; // Software ID
220 quint16 versionNumber; // Software Version Number
221 quint8 versionLetter; // Software Version Letter
222 quint32 keyColor; // Key Color
223 quint16 pixelNumerator; // Pixel Aspect Ratio
224 quint16 pixelDenominator; // Pixel Aspect Ratio
225 quint16 gammaNumerator; // Gamma Value
226 quint16 gammaDenominator; // Gamma Value
227 quint32 colorOffset; // Color Correction Offset
228 quint32 stampOffset; // Postage Stamp Offset
229 quint32 scanOffset; // Scan-Line Table Offset
230 quint8 attributesType; // Attributes Types
231};
232
233struct TgaDeveloperDirectory {
234 struct Field {
235 quint16 tagId;
236 quint32 offset;
237 quint32 size;
238 };
239
240 bool isEmpty() const
241 {
242 return fields.isEmpty();
243 }
244
245 QList<Field> fields;
246};
247
248static QDataStream &operator>>(QDataStream &s, TgaHeader &head)
249{
250 s >> head.id_length;
251 s >> head.colormap_type;
252 s >> head.image_type;
253 s >> head.colormap_index;
254 s >> head.colormap_length;
255 s >> head.colormap_size;
256 s >> head.x_origin;
257 s >> head.y_origin;
258 s >> head.width;
259 s >> head.height;
260 s >> head.pixel_size;
261 s >> head.flags;
262 return s;
263}
264
265static QDataStream &operator>>(QDataStream &s, TgaFooter &footer)
266{
267 s >> footer.extensionOffset;
268 s >> footer.developerOffset;
269 s.readRawData(footer.signature, len: sizeof(footer.signature));
270 return s;
271}
272
273static QDataStream &operator<<(QDataStream &s, const TgaFooter &footer)
274{
275 s << footer.extensionOffset;
276 s << footer.developerOffset;
277 s.writeRawData(footer.signature, len: sizeof(footer.signature));
278 return s;
279}
280
281static QDataStream &operator>>(QDataStream &s, TgaDeveloperDirectory &dir)
282{
283 quint16 n;
284 s >> n;
285 for (auto i = n; i > 0; --i) {
286 TgaDeveloperDirectory::Field f;
287 s >> f.tagId;
288 s >> f.offset;
289 s >> f.size;
290 dir.fields << f;
291 }
292 return s;
293}
294
295static QDataStream &operator<<(QDataStream &s, const TgaDeveloperDirectory &dir)
296{
297 s << quint16(dir.fields.size());
298 for (auto &&f : dir.fields) {
299 s << f.tagId;
300 s << f.offset;
301 s << f.size;
302 }
303 return s;
304}
305
306static QDataStream &operator>>(QDataStream &s, TgaExtension &ext)
307{
308 s >> ext.size;
309 s.readRawData(ext.authorName, len: sizeof(ext.authorName));
310 s.readRawData(ext.authorComment, len: sizeof(ext.authorComment));
311 s >> ext.stampMonth;
312 s >> ext.stampDay;
313 s >> ext.stampYear;
314 s >> ext.stampHour;
315 s >> ext.stampMinute;
316 s >> ext.stampSecond;
317 s.readRawData(ext.jobName, len: sizeof(ext.jobName));
318 s >> ext.jobHour;
319 s >> ext.jobMinute;
320 s >> ext.jobSecond;
321 s.readRawData(ext.softwareId, len: sizeof(ext.softwareId));
322 s >> ext.versionNumber;
323 s >> ext.versionLetter;
324 s >> ext.keyColor;
325 s >> ext.pixelNumerator;
326 s >> ext.pixelDenominator;
327 s >> ext.gammaNumerator;
328 s >> ext.gammaDenominator;
329 s >> ext.colorOffset;
330 s >> ext.stampOffset;
331 s >> ext.scanOffset;
332 s >> ext.attributesType;
333 return s;
334}
335
336static QDataStream &operator<<(QDataStream &s, const TgaExtension &ext)
337{
338 s << ext.size;
339 s.writeRawData(ext.authorName, len: sizeof(ext.authorName));
340 s.writeRawData(ext.authorComment, len: sizeof(ext.authorComment));
341 s << ext.stampMonth;
342 s << ext.stampDay;
343 s << ext.stampYear;
344 s << ext.stampHour;
345 s << ext.stampMinute;
346 s << ext.stampSecond;
347 s.writeRawData(ext.jobName, len: sizeof(ext.jobName));
348 s << ext.jobHour;
349 s << ext.jobMinute;
350 s << ext.jobSecond;
351 s.writeRawData(ext.softwareId, len: sizeof(ext.softwareId));
352 s << ext.versionNumber;
353 s << ext.versionLetter;
354 s << ext.keyColor;
355 s << ext.pixelNumerator;
356 s << ext.pixelDenominator;
357 s << ext.gammaNumerator;
358 s << ext.gammaDenominator;
359 s << ext.colorOffset;
360 s << ext.stampOffset;
361 s << ext.scanOffset;
362 s << ext.attributesType;
363 return s;
364}
365
366static bool IsSupported(const TgaHeader &head)
367{
368 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
369 && head.image_type != TGA_TYPE_RLE_RGB && head.image_type != TGA_TYPE_RLE_GREY) {
370 return false;
371 }
372 if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
373 // GIMP saves TGAs with palette size of 257 (but 256 used) so, I need to check the pixel size only.
374 if (head.pixel_size > 8 || head.colormap_type != 1) {
375 return false;
376 }
377 if (head.colormap_size != 15 && head.colormap_size != 16 && head.colormap_size != 24 && head.colormap_size != 32) {
378 return false;
379 }
380 }
381 if (head.image_type == TGA_TYPE_RGB || head.image_type == TGA_TYPE_GREY || head.image_type == TGA_TYPE_RLE_RGB || head.image_type == TGA_TYPE_RLE_GREY) {
382 if (head.colormap_type != 0) {
383 return false;
384 }
385 }
386 if (head.width == 0 || head.height == 0) {
387 return false;
388 }
389 if (head.pixel_size != 8 && head.pixel_size != 15 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
390 return false;
391 }
392 // 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.
393 if (head.colormap_type == 0 && (head.colormap_index != 0 || head.colormap_length != 0)) {
394 return false;
395 }
396
397 return true;
398}
399
400/*!
401 * \brief imageId
402 * Create the TGA imageId from the image TITLE metadata
403 */
404static QByteArray imageId(const QImage &img)
405{
406 auto ba = img.text(QStringLiteral(META_KEY_TITLE)).trimmed().toLatin1();
407 if (ba.size() > 255)
408 ba = ba.left(n: 255);
409 return ba;
410}
411
412struct TgaHeaderInfo {
413 bool rle;
414 bool pal;
415 bool rgb;
416 bool grey;
417
418 TgaHeaderInfo(const TgaHeader &tga)
419 : rle(false)
420 , pal(false)
421 , rgb(false)
422 , grey(false)
423 {
424 switch (tga.image_type) {
425 case TGA_TYPE_RLE_INDEXED:
426 rle = true;
427 Q_FALLTHROUGH();
428 // no break is intended!
429 case TGA_TYPE_INDEXED:
430 pal = true;
431 break;
432
433 case TGA_TYPE_RLE_RGB:
434 rle = true;
435 Q_FALLTHROUGH();
436 // no break is intended!
437 case TGA_TYPE_RGB:
438 rgb = true;
439 break;
440
441 case TGA_TYPE_RLE_GREY:
442 rle = true;
443 Q_FALLTHROUGH();
444 // no break is intended!
445 case TGA_TYPE_GREY:
446 grey = true;
447 break;
448
449 default:
450 // Error, unknown image type.
451 break;
452 }
453 }
454};
455
456static QImage::Format imageFormat(const TgaHeader &head)
457{
458 auto format = QImage::Format_Invalid;
459 if (IsSupported(head)) {
460 TgaHeaderInfo info(head);
461
462 // Bits 0-3 are the numbers of alpha bits (can be zero!)
463 const int numAlphaBits = head.flags & 0xf;
464 // However alpha should exists only in the 32 bit format.
465 if ((head.pixel_size == 32) && (numAlphaBits)) {
466 if (numAlphaBits <= 8) {
467 format = QImage::Format_ARGB32;
468 }
469 // Anyway, GIMP also saves gray images with alpha in TGA format
470 } else if ((info.grey) && (head.pixel_size == 16) && (numAlphaBits)) {
471 if (numAlphaBits == 8) {
472 format = QImage::Format_ARGB32;
473 }
474 } else if (info.grey) {
475 format = QImage::Format_Grayscale8;
476 } else if (info.pal) {
477 format = QImage::Format_Indexed8;
478 } else if (info.rgb && (head.pixel_size == 15 || head.pixel_size == 16)) {
479 format = QImage::Format_RGB555;
480 } else {
481 format = QImage::Format_RGB32;
482 }
483 }
484 return format;
485}
486
487/*!
488 * \brief peekHeader
489 * Reads the header but does not change the position in the device.
490 */
491static bool peekHeader(QIODevice *device, TgaHeader &header)
492{
493 auto head = device->peek(maxlen: TgaHeader::SIZE);
494 if (head.size() < TgaHeader::SIZE) {
495 return false;
496 }
497 QDataStream stream(head);
498 stream.setByteOrder(QDataStream::LittleEndian);
499 stream >> header;
500 return true;
501}
502
503/*!
504 * \brief readTgaLine
505 * Read a scan line from the raw data.
506 * \param dev The current device.
507 * \param pixel_size The number of bytes per pixel.
508 * \param size The size of the uncompressed TGA raw line
509 * \param rle True if the stream is RLE compressed, otherwise false.
510 * \param cache The cache buffer used to store data (only used when the stream is RLE).
511 * \return The uncompressed raw data of a line or an empty array on error.
512 */
513static QByteArray readTgaLine(QIODevice *dev, qint32 pixel_size, qint32 size, bool rle, QByteArray &cache)
514{
515 // uncompressed stream
516 if (!rle) {
517 auto ba = dev->read(maxlen: size);
518 if (ba.size() != size)
519 ba.clear();
520 return ba;
521 }
522
523 // RLE compressed stream
524 if (cache.size() < qsizetype(size)) {
525 // Decode image.
526 qint64 num = size;
527
528 while (num > 0) {
529 if (dev->atEnd()) {
530 break;
531 }
532
533 // Get packet header.
534 char cc;
535 if (dev->read(data: &cc, maxlen: 1) != 1) {
536 cache.clear();
537 break;
538 }
539 auto c = uchar(cc);
540
541 uint count = (c & 0x7f) + 1;
542 QByteArray tmp(count * pixel_size, char());
543 auto dst = tmp.data();
544 num -= count * pixel_size;
545
546 if (c & 0x80) { // RLE pixels.
547 assert(pixel_size <= 8);
548 char pixel[8];
549 const int dataRead = dev->read(data: pixel, maxlen: pixel_size);
550 if (dataRead < (int)pixel_size) {
551 memset(s: &pixel[dataRead], c: 0, n: pixel_size - dataRead);
552 }
553 do {
554 memcpy(dest: dst, src: pixel, n: pixel_size);
555 dst += pixel_size;
556 } while (--count);
557 } else { // Raw pixels.
558 count *= pixel_size;
559 const int dataRead = dev->read(data: dst, maxlen: count);
560 if (dataRead < 0) {
561 cache.clear();
562 break;
563 }
564
565 if ((uint)dataRead < count) {
566 const size_t toCopy = count - dataRead;
567 memset(s: &dst[dataRead], c: 0, n: toCopy);
568 }
569 dst += count;
570 }
571
572 cache.append(a: tmp);
573 }
574 }
575
576 auto data = cache.left(n: size);
577 cache.remove(index: 0, len: size);
578 if (data.size() != size)
579 data.clear();
580 return data;
581}
582
583inline QRgb rgb555ToRgb(char c0, char c1)
584{
585 // c0 = GGGBBBBB
586 // c1 = IRRRRRGG (I = interrupt control of VDA(D) -> ignore it)
587 return qRgb(r: int((c1 >> 2) & 0x1F) * 255 / 31, g: int(((c1 & 3) << 3) | ((c0 >> 5) & 7)) * 255 / 31, b: int(c0 & 0x1F) * 255 / 31);
588}
589
590static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img)
591{
592 img = imageAlloc(width: tga.width, height: tga.height, format: imageFormat(head: tga));
593 if (img.isNull()) {
594 qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
595 return false;
596 }
597
598 TgaHeaderInfo info(tga);
599
600 const int numAlphaBits = qBound(min: 0, val: tga.flags & 0xf, max: 8);
601 bool hasAlpha = img.hasAlphaChannel() && numAlphaBits > 0;
602 qint32 pixel_size = (tga.pixel_size == 15 ? 16 : tga.pixel_size) / 8;
603 qint32 line_size = qint32(tga.width) * pixel_size;
604 qint64 size = qint64(tga.height) * line_size;
605 if (size < 1) {
606 // qCDebug(LOG_TGAPLUGIN) << "This TGA file is broken with size " << size;
607 return false;
608 }
609
610 // Read palette.
611 if (info.pal) {
612 QList<QRgb> colorTable;
613#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
614 colorTable.resize(tga.colormap_length);
615#else
616 colorTable.resizeForOverwrite(size: tga.colormap_length);
617#endif
618
619 if (tga.colormap_size == 32) {
620 char data[4]; // BGRA
621 for (QRgb &rgb : colorTable) {
622 const auto dataRead = dev->read(data, maxlen: 4);
623 if (dataRead < 4) {
624 return false;
625 }
626 // BGRA.
627 rgb = qRgba(r: data[2], g: data[1], b: data[0], a: data[3]);
628 }
629 } else if (tga.colormap_size == 24) {
630 char data[3]; // BGR
631 for (QRgb &rgb : colorTable) {
632 const auto dataRead = dev->read(data, maxlen: 3);
633 if (dataRead < 3) {
634 return false;
635 }
636 // BGR.
637 rgb = qRgb(r: data[2], g: data[1], b: data[0]);
638 }
639 } else if (tga.colormap_size == 16 || tga.colormap_size == 15) {
640 char data[2];
641 for (QRgb &rgb : colorTable) {
642 const auto dataRead = dev->read(data, maxlen: 2);
643 if (dataRead < 2) {
644 return false;
645 }
646 rgb = rgb555ToRgb(c0: data[0], c1: data[1]);
647 }
648 } else {
649 return false;
650 }
651
652 img.setColorTable(colorTable);
653 }
654
655 // Convert image to internal format.
656 bool valid = true;
657 int y_start = tga.height - 1;
658 int y_step = -1;
659 int y_end = -1;
660 if (tga.flags & TGA_ORIGIN_UPPER) {
661 y_start = 0;
662 y_step = 1;
663 y_end = tga.height;
664 }
665 int x_start = 0;
666 int x_step = 1;
667 int x_end = tga.width;
668 if (tga.flags & TGA_ORIGIN_RIGHT) {
669 x_start = tga.width - 1;
670 x_step = -1;
671 x_end = -1;
672 }
673
674 QByteArray cache;
675 for (int y = y_start; y != y_end; y += y_step) {
676 auto tgaLine = readTgaLine(dev, pixel_size, size: line_size, rle: info.rle, cache);
677 if (tgaLine.size() != qsizetype(line_size)) {
678 qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Error while decoding a TGA raw line";
679 valid = false;
680 break;
681 }
682 auto src = tgaLine.data();
683 if (info.pal && img.depth() == 8) {
684 // Paletted.
685 auto scanline = img.scanLine(y);
686 for (int x = x_start; x != x_end; x += x_step) {
687 uchar idx = *src++;
688 if (Q_UNLIKELY(idx >= tga.colormap_length)) {
689 valid = false;
690 break;
691 }
692 scanline[x] = idx;
693 }
694 } else if (info.grey) {
695 if (tga.pixel_size == 16 && img.depth() == 32) { // Greyscale with alpha.
696 auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
697 for (int x = x_start; x != x_end; x += x_step) {
698 scanline[x] = qRgba(r: *src, g: *src, b: *src, a: *(src + 1));
699 src += 2;
700 }
701 } else if (tga.pixel_size == 8 && img.depth() == 8) { // Greyscale.
702 auto scanline = img.scanLine(y);
703 for (int x = x_start; x != x_end; x += x_step) {
704 scanline[x] = *src;
705 src++;
706 }
707 } else {
708 valid = false;
709 break;
710 }
711 } else {
712 // True Color.
713 if ((tga.pixel_size == 15 || tga.pixel_size == 16) && img.depth() == 16) {
714 auto scanline = reinterpret_cast<quint16 *>(img.scanLine(y));
715 for (int x = x_start; x != x_end; x += x_step) {
716 scanline[x] = ((quint16(src[1] & 0x7f) << 8) | quint8(src[0]));
717 src += 2;
718 }
719 } else if (tga.pixel_size == 24 && img.depth() == 32) {
720 auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
721 for (int x = x_start; x != x_end; x += x_step) {
722 scanline[x] = qRgb(r: src[2], g: src[1], b: src[0]);
723 src += 3;
724 }
725 } else if (tga.pixel_size == 32 && img.depth() == 32) {
726 auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
727 auto div = (1 << numAlphaBits) - 1;
728 if (div == 0)
729 hasAlpha = false;
730 for (int x = x_start; x != x_end; x += x_step) {
731 const int alpha = hasAlpha ? int((src[3]) << (8 - numAlphaBits)) * 255 / div : 255;
732 scanline[x] = qRgba(r: src[2], g: src[1], b: src[0], a: alpha);
733 src += 4;
734 }
735 } else {
736 valid = false;
737 break;
738 }
739 }
740 }
741
742 if (!cache.isEmpty() && valid) {
743 qCDebug(LOG_TGAPLUGIN) << "LoadTGA: Found unused image data";
744 }
745
746 return valid;
747}
748
749} // namespace
750
751class TGAHandlerPrivate
752{
753public:
754 TGAHandlerPrivate()
755#ifdef TGA_V2E_AS_DEFAULT
756 : m_subType(subTypeTGA_V2E())
757#else
758 : m_subType(subTypeTGA_V2S())
759#endif
760 {
761 }
762 ~TGAHandlerPrivate() {}
763
764 static QByteArray subTypeTGA_V1()
765 {
766 return QByteArrayLiteral("TGAv1");
767 }
768 static QByteArray subTypeTGA_V2S()
769 {
770 return QByteArrayLiteral("TGAv2");
771 }
772 static QByteArray subTypeTGA_V2E()
773 {
774 return QByteArrayLiteral("TGAv2E");
775 }
776
777 TgaHeader m_header;
778
779 QByteArray m_subType;
780};
781
782TGAHandler::TGAHandler()
783 : QImageIOHandler()
784 , d(new TGAHandlerPrivate)
785{
786}
787
788bool TGAHandler::canRead() const
789{
790 if (canRead(device: device())) {
791 setFormat("tga");
792 return true;
793 }
794 return false;
795}
796
797bool TGAHandler::read(QImage *outImage)
798{
799 // qCDebug(LOG_TGAPLUGIN) << "Loading TGA file!";
800
801 auto dev = device();
802 auto&& tga = d->m_header;
803 if (!peekHeader(device: dev, header&: tga) || !IsSupported(head: tga)) {
804 // qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid.";
805 return false;
806 }
807
808 QByteArray imageId;
809 if (dev->isSequential()) {
810 auto tmp = dev->read(maxlen: TgaHeader::SIZE);
811 if (tmp.size() != TgaHeader::SIZE)
812 return false;
813 } else {
814 if (!dev->seek(pos: TgaHeader::SIZE))
815 return false;
816 }
817 if (tga.id_length > 0) {
818 imageId = dev->read(maxlen: tga.id_length);
819 }
820
821 // Check image file format.
822 if (dev->atEnd()) {
823 // qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid.";
824 return false;
825 }
826
827 QImage img;
828 if (!LoadTGA(dev, tga, img)) {
829 // qCDebug(LOG_TGAPLUGIN) << "Error loading TGA file.";
830 return false;
831 } else if (!imageId.isEmpty()) {
832 img.setText(QStringLiteral(META_KEY_TITLE), value: QString::fromLatin1(ba: imageId));
833 }
834 if (!readMetadata(image&: img)) {
835 qCDebug(LOG_TGAPLUGIN) << "read: error while reading metadata";
836 }
837
838 *outImage = img;
839 return true;
840}
841
842bool TGAHandler::write(const QImage &image)
843{
844 auto ok = false;
845 if (image.format() == QImage::Format_Indexed8) {
846 ok = writeIndexed(image);
847 } else if (image.format() == QImage::Format_Grayscale8 || image.format() == QImage::Format_Grayscale16) {
848 ok = writeGrayscale(image);
849 } else if (image.format() == QImage::Format_RGB555 || image.format() == QImage::Format_RGB16 || image.format() == QImage::Format_RGB444) {
850 ok = writeRGB555(image);
851 } else {
852 ok = writeRGBA(image);
853 }
854 return (ok && writeMetadata(image));
855}
856
857bool TGAHandler::writeIndexed(const QImage &image)
858{
859 auto dev = device();
860 { // write header and palette
861 QDataStream s(dev);
862 s.setByteOrder(QDataStream::LittleEndian);
863
864 auto ct = image.colorTable();
865 auto iid = imageId(img: image);
866 s << quint8(iid.size()); // ID Length
867 s << quint8(1); // Color Map Type
868 s << quint8(TGA_TYPE_INDEXED); // Image Type
869 s << quint16(0); // First Entry Index
870 s << quint16(ct.size()); // Color Map Length
871 s << quint8(32); // Color map Entry Size
872 s << quint16(0); // X-origin of Image
873 s << quint16(0); // Y-origin of Image
874
875 s << quint16(image.width()); // Image Width
876 s << quint16(image.height()); // Image Height
877 s << quint8(8); // Pixel Depth
878 s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor
879
880 if (!iid.isEmpty())
881 s.writeRawData(iid.data(), len: iid.size());
882
883 for (auto &&rgb : ct) {
884 s << quint8(qBlue(rgb));
885 s << quint8(qGreen(rgb));
886 s << quint8(qRed(rgb));
887 s << quint8(qAlpha(rgb));
888 }
889
890 if (s.status() != QDataStream::Ok) {
891 return false;
892 }
893 }
894
895 for (int y = 0, h = image.height(), w = image.width(); y < h; y++) {
896 auto ptr = reinterpret_cast<const char *>(image.constScanLine(y));
897 if (dev->write(data: ptr, len: w) != w) {
898 return false;
899 }
900 }
901
902 return true;
903}
904
905bool TGAHandler::writeGrayscale(const QImage &image)
906{
907 auto dev = device();
908 { // write header
909 QDataStream s(dev);
910 s.setByteOrder(QDataStream::LittleEndian);
911
912 auto iid = imageId(img: image);
913 s << quint8(iid.size()); // ID Length
914 s << quint8(0); // Color Map Type
915 s << quint8(TGA_TYPE_GREY); // Image Type
916 s << quint16(0); // First Entry Index
917 s << quint16(0); // Color Map Length
918 s << quint8(0); // Color map Entry Size
919 s << quint16(0); // X-origin of Image
920 s << quint16(0); // Y-origin of Image
921
922 s << quint16(image.width()); // Image Width
923 s << quint16(image.height()); // Image Height
924 s << quint8(8); // Pixel Depth
925 s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor
926
927 if (!iid.isEmpty())
928 s.writeRawData(iid.data(), len: iid.size());
929
930 if (s.status() != QDataStream::Ok) {
931 return false;
932 }
933 }
934
935 ScanLineConverter scl(QImage::Format_Grayscale8);
936 for (int y = 0, h = image.height(), w = image.width(); y < h; y++) {
937 auto ptr = reinterpret_cast<const char *>(scl.convertedScanLine(image, y));
938 if (dev->write(data: ptr, len: w) != w) {
939 return false;
940 }
941 }
942
943 return true;
944}
945
946bool TGAHandler::writeRGB555(const QImage &image)
947{
948 auto dev = device();
949 { // write header
950 QDataStream s(dev);
951 s.setByteOrder(QDataStream::LittleEndian);
952
953 auto iid = imageId(img: image);
954 for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
955 s << c;
956 }
957 s << quint16(image.width()); // width
958 s << quint16(image.height()); // height
959 s << quint8(16); // depth
960 s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT);
961
962 if (!iid.isEmpty())
963 s.writeRawData(iid.data(), len: iid.size());
964
965 if (s.status() != QDataStream::Ok) {
966 return false;
967 }
968 }
969
970 ScanLineConverter scl(QImage::Format_RGB555);
971 QByteArray ba(image.width() * 2, char());
972 for (int y = 0, h = image.height(); y < h; y++) {
973 auto ptr = reinterpret_cast<const quint16 *>(scl.convertedScanLine(image, y));
974 for (int x = 0, w = image.width(); x < w; x++) {
975 auto color = *(ptr + x);
976 ba[x * 2] = char(color);
977 ba[x * 2 + 1] = char(color >> 8);
978 }
979 if (dev->write(data: ba.data(), len: ba.size()) != qint64(ba.size())) {
980 return false;
981 }
982 }
983
984 return true;
985}
986
987bool TGAHandler::writeRGBA(const QImage &image)
988{
989 auto format = image.format();
990 const bool hasAlpha = image.hasAlphaChannel();
991#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
992 auto cs = image.colorSpace();
993 auto tcs = QColorSpace();
994 if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
995 format = QImage::Format_RGB32;
996 tcs = QColorSpace(QColorSpace::SRgb);
997 } else if (hasAlpha && image.format() != QImage::Format_ARGB32) {
998#else
999 if (hasAlpha && image.format() != QImage::Format_ARGB32) {
1000#endif
1001 format = QImage::Format_ARGB32;
1002 } else if (!hasAlpha && image.format() != QImage::Format_RGB32) {
1003 format = QImage::Format_RGB32;
1004 }
1005
1006 auto dev = device();
1007 { // write header
1008 QDataStream s(dev);
1009 s.setByteOrder(QDataStream::LittleEndian);
1010
1011 const quint8 originTopLeft = TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT; // 0x20
1012 const quint8 alphaChannel8Bits = 0x08;
1013
1014 auto iid = imageId(img: image);
1015 for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
1016 s << c;
1017 }
1018 s << quint16(image.width()); // width
1019 s << quint16(image.height()); // height
1020 s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
1021 s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8)
1022
1023 if (!iid.isEmpty())
1024 s.writeRawData(iid.data(), len: iid.size());
1025
1026 if (s.status() != QDataStream::Ok) {
1027 return false;
1028 }
1029 }
1030
1031 ScanLineConverter scl(format);
1032 if (tcs.isValid()) {
1033 scl.setTargetColorSpace(tcs);
1034 }
1035 auto mul = hasAlpha ? 4 : 3;
1036 QByteArray ba(image.width() * mul, char());
1037 for (int y = 0, h = image.height(); y < h; y++) {
1038 auto ptr = reinterpret_cast<const QRgb *>(scl.convertedScanLine(image, y));
1039 for (int x = 0, w = image.width(); x < w; x++) {
1040 auto color = *(ptr + x);
1041 auto xmul = x * mul;
1042 ba[xmul] = char(qBlue(rgb: color));
1043 ba[xmul + 1] = char(qGreen(rgb: color));
1044 ba[xmul + 2] = char(qRed(rgb: color));
1045 if (hasAlpha) {
1046 ba[xmul + 3] = char(qAlpha(rgb: color));
1047 }
1048 }
1049 if (dev->write(data: ba.data(), len: ba.size()) != qint64(ba.size())) {
1050 return false;
1051 }
1052 }
1053
1054 return true;
1055}
1056
1057bool TGAHandler::writeMetadata(const QImage &image)
1058{
1059 if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V1()) {
1060 return true; // TGA V1 does not have these data
1061 }
1062
1063 auto dev = device();
1064 if (dev == nullptr) {
1065 return false;
1066 }
1067 if (dev->isSequential()) {
1068 qCInfo(LOG_TGAPLUGIN) << "writeMetadata: unable to save metadata on a sequential device";
1069 return true;
1070 }
1071
1072 QDataStream s(dev);
1073 s.setByteOrder(QDataStream::LittleEndian);
1074
1075 // TGA 2.0 footer
1076 TgaFooter foot;
1077
1078 // 32-bit overflow check (rough check)
1079 // I need at least 495 (extension) + 26 (footer) bytes -> 1024 bytes.
1080 // for the development area I roughly estimate 4096 KiB (profile, exif and xmp) they should always be less.
1081 auto reqBytes = qint64(d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E() ? 4096 * 1024 : 1024);
1082 if (dev->pos() > std::numeric_limits<quint32>::max() - reqBytes) {
1083 qCInfo(LOG_TGAPLUGIN) << "writeMetadata: there is no enough space for metadata";
1084 return true;
1085 }
1086
1087 // TGA 2.0 developer area
1088 TgaDeveloperDirectory dir;
1089 if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E()) {
1090 auto exif = MicroExif::fromImage(image);
1091 if (!exif.isEmpty()) {
1092 auto ba = QByteArray("eXif").append(a: exif.toByteArray(byteOrder: s.byteOrder()));
1093 TgaDeveloperDirectory::Field f;
1094 f.tagId = TGA_EXIF_TAGID;
1095 f.offset = dev->pos();
1096 f.size = ba.size();
1097 if (s.writeRawData(ba.data(), len: ba.size()) != ba.size()) {
1098 return false;
1099 }
1100 dir.fields << f;
1101 }
1102 auto icc = image.colorSpace().iccProfile();
1103 if (!icc.isEmpty()) {
1104 auto ba = QByteArray("iCCP").append(a: icc);
1105 TgaDeveloperDirectory::Field f;
1106 f.tagId = TGA_ICCP_TAGID;
1107 f.offset = dev->pos();
1108 f.size = ba.size();
1109 if (s.writeRawData(ba.data(), len: ba.size()) != ba.size()) {
1110 return false;
1111 }
1112 dir.fields << f;
1113 }
1114 auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).trimmed();
1115 if (!xmp.isEmpty()) {
1116 auto ba = QByteArray("xMPP").append(a: xmp.toUtf8());
1117 TgaDeveloperDirectory::Field f;
1118 f.tagId = TGA_XMPP_TAGID;
1119 f.offset = dev->pos();
1120 f.size = ba.size();
1121 if (s.writeRawData(ba.data(), len: ba.size()) != ba.size()) {
1122 return false;
1123 }
1124 dir.fields << f;
1125 }
1126 }
1127
1128 // TGA 2.0 extension area
1129 TgaExtension ext;
1130 ext.setDateTime(QDateTime::currentDateTimeUtc());
1131 if (image.hasAlphaChannel()) {
1132 ext.attributesType = TgaExtension::Alpha;
1133 }
1134 auto keys = image.textKeys();
1135 for (auto &&key : keys) {
1136 if (!key.compare(QStringLiteral(META_KEY_AUTHOR), cs: Qt::CaseInsensitive)) {
1137 ext.setAuthor(image.text(key));
1138 continue;
1139 }
1140 if (!key.compare(QStringLiteral(META_KEY_COMMENT), cs: Qt::CaseInsensitive)) {
1141 ext.setComment(image.text(key));
1142 continue;
1143 }
1144 if (!key.compare(QStringLiteral(META_KEY_DESCRIPTION), cs: Qt::CaseInsensitive)) {
1145 if (ext.comment().isEmpty())
1146 ext.setComment(image.text(key));
1147 continue;
1148 }
1149 if (!key.compare(QStringLiteral(META_KEY_SOFTWARE), cs: Qt::CaseInsensitive)) {
1150 ext.setSoftware(image.text(key));
1151 continue;
1152 }
1153 }
1154
1155 // write developer area
1156 if (!dir.isEmpty()) {
1157 foot.developerOffset = dev->pos();
1158 s << dir;
1159 }
1160
1161 // write extension area (date time is always set)
1162 foot.extensionOffset = dev->pos();
1163 s << ext;
1164 s << foot;
1165
1166 return s.status() == QDataStream::Ok;
1167}
1168
1169bool TGAHandler::readMetadata(QImage &image)
1170{
1171 auto dev = device();
1172 if (dev == nullptr) {
1173 return false;
1174 }
1175 if (dev->isSequential()) {
1176 qCInfo(LOG_TGAPLUGIN) << "readMetadata: unable to load metadata on a sequential device";
1177 return true;
1178 }
1179
1180 // read TGA footer
1181 if (!dev->seek(pos: dev->size() - 26)) {
1182 return false;
1183 }
1184
1185 QDataStream s(dev);
1186 s.setByteOrder(QDataStream::LittleEndian);
1187
1188 TgaFooter foot;
1189 s >> foot;
1190 if (s.status() != QDataStream::Ok) {
1191 return false;
1192 }
1193 if (!foot.isValid()) {
1194 return true; // not a TGA 2.0 -> no metadata are present
1195 }
1196
1197 if (foot.extensionOffset > 0) {
1198 // read the extension area
1199 if (!dev->seek(pos: foot.extensionOffset)) {
1200 return false;
1201 }
1202
1203 TgaExtension ext;
1204 s >> ext;
1205 if (s.status() != QDataStream::Ok || !ext.isValid()) {
1206 return false;
1207 }
1208
1209 auto dt = ext.dateTime();
1210 if (dt.isValid()) {
1211 image.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), value: dt.toString(format: Qt::ISODate));
1212 }
1213 auto au = ext.author();
1214 if (!au.isEmpty()) {
1215 image.setText(QStringLiteral(META_KEY_AUTHOR), value: au);
1216 }
1217 auto cm = ext.comment();
1218 if (!cm.isEmpty()) {
1219 image.setText(QStringLiteral(META_KEY_COMMENT), value: cm);
1220 }
1221 auto sw = ext.software();
1222 if (!sw.isEmpty()) {
1223 image.setText(QStringLiteral(META_KEY_SOFTWARE), value: sw);
1224 }
1225 }
1226
1227 if (foot.developerOffset > 0) {
1228 // read developer area
1229 if (!dev->seek(pos: foot.developerOffset)) {
1230 return false;
1231 }
1232
1233 TgaDeveloperDirectory dir;
1234 s >> dir;
1235 if (s.status() != QDataStream::Ok) {
1236 return false;
1237 }
1238
1239 for (auto &&f : dir.fields) {
1240 if (!dev->seek(pos: f.offset)) {
1241 return false;
1242 }
1243 if (f.tagId == TGA_EXIF_TAGID) {
1244 auto ba = dev->read(maxlen: f.size);
1245 if (ba.startsWith(bv: QByteArray("eXif"))) {
1246 auto exif = MicroExif::fromByteArray(ba: ba.mid(index: 4));
1247 exif.updateImageMetadata(targetImage&: image, replaceExisting: true);
1248 exif.updateImageResolution(targetImage&: image);
1249 }
1250 continue;
1251 }
1252 if (f.tagId == TGA_ICCP_TAGID) {
1253 auto ba = dev->read(maxlen: f.size);
1254 if (ba.startsWith(bv: QByteArray("iCCP"))) {
1255 image.setColorSpace(QColorSpace::fromIccProfile(iccProfile: ba.mid(index: 4)));
1256 }
1257 continue;
1258 }
1259 if (f.tagId == TGA_XMPP_TAGID) {
1260 auto ba = dev->read(maxlen: f.size);
1261 if (ba.startsWith(bv: QByteArray("xMPP"))) {
1262 image.setText(QStringLiteral(META_KEY_XMP_ADOBE), value: QString::fromUtf8(ba: ba.mid(index: 4)));
1263 }
1264 continue;
1265 }
1266 }
1267 }
1268
1269 return s.status() == QDataStream::Ok;
1270}
1271
1272bool TGAHandler::supportsOption(ImageOption option) const
1273{
1274 if (option == QImageIOHandler::Size) {
1275 return true;
1276 }
1277 if (option == QImageIOHandler::ImageFormat) {
1278 return true;
1279 }
1280 if (option == QImageIOHandler::SubType) {
1281 return true;
1282 }
1283 if (option == QImageIOHandler::SupportedSubTypes) {
1284 return true;
1285 }
1286 return false;
1287}
1288
1289void TGAHandler::setOption(ImageOption option, const QVariant &value)
1290{
1291 if (option == QImageIOHandler::SubType) {
1292 auto subType = value.toByteArray();
1293 auto list = TGAHandler::option(option: QImageIOHandler::SupportedSubTypes).value<QList<QByteArray>>();
1294 if (list.contains(t: subType)) {
1295 d->m_subType = subType;
1296 } else {
1297 d->m_subType = TGAHandlerPrivate::subTypeTGA_V2S();
1298 }
1299 }
1300}
1301
1302QVariant TGAHandler::option(ImageOption option) const
1303{
1304 if (!supportsOption(option)) {
1305 return {};
1306 }
1307
1308 if (option == QImageIOHandler::SupportedSubTypes) {
1309 return QVariant::fromValue(value: QList<QByteArray>()
1310 << TGAHandlerPrivate::subTypeTGA_V1() << TGAHandlerPrivate::subTypeTGA_V2S() << TGAHandlerPrivate::subTypeTGA_V2E());
1311 }
1312
1313 if (option == QImageIOHandler::SubType) {
1314 return QVariant::fromValue(value: d->m_subType);
1315 }
1316
1317 auto &&header = d->m_header;
1318 if (!IsSupported(head: header)) {
1319 if (auto dev = device())
1320 if (!peekHeader(device: dev, header) && IsSupported(head: header))
1321 return {};
1322 if (!IsSupported(head: header)) {
1323 return {};
1324 }
1325 }
1326
1327 if (option == QImageIOHandler::Size) {
1328 return QVariant::fromValue(value: QSize(header.width, header.height));
1329 }
1330
1331 if (option == QImageIOHandler::ImageFormat) {
1332 return QVariant::fromValue(value: imageFormat(head: header));
1333 }
1334
1335 return {};
1336}
1337
1338bool TGAHandler::canRead(QIODevice *device)
1339{
1340 if (!device) {
1341 qCWarning(LOG_TGAPLUGIN) << "TGAHandler::canRead() called with no device";
1342 return false;
1343 }
1344
1345 TgaHeader tga;
1346 if (!peekHeader(device, header&: tga)) {
1347 qCWarning(LOG_TGAPLUGIN) << "TGAHandler::canRead() error while reading the header";
1348 return false;
1349 }
1350
1351 return IsSupported(head: tga);
1352}
1353
1354QImageIOPlugin::Capabilities TGAPlugin::capabilities(QIODevice *device, const QByteArray &format) const
1355{
1356 if (format == "tga") {
1357 return Capabilities(CanRead | CanWrite);
1358 }
1359 if (!format.isEmpty()) {
1360 return {};
1361 }
1362 if (!device->isOpen()) {
1363 return {};
1364 }
1365
1366 Capabilities cap;
1367 if (device->isReadable() && TGAHandler::canRead(device)) {
1368 cap |= CanRead;
1369 }
1370 if (device->isWritable()) {
1371 cap |= CanWrite;
1372 }
1373 return cap;
1374}
1375
1376QImageIOHandler *TGAPlugin::create(QIODevice *device, const QByteArray &format) const
1377{
1378 QImageIOHandler *handler = new TGAHandler;
1379 handler->setDevice(device);
1380 handler->setFormat(format);
1381 return handler;
1382}
1383
1384#include "moc_tga_p.cpp"
1385

source code of kimageformats/src/imageformats/tga.cpp