mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-05-28 00:30:23 -04:00
513 lines
14 KiB
C++
513 lines
14 KiB
C++
/*
|
|
This file is part of the KDE project
|
|
SPDX-FileCopyrightText: 2003 Dominik Seichter <domseichter@web.de>
|
|
SPDX-FileCopyrightText: 2004 Ignacio Castaño <castano@ludicon.com>
|
|
|
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
|
*/
|
|
|
|
/* this code supports:
|
|
* reading:
|
|
* uncompressed and run length encoded indexed, grey and color tga files.
|
|
* image types 1, 2, 3, 9, 10 and 11.
|
|
* only RGB color maps with no more than 256 colors.
|
|
* pixel formats 8, 16, 24 and 32.
|
|
* writing:
|
|
* uncompressed true color tga files
|
|
*/
|
|
|
|
#include "tga_p.h"
|
|
|
|
#include <assert.h>
|
|
|
|
#include <QDataStream>
|
|
#include <QDebug>
|
|
#include <QImage>
|
|
|
|
typedef quint32 uint;
|
|
typedef quint16 ushort;
|
|
typedef quint8 uchar;
|
|
|
|
namespace // Private.
|
|
{
|
|
// Header format of saved files.
|
|
uchar targaMagic[12] = {0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
|
|
|
enum TGAType {
|
|
TGA_TYPE_INDEXED = 1,
|
|
TGA_TYPE_RGB = 2,
|
|
TGA_TYPE_GREY = 3,
|
|
TGA_TYPE_RLE_INDEXED = 9,
|
|
TGA_TYPE_RLE_RGB = 10,
|
|
TGA_TYPE_RLE_GREY = 11,
|
|
};
|
|
|
|
#define TGA_INTERLEAVE_MASK 0xc0
|
|
#define TGA_INTERLEAVE_NONE 0x00
|
|
#define TGA_INTERLEAVE_2WAY 0x40
|
|
#define TGA_INTERLEAVE_4WAY 0x80
|
|
|
|
#define TGA_ORIGIN_MASK 0x30
|
|
#define TGA_ORIGIN_LEFT 0x00
|
|
#define TGA_ORIGIN_RIGHT 0x10
|
|
#define TGA_ORIGIN_LOWER 0x00
|
|
#define TGA_ORIGIN_UPPER 0x20
|
|
|
|
/** Tga Header. */
|
|
struct TgaHeader {
|
|
uchar id_length;
|
|
uchar colormap_type;
|
|
uchar image_type;
|
|
ushort colormap_index;
|
|
ushort colormap_length;
|
|
uchar colormap_size;
|
|
ushort x_origin;
|
|
ushort y_origin;
|
|
ushort width;
|
|
ushort height;
|
|
uchar pixel_size;
|
|
uchar flags;
|
|
|
|
enum {
|
|
SIZE = 18,
|
|
}; // const static int SIZE = 18;
|
|
};
|
|
|
|
static QDataStream &operator>>(QDataStream &s, TgaHeader &head)
|
|
{
|
|
s >> head.id_length;
|
|
s >> head.colormap_type;
|
|
s >> head.image_type;
|
|
s >> head.colormap_index;
|
|
s >> head.colormap_length;
|
|
s >> head.colormap_size;
|
|
s >> head.x_origin;
|
|
s >> head.y_origin;
|
|
s >> head.width;
|
|
s >> head.height;
|
|
s >> head.pixel_size;
|
|
s >> head.flags;
|
|
/*qDebug() << "id_length: " << head.id_length << " - colormap_type: " << head.colormap_type << " - image_type: " << head.image_type;
|
|
qDebug() << "colormap_index: " << head.colormap_index << " - colormap_length: " << head.colormap_length << " - colormap_size: " << head.colormap_size;
|
|
qDebug() << "x_origin: " << head.x_origin << " - y_origin: " << head.y_origin << " - width:" << head.width << " - height:" << head.height << " - pixelsize:
|
|
" << head.pixel_size << " - flags: " << head.flags;*/
|
|
return s;
|
|
}
|
|
|
|
static bool IsSupported(const TgaHeader &head)
|
|
{
|
|
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
|
|
&& head.image_type != TGA_TYPE_RLE_RGB && head.image_type != TGA_TYPE_RLE_GREY) {
|
|
return false;
|
|
}
|
|
if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
|
|
if (head.colormap_length > 256 || head.colormap_size != 24 || head.colormap_type != 1) {
|
|
return false;
|
|
}
|
|
}
|
|
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) {
|
|
if (head.colormap_type != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
if (head.width == 0 || head.height == 0) {
|
|
return false;
|
|
}
|
|
if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
struct Color555 {
|
|
ushort b : 5;
|
|
ushort g : 5;
|
|
ushort r : 5;
|
|
};
|
|
|
|
struct TgaHeaderInfo {
|
|
bool rle;
|
|
bool pal;
|
|
bool rgb;
|
|
bool grey;
|
|
|
|
TgaHeaderInfo(const TgaHeader &tga)
|
|
: rle(false)
|
|
, pal(false)
|
|
, rgb(false)
|
|
, grey(false)
|
|
{
|
|
switch (tga.image_type) {
|
|
case TGA_TYPE_RLE_INDEXED:
|
|
rle = true;
|
|
Q_FALLTHROUGH();
|
|
// no break is intended!
|
|
case TGA_TYPE_INDEXED:
|
|
pal = true;
|
|
break;
|
|
|
|
case TGA_TYPE_RLE_RGB:
|
|
rle = true;
|
|
Q_FALLTHROUGH();
|
|
// no break is intended!
|
|
case TGA_TYPE_RGB:
|
|
rgb = true;
|
|
break;
|
|
|
|
case TGA_TYPE_RLE_GREY:
|
|
rle = true;
|
|
Q_FALLTHROUGH();
|
|
// no break is intended!
|
|
case TGA_TYPE_GREY:
|
|
grey = true;
|
|
break;
|
|
|
|
default:
|
|
// Error, unknown image type.
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
|
{
|
|
// Create image.
|
|
img = QImage(tga.width, tga.height, QImage::Format_RGB32);
|
|
if (img.isNull()) {
|
|
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
|
|
return false;
|
|
}
|
|
|
|
TgaHeaderInfo info(tga);
|
|
|
|
// Bits 0-3 are the numbers of alpha bits (can be zero!)
|
|
const int numAlphaBits = tga.flags & 0xf;
|
|
// However alpha exists only in the 32 bit format.
|
|
if ((tga.pixel_size == 32) && (tga.flags & 0xf)) {
|
|
img = QImage(tga.width, tga.height, QImage::Format_ARGB32);
|
|
if (img.isNull()) {
|
|
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
|
|
return false;
|
|
}
|
|
|
|
if (numAlphaBits > 8) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
uint pixel_size = (tga.pixel_size / 8);
|
|
qint64 size = qint64(tga.width) * qint64(tga.height) * pixel_size;
|
|
|
|
if (size < 1) {
|
|
// qDebug() << "This TGA file is broken with size " << size;
|
|
return false;
|
|
}
|
|
|
|
// Read palette.
|
|
static const int max_palette_size = 768;
|
|
char palette[max_palette_size];
|
|
if (info.pal) {
|
|
// @todo Support palettes in other formats!
|
|
const int palette_size = 3 * tga.colormap_length;
|
|
if (palette_size > max_palette_size) {
|
|
return false;
|
|
}
|
|
const int dataRead = s.readRawData(palette, palette_size);
|
|
if (dataRead < 0) {
|
|
return false;
|
|
}
|
|
if (dataRead < max_palette_size) {
|
|
memset(&palette[dataRead], 0, max_palette_size - dataRead);
|
|
}
|
|
}
|
|
|
|
// Allocate image.
|
|
uchar *const image = reinterpret_cast<uchar *>(malloc(size));
|
|
if (!image) {
|
|
return false;
|
|
}
|
|
|
|
bool valid = true;
|
|
|
|
if (info.rle) {
|
|
// Decode image.
|
|
char *dst = (char *)image;
|
|
char *imgEnd = dst + size;
|
|
qint64 num = size;
|
|
|
|
while (num > 0 && valid) {
|
|
if (s.atEnd()) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
|
|
// Get packet header.
|
|
uchar c;
|
|
s >> c;
|
|
|
|
uint count = (c & 0x7f) + 1;
|
|
num -= count * pixel_size;
|
|
if (num < 0) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
|
|
if (c & 0x80) {
|
|
// RLE pixels.
|
|
assert(pixel_size <= 8);
|
|
char pixel[8];
|
|
const int dataRead = s.readRawData(pixel, pixel_size);
|
|
if (dataRead < (int)pixel_size) {
|
|
memset(&pixel[dataRead], 0, pixel_size - dataRead);
|
|
}
|
|
do {
|
|
if (dst + pixel_size > imgEnd) {
|
|
qWarning() << "Trying to write out of bounds!" << ptrdiff_t(dst) << (ptrdiff_t(imgEnd) - ptrdiff_t(pixel_size));
|
|
valid = false;
|
|
break;
|
|
}
|
|
|
|
memcpy(dst, pixel, pixel_size);
|
|
dst += pixel_size;
|
|
} while (--count);
|
|
} else {
|
|
// Raw pixels.
|
|
count *= pixel_size;
|
|
const int dataRead = s.readRawData(dst, count);
|
|
if (dataRead < 0) {
|
|
free(image);
|
|
return false;
|
|
}
|
|
|
|
if ((uint)dataRead < count) {
|
|
const size_t toCopy = count - dataRead;
|
|
if (&dst[dataRead] + toCopy > imgEnd) {
|
|
qWarning() << "Trying to write out of bounds!" << ptrdiff_t(image) << ptrdiff_t(&dst[dataRead]);
|
|
;
|
|
valid = false;
|
|
break;
|
|
}
|
|
|
|
memset(&dst[dataRead], 0, toCopy);
|
|
}
|
|
dst += count;
|
|
}
|
|
}
|
|
} else {
|
|
// Read raw image.
|
|
const int dataRead = s.readRawData((char *)image, size);
|
|
if (dataRead < 0) {
|
|
free(image);
|
|
return false;
|
|
}
|
|
if (dataRead < size) {
|
|
memset(&image[dataRead], 0, size - dataRead);
|
|
}
|
|
}
|
|
|
|
if (!valid) {
|
|
free(image);
|
|
return false;
|
|
}
|
|
|
|
// Convert image to internal format.
|
|
int y_start, y_step, y_end;
|
|
if (tga.flags & TGA_ORIGIN_UPPER) {
|
|
y_start = 0;
|
|
y_step = 1;
|
|
y_end = tga.height;
|
|
} else {
|
|
y_start = tga.height - 1;
|
|
y_step = -1;
|
|
y_end = -1;
|
|
}
|
|
|
|
uchar *src = image;
|
|
|
|
for (int y = y_start; y != y_end; y += y_step) {
|
|
QRgb *scanline = (QRgb *)img.scanLine(y);
|
|
|
|
if (info.pal) {
|
|
// Paletted.
|
|
for (int x = 0; x < tga.width; x++) {
|
|
uchar idx = *src++;
|
|
scanline[x] = qRgb(palette[3 * idx + 2], palette[3 * idx + 1], palette[3 * idx + 0]);
|
|
}
|
|
} else if (info.grey) {
|
|
// Greyscale.
|
|
for (int x = 0; x < tga.width; x++) {
|
|
scanline[x] = qRgb(*src, *src, *src);
|
|
src++;
|
|
}
|
|
} else {
|
|
// True Color.
|
|
if (tga.pixel_size == 16) {
|
|
for (int x = 0; x < tga.width; x++) {
|
|
Color555 c = *reinterpret_cast<Color555 *>(src);
|
|
scanline[x] = qRgb((c.r << 3) | (c.r >> 2), (c.g << 3) | (c.g >> 2), (c.b << 3) | (c.b >> 2));
|
|
src += 2;
|
|
}
|
|
} else if (tga.pixel_size == 24) {
|
|
for (int x = 0; x < tga.width; x++) {
|
|
scanline[x] = qRgb(src[2], src[1], src[0]);
|
|
src += 3;
|
|
}
|
|
} else if (tga.pixel_size == 32) {
|
|
for (int x = 0; x < tga.width; x++) {
|
|
// ### TODO: verify with images having really some alpha data
|
|
const uchar alpha = (src[3] << (8 - numAlphaBits));
|
|
scanline[x] = qRgba(src[2], src[1], src[0], alpha);
|
|
src += 4;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Free image.
|
|
free(image);
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TGAHandler::TGAHandler()
|
|
{
|
|
}
|
|
|
|
bool TGAHandler::canRead() const
|
|
{
|
|
if (canRead(device())) {
|
|
setFormat("tga");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TGAHandler::read(QImage *outImage)
|
|
{
|
|
// qDebug() << "Loading TGA file!";
|
|
|
|
QDataStream s(device());
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
// Read image header.
|
|
TgaHeader tga;
|
|
s >> tga;
|
|
s.device()->seek(TgaHeader::SIZE + tga.id_length);
|
|
|
|
// Check image file format.
|
|
if (s.atEnd()) {
|
|
// qDebug() << "This TGA file is not valid.";
|
|
return false;
|
|
}
|
|
|
|
// Check supported file types.
|
|
if (!IsSupported(tga)) {
|
|
// qDebug() << "This TGA file is not supported.";
|
|
return false;
|
|
}
|
|
|
|
QImage img;
|
|
bool result = LoadTGA(s, tga, img);
|
|
|
|
if (result == false) {
|
|
// qDebug() << "Error loading TGA file.";
|
|
return false;
|
|
}
|
|
|
|
*outImage = img;
|
|
return true;
|
|
}
|
|
|
|
bool TGAHandler::write(const QImage &image)
|
|
{
|
|
QDataStream s(device());
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
const QImage &img = image;
|
|
const bool hasAlpha = (img.format() == QImage::Format_ARGB32);
|
|
for (int i = 0; i < 12; i++) {
|
|
s << targaMagic[i];
|
|
}
|
|
|
|
// write header
|
|
s << quint16(img.width()); // width
|
|
s << quint16(img.height()); // height
|
|
s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
|
|
s << quint8(hasAlpha ? 0x24 : 0x20); // top left image (0x20) + 8 bit alpha (0x4)
|
|
|
|
for (int y = 0; y < img.height(); y++)
|
|
for (int x = 0; x < img.width(); x++) {
|
|
const QRgb color = img.pixel(x, y);
|
|
s << quint8(qBlue(color));
|
|
s << quint8(qGreen(color));
|
|
s << quint8(qRed(color));
|
|
if (hasAlpha) {
|
|
s << quint8(qAlpha(color));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TGAHandler::canRead(QIODevice *device)
|
|
{
|
|
if (!device) {
|
|
qWarning("TGAHandler::canRead() called with no device");
|
|
return false;
|
|
}
|
|
|
|
qint64 oldPos = device->pos();
|
|
QByteArray head = device->read(TgaHeader::SIZE);
|
|
int readBytes = head.size();
|
|
|
|
if (device->isSequential()) {
|
|
for (int pos = readBytes - 1; pos >= 0; --pos) {
|
|
device->ungetChar(head[pos]);
|
|
}
|
|
} else {
|
|
device->seek(oldPos);
|
|
}
|
|
|
|
if (readBytes < TgaHeader::SIZE) {
|
|
return false;
|
|
}
|
|
|
|
QDataStream stream(head);
|
|
stream.setByteOrder(QDataStream::LittleEndian);
|
|
TgaHeader tga;
|
|
stream >> tga;
|
|
return IsSupported(tga);
|
|
}
|
|
|
|
QImageIOPlugin::Capabilities TGAPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
|
{
|
|
if (format == "tga") {
|
|
return Capabilities(CanRead | CanWrite);
|
|
}
|
|
if (!format.isEmpty()) {
|
|
return {};
|
|
}
|
|
if (!device->isOpen()) {
|
|
return {};
|
|
}
|
|
|
|
Capabilities cap;
|
|
if (device->isReadable() && TGAHandler::canRead(device)) {
|
|
cap |= CanRead;
|
|
}
|
|
if (device->isWritable()) {
|
|
cap |= CanWrite;
|
|
}
|
|
return cap;
|
|
}
|
|
|
|
QImageIOHandler *TGAPlugin::create(QIODevice *device, const QByteArray &format) const
|
|
{
|
|
QImageIOHandler *handler = new TGAHandler;
|
|
handler->setDevice(device);
|
|
handler->setFormat(format);
|
|
return handler;
|
|
}
|