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 | |
35 | typedef quint32 uint; |
36 | typedef quint16 ushort; |
37 | typedef quint8 uchar; |
38 | |
39 | #ifdef QT_DEBUG |
40 | Q_LOGGING_CATEGORY(LOG_TGAPLUGIN, "kf.imageformats.plugins.tga" , QtDebugMsg) |
41 | #else |
42 | Q_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 | |
52 | namespace // Private. |
53 | { |
54 | // Header format of saved files. |
55 | enum 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. */ |
86 | struct { |
87 | uchar = 0; |
88 | uchar = 0; |
89 | uchar = 0; |
90 | ushort = 0; |
91 | ushort = 0; |
92 | uchar = 0; |
93 | ushort = 0; |
94 | ushort = 0; |
95 | ushort = 0; |
96 | ushort = 0; |
97 | uchar = 0; |
98 | uchar = 0; |
99 | |
100 | enum { |
101 | = 18, |
102 | }; // const static int SIZE = 18; |
103 | }; |
104 | |
105 | /** Tga 2.0 Footer */ |
106 | struct { |
107 | () |
108 | : extensionOffset(0) |
109 | , developerOffset(0) |
110 | { |
111 | std::memcpy(dest: signature, src: "TRUEVISION-XFILE.\0" , n: 18); |
112 | } |
113 | bool () const |
114 | { |
115 | return std::memcmp(s1: signature, s2: "TRUEVISION-XFILE.\0" , n: 18) == 0; |
116 | } |
117 | |
118 | quint32 ; // Extension Area Offset |
119 | quint32 ; // Developer Directory Offset |
120 | char [18]; // TGA Signature |
121 | }; |
122 | |
123 | /** Tga 2.0 extension area */ |
124 | struct 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 (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 () 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 [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 | |
233 | struct 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 | |
248 | static QDataStream &(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 | |
265 | static QDataStream &(QDataStream &s, TgaFooter &) |
266 | { |
267 | s >> footer.extensionOffset; |
268 | s >> footer.developerOffset; |
269 | s.readRawData(footer.signature, len: sizeof(footer.signature)); |
270 | return s; |
271 | } |
272 | |
273 | static QDataStream &(QDataStream &s, const TgaFooter &) |
274 | { |
275 | s << footer.extensionOffset; |
276 | s << footer.developerOffset; |
277 | s.writeRawData(footer.signature, len: sizeof(footer.signature)); |
278 | return s; |
279 | } |
280 | |
281 | static 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 | |
295 | static 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 | |
306 | static 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 | |
336 | static 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 | |
366 | static bool (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 | */ |
404 | static 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 | |
412 | struct { |
413 | bool ; |
414 | bool ; |
415 | bool ; |
416 | bool ; |
417 | |
418 | (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 | |
456 | static QImage::Format (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 | */ |
491 | static bool (QIODevice *device, TgaHeader &) |
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 | */ |
513 | static 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 | |
583 | inline 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 | |
590 | static bool (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 | |
751 | class TGAHandlerPrivate |
752 | { |
753 | public: |
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 | |
782 | TGAHandler::TGAHandler() |
783 | : QImageIOHandler() |
784 | , d(new TGAHandlerPrivate) |
785 | { |
786 | } |
787 | |
788 | bool TGAHandler::canRead() const |
789 | { |
790 | if (canRead(device: device())) { |
791 | setFormat("tga" ); |
792 | return true; |
793 | } |
794 | return false; |
795 | } |
796 | |
797 | bool 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 | |
842 | bool 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 | |
857 | bool 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 | |
905 | bool 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 | |
946 | bool 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 | |
987 | bool 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 | |
1057 | bool 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 ; |
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 | |
1169 | bool 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 ; |
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 | |
1272 | bool 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 | |
1289 | void 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 | |
1302 | QVariant 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 && = 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 | |
1338 | bool 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 | |
1354 | QImageIOPlugin::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 | |
1376 | QImageIOHandler *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 | |