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
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9/* this code supports:
10 * reading:
11 * uncompressed and run length encoded indexed, grey and color tga files.
12 * image types 1, 2, 3, 9, 10 and 11.
13 * only RGB color maps with no more than 256 colors.
14 * pixel formats 8, 16, 24 and 32.
15 * writing:
16 * uncompressed true color tga files
17 */
18
19#include "tga_p.h"
20#include "util_p.h"
21
22#include <assert.h>
23
24#include <QDataStream>
25#include <QDebug>
26#include <QImage>
27
28typedef quint32 uint;
29typedef quint16 ushort;
30typedef quint8 uchar;
31
32namespace // Private.
33{
34// Header format of saved files.
35uchar targaMagic[12] = {0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0};
36
37enum TGAType {
38 TGA_TYPE_INDEXED = 1,
39 TGA_TYPE_RGB = 2,
40 TGA_TYPE_GREY = 3,
41 TGA_TYPE_RLE_INDEXED = 9,
42 TGA_TYPE_RLE_RGB = 10,
43 TGA_TYPE_RLE_GREY = 11,
44};
45
46#define TGA_INTERLEAVE_MASK 0xc0
47#define TGA_INTERLEAVE_NONE 0x00
48#define TGA_INTERLEAVE_2WAY 0x40
49#define TGA_INTERLEAVE_4WAY 0x80
50
51#define TGA_ORIGIN_MASK 0x30
52#define TGA_ORIGIN_LEFT 0x00
53#define TGA_ORIGIN_RIGHT 0x10
54#define TGA_ORIGIN_LOWER 0x00
55#define TGA_ORIGIN_UPPER 0x20
56
57/** Tga Header. */
58struct TgaHeader {
59 uchar id_length;
60 uchar colormap_type;
61 uchar image_type;
62 ushort colormap_index;
63 ushort colormap_length;
64 uchar colormap_size;
65 ushort x_origin;
66 ushort y_origin;
67 ushort width;
68 ushort height;
69 uchar pixel_size;
70 uchar flags;
71
72 enum {
73 SIZE = 18,
74 }; // const static int SIZE = 18;
75};
76
77static QDataStream &operator>>(QDataStream &s, TgaHeader &head)
78{
79 s >> head.id_length;
80 s >> head.colormap_type;
81 s >> head.image_type;
82 s >> head.colormap_index;
83 s >> head.colormap_length;
84 s >> head.colormap_size;
85 s >> head.x_origin;
86 s >> head.y_origin;
87 s >> head.width;
88 s >> head.height;
89 s >> head.pixel_size;
90 s >> head.flags;
91 return s;
92}
93
94static bool IsSupported(const TgaHeader &head)
95{
96 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
97 && head.image_type != TGA_TYPE_RLE_RGB && head.image_type != TGA_TYPE_RLE_GREY) {
98 return false;
99 }
100 if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
101 if (head.colormap_length > 256 || head.colormap_size != 24 || head.colormap_type != 1) {
102 return false;
103 }
104 }
105 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) {
106 if (head.colormap_type != 0) {
107 return false;
108 }
109 }
110 if (head.width == 0 || head.height == 0) {
111 return false;
112 }
113 if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
114 return false;
115 }
116 // If the colormap_type field is set to zero, indicating that no color map exists, then colormap_size, colormap_index and colormap_length should be set to zero.
117 if (head.colormap_type == 0 && (head.colormap_size != 0 || head.colormap_index != 0 || head.colormap_length != 0)) {
118 return false;
119 }
120 return true;
121}
122
123struct Color555 {
124 ushort b : 5;
125 ushort g : 5;
126 ushort r : 5;
127};
128
129struct TgaHeaderInfo {
130 bool rle;
131 bool pal;
132 bool rgb;
133 bool grey;
134
135 TgaHeaderInfo(const TgaHeader &tga)
136 : rle(false)
137 , pal(false)
138 , rgb(false)
139 , grey(false)
140 {
141 switch (tga.image_type) {
142 case TGA_TYPE_RLE_INDEXED:
143 rle = true;
144 Q_FALLTHROUGH();
145 // no break is intended!
146 case TGA_TYPE_INDEXED:
147 pal = true;
148 break;
149
150 case TGA_TYPE_RLE_RGB:
151 rle = true;
152 Q_FALLTHROUGH();
153 // no break is intended!
154 case TGA_TYPE_RGB:
155 rgb = true;
156 break;
157
158 case TGA_TYPE_RLE_GREY:
159 rle = true;
160 Q_FALLTHROUGH();
161 // no break is intended!
162 case TGA_TYPE_GREY:
163 grey = true;
164 break;
165
166 default:
167 // Error, unknown image type.
168 break;
169 }
170 }
171};
172
173static QImage::Format imageFormat(const TgaHeader &head)
174{
175 auto format = QImage::Format_Invalid;
176 if (IsSupported(head)) {
177 // Bits 0-3 are the numbers of alpha bits (can be zero!)
178 const int numAlphaBits = head.flags & 0xf;
179 // However alpha exists only in the 32 bit format.
180 if ((head.pixel_size == 32) && (head.flags & 0xf)) {
181 if (numAlphaBits <= 8) {
182 format = QImage::Format_ARGB32;
183 }
184 }
185 else {
186 format = QImage::Format_RGB32;
187 }
188 }
189 return format;
190}
191
192/*!
193 * \brief peekHeader
194 * Reads the header but does not change the position in the device.
195 */
196static bool peekHeader(QIODevice *device, TgaHeader &header)
197{
198 qint64 oldPos = device->pos();
199 QByteArray head = device->read(maxlen: TgaHeader::SIZE);
200 int readBytes = head.size();
201
202 if (device->isSequential()) {
203 for (int pos = readBytes - 1; pos >= 0; --pos) {
204 device->ungetChar(c: head[pos]);
205 }
206 } else {
207 device->seek(pos: oldPos);
208 }
209
210 if (readBytes < TgaHeader::SIZE) {
211 return false;
212 }
213
214 QDataStream stream(head);
215 stream.setByteOrder(QDataStream::LittleEndian);
216 stream >> header;
217
218 return true;
219}
220
221static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
222{
223 img = imageAlloc(width: tga.width, height: tga.height, format: imageFormat(head: tga));
224 if (img.isNull()) {
225 qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
226 return false;
227 }
228
229 TgaHeaderInfo info(tga);
230
231 const int numAlphaBits = tga.flags & 0xf;
232 uint pixel_size = (tga.pixel_size / 8);
233 qint64 size = qint64(tga.width) * qint64(tga.height) * pixel_size;
234
235 if (size < 1) {
236 // qDebug() << "This TGA file is broken with size " << size;
237 return false;
238 }
239
240 // Read palette.
241 static const int max_palette_size = 768;
242 char palette[max_palette_size];
243 if (info.pal) {
244 // @todo Support palettes in other formats!
245 const int palette_size = 3 * tga.colormap_length;
246 if (palette_size > max_palette_size) {
247 return false;
248 }
249 const int dataRead = s.readRawData(palette, len: palette_size);
250 if (dataRead < 0) {
251 return false;
252 }
253 if (dataRead < max_palette_size) {
254 memset(s: &palette[dataRead], c: 0, n: max_palette_size - dataRead);
255 }
256 }
257
258 // Allocate image.
259 uchar *const image = reinterpret_cast<uchar *>(malloc(size: size));
260 if (!image) {
261 return false;
262 }
263
264 bool valid = true;
265
266 if (info.rle) {
267 // Decode image.
268 char *dst = (char *)image;
269 char *imgEnd = dst + size;
270 qint64 num = size;
271
272 while (num > 0 && valid) {
273 if (s.atEnd()) {
274 valid = false;
275 break;
276 }
277
278 // Get packet header.
279 uchar c;
280 s >> c;
281
282 uint count = (c & 0x7f) + 1;
283 num -= count * pixel_size;
284 if (num < 0) {
285 valid = false;
286 break;
287 }
288
289 if (c & 0x80) {
290 // RLE pixels.
291 assert(pixel_size <= 8);
292 char pixel[8];
293 const int dataRead = s.readRawData(pixel, len: pixel_size);
294 if (dataRead < (int)pixel_size) {
295 memset(s: &pixel[dataRead], c: 0, n: pixel_size - dataRead);
296 }
297 do {
298 if (dst + pixel_size > imgEnd) {
299 qWarning() << "Trying to write out of bounds!" << ptrdiff_t(dst) << (ptrdiff_t(imgEnd) - ptrdiff_t(pixel_size));
300 valid = false;
301 break;
302 }
303
304 memcpy(dest: dst, src: pixel, n: pixel_size);
305 dst += pixel_size;
306 } while (--count);
307 } else {
308 // Raw pixels.
309 count *= pixel_size;
310 const int dataRead = s.readRawData(dst, len: count);
311 if (dataRead < 0) {
312 free(ptr: image);
313 return false;
314 }
315
316 if ((uint)dataRead < count) {
317 const size_t toCopy = count - dataRead;
318 if (&dst[dataRead] + toCopy > imgEnd) {
319 qWarning() << "Trying to write out of bounds!" << ptrdiff_t(image) << ptrdiff_t(&dst[dataRead]);
320 ;
321 valid = false;
322 break;
323 }
324
325 memset(s: &dst[dataRead], c: 0, n: toCopy);
326 }
327 dst += count;
328 }
329 }
330 } else {
331 // Read raw image.
332 const int dataRead = s.readRawData((char *)image, len: size);
333 if (dataRead < 0) {
334 free(ptr: image);
335 return false;
336 }
337 if (dataRead < size) {
338 memset(s: &image[dataRead], c: 0, n: size - dataRead);
339 }
340 }
341
342 if (!valid) {
343 free(ptr: image);
344 return false;
345 }
346
347 // Convert image to internal format.
348 int y_start;
349 int y_step;
350 int y_end;
351 if (tga.flags & TGA_ORIGIN_UPPER) {
352 y_start = 0;
353 y_step = 1;
354 y_end = tga.height;
355 } else {
356 y_start = tga.height - 1;
357 y_step = -1;
358 y_end = -1;
359 }
360
361 uchar *src = image;
362
363 for (int y = y_start; y != y_end; y += y_step) {
364 QRgb *scanline = (QRgb *)img.scanLine(y);
365
366 if (info.pal) {
367 // Paletted.
368 for (int x = 0; x < tga.width; x++) {
369 uchar idx = *src++;
370 scanline[x] = qRgb(r: palette[3 * idx + 2], g: palette[3 * idx + 1], b: palette[3 * idx + 0]);
371 }
372 } else if (info.grey) {
373 // Greyscale.
374 for (int x = 0; x < tga.width; x++) {
375 scanline[x] = qRgb(r: *src, g: *src, b: *src);
376 src++;
377 }
378 } else {
379 // True Color.
380 if (tga.pixel_size == 16) {
381 for (int x = 0; x < tga.width; x++) {
382 Color555 c = *reinterpret_cast<Color555 *>(src);
383 scanline[x] = qRgb(r: (c.r << 3) | (c.r >> 2), g: (c.g << 3) | (c.g >> 2), b: (c.b << 3) | (c.b >> 2));
384 src += 2;
385 }
386 } else if (tga.pixel_size == 24) {
387 for (int x = 0; x < tga.width; x++) {
388 scanline[x] = qRgb(r: src[2], g: src[1], b: src[0]);
389 src += 3;
390 }
391 } else if (tga.pixel_size == 32) {
392 for (int x = 0; x < tga.width; x++) {
393 // ### TODO: verify with images having really some alpha data
394 const uchar alpha = (src[3] << (8 - numAlphaBits));
395 scanline[x] = qRgba(r: src[2], g: src[1], b: src[0], a: alpha);
396 src += 4;
397 }
398 }
399 }
400 }
401
402 // Free image.
403 free(ptr: image);
404
405 return true;
406}
407
408} // namespace
409
410TGAHandler::TGAHandler()
411{
412}
413
414bool TGAHandler::canRead() const
415{
416 if (canRead(device: device())) {
417 setFormat("tga");
418 return true;
419 }
420 return false;
421}
422
423bool TGAHandler::read(QImage *outImage)
424{
425 // qDebug() << "Loading TGA file!";
426
427 auto d = device();
428 TgaHeader tga;
429 if (!peekHeader(device: d, header&: tga) || !IsSupported(head: tga)) {
430 // qDebug() << "This TGA file is not valid.";
431 return false;
432 }
433
434 if (d->isSequential()) {
435 d->read(maxlen: TgaHeader::SIZE + tga.id_length);
436 } else {
437 d->seek(pos: TgaHeader::SIZE + tga.id_length);
438 }
439
440 QDataStream s(d);
441 s.setByteOrder(QDataStream::LittleEndian);
442
443 // Check image file format.
444 if (s.atEnd()) {
445 // qDebug() << "This TGA file is not valid.";
446 return false;
447 }
448
449 QImage img;
450 bool result = LoadTGA(s, tga, img);
451
452 if (result == false) {
453 // qDebug() << "Error loading TGA file.";
454 return false;
455 }
456
457 *outImage = img;
458 return true;
459}
460
461bool TGAHandler::write(const QImage &image)
462{
463 QDataStream s(device());
464 s.setByteOrder(QDataStream::LittleEndian);
465
466 QImage img(image);
467 const bool hasAlpha = img.hasAlphaChannel();
468 if (hasAlpha && img.format() != QImage::Format_ARGB32) {
469 img = img.convertToFormat(f: QImage::Format_ARGB32);
470 } else if (!hasAlpha && img.format() != QImage::Format_RGB32) {
471 img = img.convertToFormat(f: QImage::Format_RGB32);
472 }
473 if (img.isNull()) {
474 qDebug() << "TGAHandler::write: image conversion to 32 bits failed!";
475 return false;
476 }
477 static constexpr quint8 originTopLeft = TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT; // 0x20
478 static constexpr quint8 alphaChannel8Bits = 0x08;
479
480 for (int i = 0; i < 12; i++) {
481 s << targaMagic[i];
482 }
483
484 // write header
485 s << quint16(img.width()); // width
486 s << quint16(img.height()); // height
487 s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
488 s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8)
489
490 for (int y = 0; y < img.height(); y++) {
491 auto ptr = reinterpret_cast<QRgb *>(img.scanLine(y));
492 for (int x = 0; x < img.width(); x++) {
493 auto color = *(ptr + x);
494 s << quint8(qBlue(rgb: color));
495 s << quint8(qGreen(rgb: color));
496 s << quint8(qRed(rgb: color));
497 if (hasAlpha) {
498 s << quint8(qAlpha(rgb: color));
499 }
500 }
501 }
502
503 return true;
504}
505
506bool TGAHandler::supportsOption(ImageOption option) const
507{
508 if (option == QImageIOHandler::Size) {
509 return true;
510 }
511 if (option == QImageIOHandler::ImageFormat) {
512 return true;
513 }
514 return false;
515}
516
517QVariant TGAHandler::option(ImageOption option) const
518{
519 QVariant v;
520
521 if (option == QImageIOHandler::Size) {
522 if (auto d = device()) {
523 TgaHeader header;
524 if (peekHeader(device: d, header) && IsSupported(head: header)) {
525 v = QVariant::fromValue(value: QSize(header.width, header.height));
526 }
527 }
528 }
529
530 if (option == QImageIOHandler::ImageFormat) {
531 if (auto d = device()) {
532 TgaHeader header;
533 if (peekHeader(device: d, header) && IsSupported(head: header)) {
534 v = QVariant::fromValue(value: imageFormat(head: header));
535 }
536 }
537 }
538
539 return v;
540}
541
542bool TGAHandler::canRead(QIODevice *device)
543{
544 if (!device) {
545 qWarning(msg: "TGAHandler::canRead() called with no device");
546 return false;
547 }
548
549 qint64 oldPos = device->pos();
550 QByteArray head = device->read(maxlen: TgaHeader::SIZE);
551 int readBytes = head.size();
552
553 if (device->isSequential()) {
554 for (int pos = readBytes - 1; pos >= 0; --pos) {
555 device->ungetChar(c: head[pos]);
556 }
557 } else {
558 device->seek(pos: oldPos);
559 }
560
561 if (readBytes < TgaHeader::SIZE) {
562 return false;
563 }
564
565 TgaHeader tga;
566 if (!peekHeader(device, header&: tga)) {
567 qWarning(msg: "TGAHandler::canRead() error while reading the header");
568 return false;
569 }
570
571 return IsSupported(head: tga);
572}
573
574QImageIOPlugin::Capabilities TGAPlugin::capabilities(QIODevice *device, const QByteArray &format) const
575{
576 if (format == "tga") {
577 return Capabilities(CanRead | CanWrite);
578 }
579 if (!format.isEmpty()) {
580 return {};
581 }
582 if (!device->isOpen()) {
583 return {};
584 }
585
586 Capabilities cap;
587 if (device->isReadable() && TGAHandler::canRead(device)) {
588 cap |= CanRead;
589 }
590 if (device->isWritable()) {
591 cap |= CanWrite;
592 }
593 return cap;
594}
595
596QImageIOHandler *TGAPlugin::create(QIODevice *device, const QByteArray &format) const
597{
598 QImageIOHandler *handler = new TGAHandler;
599 handler->setDevice(device);
600 handler->setFormat(format);
601 return handler;
602}
603
604#include "moc_tga_p.cpp"
605

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