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 | |
28 | typedef quint32 uint; |
29 | typedef quint16 ushort; |
30 | typedef quint8 uchar; |
31 | |
32 | namespace // Private. |
33 | { |
34 | // Header format of saved files. |
35 | uchar targaMagic[12] = {0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0}; |
36 | |
37 | enum 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. */ |
58 | struct { |
59 | uchar ; |
60 | uchar ; |
61 | uchar ; |
62 | ushort ; |
63 | ushort ; |
64 | uchar ; |
65 | ushort ; |
66 | ushort ; |
67 | ushort ; |
68 | ushort ; |
69 | uchar ; |
70 | uchar ; |
71 | |
72 | enum { |
73 | = 18, |
74 | }; // const static int SIZE = 18; |
75 | }; |
76 | |
77 | static QDataStream &(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 | |
94 | static bool (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 | |
123 | struct Color555 { |
124 | ushort b : 5; |
125 | ushort g : 5; |
126 | ushort r : 5; |
127 | }; |
128 | |
129 | struct { |
130 | bool ; |
131 | bool ; |
132 | bool ; |
133 | bool ; |
134 | |
135 | (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 | |
173 | static QImage::Format (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 | */ |
196 | static bool (QIODevice *device, TgaHeader &) |
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 | |
221 | static bool (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 | |
410 | TGAHandler::TGAHandler() |
411 | { |
412 | } |
413 | |
414 | bool TGAHandler::canRead() const |
415 | { |
416 | if (canRead(device: device())) { |
417 | setFormat("tga" ); |
418 | return true; |
419 | } |
420 | return false; |
421 | } |
422 | |
423 | bool 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 | |
461 | bool 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 | |
506 | bool 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 | |
517 | QVariant TGAHandler::option(ImageOption option) const |
518 | { |
519 | QVariant v; |
520 | |
521 | if (option == QImageIOHandler::Size) { |
522 | if (auto d = device()) { |
523 | TgaHeader ; |
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 ; |
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 | |
542 | bool 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 | |
574 | QImageIOPlugin::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 | |
596 | QImageIOHandler *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 | |