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

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