mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-11-22 10:02:43 -05:00
1385 lines
41 KiB
C++
1385 lines
41 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-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.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, 15, 16, 24 and 32.
|
|
* writing:
|
|
* uncompressed rgb color tga files
|
|
* uncompressed grayscale tga files
|
|
* uncompressed indexed tga files
|
|
*/
|
|
|
|
#include "microexif_p.h"
|
|
#include "scanlineconverter_p.h"
|
|
#include "tga_p.h"
|
|
#include "util_p.h"
|
|
|
|
#include <assert.h>
|
|
|
|
#include <QColorSpace>
|
|
#include <QDataStream>
|
|
#include <QDateTime>
|
|
#include <QImage>
|
|
#include <QLoggingCategory>
|
|
|
|
typedef quint32 uint;
|
|
typedef quint16 ushort;
|
|
typedef quint8 uchar;
|
|
|
|
#ifdef QT_DEBUG
|
|
Q_LOGGING_CATEGORY(LOG_TGAPLUGIN, "kf.imageformats.plugins.tga", QtDebugMsg)
|
|
#else
|
|
Q_LOGGING_CATEGORY(LOG_TGAPLUGIN, "kf.imageformats.plugins.tga", QtWarningMsg)
|
|
#endif
|
|
|
|
#ifndef TGA_V2E_AS_DEFAULT
|
|
/*
|
|
* Uncomment to change the default version of the plugin to `TGAv2E`.
|
|
*/
|
|
// #define TGA_V2E_AS_DEFAULT
|
|
#endif // TGA_V2E_AS_DEFAULT
|
|
|
|
namespace // Private.
|
|
{
|
|
// Header format of saved files.
|
|
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
|
|
|
|
/*
|
|
* Each TAG is a SHORT value in the range of 0 to 65535. Values from 0 - 32767 are available
|
|
* for developer use, while values from 32768 - 65535 are reserved for Truevision.
|
|
* Truevision will maintain a list of tags assigned to companies.
|
|
* In any case, there's no public "list of tag" and Truevision no longer exists.
|
|
*/
|
|
#define TGA_EXIF_TAGID 0x7001 // Exif data preceded by "eXif" string
|
|
#define TGA_XMPP_TAGID 0x7002 // Xmp packet preceded by "xMPP" string
|
|
#define TGA_ICCP_TAGID 0x7003 // Icc profile preceded by "iCCP" string
|
|
|
|
/** Tga Header. */
|
|
struct TgaHeader {
|
|
uchar id_length = 0;
|
|
uchar colormap_type = 0;
|
|
uchar image_type = 0;
|
|
ushort colormap_index = 0;
|
|
ushort colormap_length = 0;
|
|
uchar colormap_size = 0;
|
|
ushort x_origin = 0;
|
|
ushort y_origin = 0;
|
|
ushort width = 0;
|
|
ushort height = 0;
|
|
uchar pixel_size = 0;
|
|
uchar flags = 0;
|
|
|
|
enum {
|
|
SIZE = 18,
|
|
}; // const static int SIZE = 18;
|
|
};
|
|
|
|
/** Tga 2.0 Footer */
|
|
struct TgaFooter {
|
|
TgaFooter()
|
|
: extensionOffset(0)
|
|
, developerOffset(0)
|
|
{
|
|
std::memcpy(signature, "TRUEVISION-XFILE.\0", 18);
|
|
}
|
|
bool isValid() const
|
|
{
|
|
return std::memcmp(signature, "TRUEVISION-XFILE.\0", 18) == 0;
|
|
}
|
|
|
|
quint32 extensionOffset; // Extension Area Offset
|
|
quint32 developerOffset; // Developer Directory Offset
|
|
char signature[18]; // TGA Signature
|
|
};
|
|
|
|
/** Tga 2.0 extension area */
|
|
struct TgaExtension {
|
|
enum AttributeType : quint16 {
|
|
NoAlpha = 0, // no Alpha data included (bits 3-0 of TgaHeader::flags should also be set to zero).
|
|
IgnoreAlpha = 1, // undefined data in the Alpha field, can be ignored
|
|
RetainAlpha = 2, // undefined data in the Alpha field, but should be retained
|
|
Alpha = 3, // useful Alpha channel data is present
|
|
PremultipliedAlpha = 4 // pre-multiplied Alpha (see description below)
|
|
};
|
|
|
|
TgaExtension()
|
|
{
|
|
std::memset(this, 0, sizeof(TgaExtension));
|
|
size = 495; // TGA 2.0 specs
|
|
|
|
// If you do not use Software Version field, set the SHORT to binary
|
|
// zero, and the BYTE to a space (' ').
|
|
versionLetter = 0x20;
|
|
}
|
|
|
|
bool isValid() const
|
|
{
|
|
return size == 495;
|
|
}
|
|
|
|
void setDateTime(const QDateTime &dt)
|
|
{
|
|
if (dt.isValid()) {
|
|
auto date = dt.date();
|
|
stampMonth = date.month();
|
|
stampDay = date.day();
|
|
stampYear = date.year();
|
|
auto time = dt.time();
|
|
stampHour = time.hour();
|
|
stampMinute = time.minute();
|
|
stampSecond = time.second();
|
|
}
|
|
}
|
|
QDateTime dateTime() const
|
|
{
|
|
auto date = QDate(stampYear, stampMonth, stampDay);
|
|
auto time = QTime(stampHour, stampMinute, stampSecond);
|
|
if (!date.isValid() || !time.isValid())
|
|
return {};
|
|
return QDateTime(date, time);
|
|
}
|
|
|
|
void setAuthor(const QString &str)
|
|
{
|
|
auto ba = str.toLatin1();
|
|
std::memcpy(authorName, ba.data(), std::min(sizeof(authorName) - 1, size_t(ba.size())));
|
|
}
|
|
QString author() const
|
|
{
|
|
if (authorName[sizeof(authorName) - 1] != char(0))
|
|
return {};
|
|
return QString::fromLatin1(authorName);
|
|
}
|
|
|
|
void setComment(const QString &str)
|
|
{
|
|
auto ba = str.toLatin1();
|
|
std::memcpy(authorComment, ba.data(), std::min(sizeof(authorComment) - 1, size_t(ba.size())));
|
|
}
|
|
QString comment() const
|
|
{
|
|
if (authorComment[sizeof(authorComment) - 1] != char(0))
|
|
return {};
|
|
return QString::fromLatin1(authorComment);
|
|
}
|
|
|
|
void setSoftware(const QString &str)
|
|
{
|
|
auto ba = str.toLatin1();
|
|
std::memcpy(softwareId, ba.data(), std::min(sizeof(softwareId) - 1, size_t(ba.size())));
|
|
}
|
|
QString software() const
|
|
{
|
|
if (softwareId[sizeof(softwareId) - 1] != char(0))
|
|
return {};
|
|
return QString::fromLatin1(softwareId);
|
|
}
|
|
|
|
quint16 size; // Extension Size
|
|
char authorName[41]; // Author Name
|
|
char authorComment[324]; // Author Comment
|
|
quint16 stampMonth; // Date/Time Stamp: Month
|
|
quint16 stampDay; // Date/Time Stamp: Day
|
|
quint16 stampYear; // Date/Time Stamp: Year
|
|
quint16 stampHour; // Date/Time Stamp: Hour
|
|
quint16 stampMinute; // Date/Time Stamp: Minute
|
|
quint16 stampSecond; // Date/Time Stamp: Second
|
|
char jobName[41]; // Job Name/ID
|
|
quint16 jobHour; // Job Time: Hours
|
|
quint16 jobMinute; // Job Time: Minutes
|
|
quint16 jobSecond; // Job Time: Seconds
|
|
char softwareId[41]; // Software ID
|
|
quint16 versionNumber; // Software Version Number
|
|
quint8 versionLetter; // Software Version Letter
|
|
quint32 keyColor; // Key Color
|
|
quint16 pixelNumerator; // Pixel Aspect Ratio
|
|
quint16 pixelDenominator; // Pixel Aspect Ratio
|
|
quint16 gammaNumerator; // Gamma Value
|
|
quint16 gammaDenominator; // Gamma Value
|
|
quint32 colorOffset; // Color Correction Offset
|
|
quint32 stampOffset; // Postage Stamp Offset
|
|
quint32 scanOffset; // Scan-Line Table Offset
|
|
quint8 attributesType; // Attributes Types
|
|
};
|
|
|
|
struct TgaDeveloperDirectory {
|
|
struct Field {
|
|
quint16 tagId;
|
|
quint32 offset;
|
|
quint32 size;
|
|
};
|
|
|
|
bool isEmpty() const
|
|
{
|
|
return fields.isEmpty();
|
|
}
|
|
|
|
QList<Field> fields;
|
|
};
|
|
|
|
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;
|
|
return s;
|
|
}
|
|
|
|
static QDataStream &operator>>(QDataStream &s, TgaFooter &footer)
|
|
{
|
|
s >> footer.extensionOffset;
|
|
s >> footer.developerOffset;
|
|
s.readRawData(footer.signature, sizeof(footer.signature));
|
|
return s;
|
|
}
|
|
|
|
static QDataStream &operator<<(QDataStream &s, const TgaFooter &footer)
|
|
{
|
|
s << footer.extensionOffset;
|
|
s << footer.developerOffset;
|
|
s.writeRawData(footer.signature, sizeof(footer.signature));
|
|
return s;
|
|
}
|
|
|
|
static QDataStream &operator>>(QDataStream &s, TgaDeveloperDirectory &dir)
|
|
{
|
|
quint16 n;
|
|
s >> n;
|
|
for (auto i = n; i > 0; --i) {
|
|
TgaDeveloperDirectory::Field f;
|
|
s >> f.tagId;
|
|
s >> f.offset;
|
|
s >> f.size;
|
|
dir.fields << f;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
static QDataStream &operator<<(QDataStream &s, const TgaDeveloperDirectory &dir)
|
|
{
|
|
s << quint16(dir.fields.size());
|
|
for (auto &&f : dir.fields) {
|
|
s << f.tagId;
|
|
s << f.offset;
|
|
s << f.size;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
static QDataStream &operator>>(QDataStream &s, TgaExtension &ext)
|
|
{
|
|
s >> ext.size;
|
|
s.readRawData(ext.authorName, sizeof(ext.authorName));
|
|
s.readRawData(ext.authorComment, sizeof(ext.authorComment));
|
|
s >> ext.stampMonth;
|
|
s >> ext.stampDay;
|
|
s >> ext.stampYear;
|
|
s >> ext.stampHour;
|
|
s >> ext.stampMinute;
|
|
s >> ext.stampSecond;
|
|
s.readRawData(ext.jobName, sizeof(ext.jobName));
|
|
s >> ext.jobHour;
|
|
s >> ext.jobMinute;
|
|
s >> ext.jobSecond;
|
|
s.readRawData(ext.softwareId, sizeof(ext.softwareId));
|
|
s >> ext.versionNumber;
|
|
s >> ext.versionLetter;
|
|
s >> ext.keyColor;
|
|
s >> ext.pixelNumerator;
|
|
s >> ext.pixelDenominator;
|
|
s >> ext.gammaNumerator;
|
|
s >> ext.gammaDenominator;
|
|
s >> ext.colorOffset;
|
|
s >> ext.stampOffset;
|
|
s >> ext.scanOffset;
|
|
s >> ext.attributesType;
|
|
return s;
|
|
}
|
|
|
|
static QDataStream &operator<<(QDataStream &s, const TgaExtension &ext)
|
|
{
|
|
s << ext.size;
|
|
s.writeRawData(ext.authorName, sizeof(ext.authorName));
|
|
s.writeRawData(ext.authorComment, sizeof(ext.authorComment));
|
|
s << ext.stampMonth;
|
|
s << ext.stampDay;
|
|
s << ext.stampYear;
|
|
s << ext.stampHour;
|
|
s << ext.stampMinute;
|
|
s << ext.stampSecond;
|
|
s.writeRawData(ext.jobName, sizeof(ext.jobName));
|
|
s << ext.jobHour;
|
|
s << ext.jobMinute;
|
|
s << ext.jobSecond;
|
|
s.writeRawData(ext.softwareId, sizeof(ext.softwareId));
|
|
s << ext.versionNumber;
|
|
s << ext.versionLetter;
|
|
s << ext.keyColor;
|
|
s << ext.pixelNumerator;
|
|
s << ext.pixelDenominator;
|
|
s << ext.gammaNumerator;
|
|
s << ext.gammaDenominator;
|
|
s << ext.colorOffset;
|
|
s << ext.stampOffset;
|
|
s << ext.scanOffset;
|
|
s << ext.attributesType;
|
|
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) {
|
|
// GIMP saves TGAs with palette size of 257 (but 256 used) so, I need to check the pixel size only.
|
|
if (head.pixel_size > 8 || head.colormap_type != 1) {
|
|
return false;
|
|
}
|
|
if (head.colormap_size != 15 && head.colormap_size != 16 && head.colormap_size != 24 && head.colormap_size != 32) {
|
|
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 != 15 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
|
|
return false;
|
|
}
|
|
// 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.
|
|
if (head.colormap_type == 0 && (head.colormap_index != 0 || head.colormap_length != 0)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*!
|
|
* \brief imageId
|
|
* Create the TGA imageId from the image TITLE metadata
|
|
*/
|
|
static QByteArray imageId(const QImage &img)
|
|
{
|
|
auto ba = img.text(QStringLiteral(META_KEY_TITLE)).trimmed().toLatin1();
|
|
if (ba.size() > 255)
|
|
ba = ba.left(255);
|
|
return ba;
|
|
}
|
|
|
|
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 QImage::Format imageFormat(const TgaHeader &head)
|
|
{
|
|
auto format = QImage::Format_Invalid;
|
|
if (IsSupported(head)) {
|
|
TgaHeaderInfo info(head);
|
|
|
|
// Bits 0-3 are the numbers of alpha bits (can be zero!)
|
|
const int numAlphaBits = head.flags & 0xf;
|
|
// However alpha should exists only in the 32 bit format.
|
|
if ((head.pixel_size == 32) && (numAlphaBits)) {
|
|
if (numAlphaBits <= 8) {
|
|
format = QImage::Format_ARGB32;
|
|
}
|
|
// Anyway, GIMP also saves gray images with alpha in TGA format
|
|
} else if ((info.grey) && (head.pixel_size == 16) && (numAlphaBits)) {
|
|
if (numAlphaBits == 8) {
|
|
format = QImage::Format_ARGB32;
|
|
}
|
|
} else if (info.grey) {
|
|
format = QImage::Format_Grayscale8;
|
|
} else if (info.pal) {
|
|
format = QImage::Format_Indexed8;
|
|
} else if (info.rgb && (head.pixel_size == 15 || head.pixel_size == 16)) {
|
|
format = QImage::Format_RGB555;
|
|
} else {
|
|
format = QImage::Format_RGB32;
|
|
}
|
|
}
|
|
return format;
|
|
}
|
|
|
|
/*!
|
|
* \brief peekHeader
|
|
* Reads the header but does not change the position in the device.
|
|
*/
|
|
static bool peekHeader(QIODevice *device, TgaHeader &header)
|
|
{
|
|
auto head = device->peek(TgaHeader::SIZE);
|
|
if (head.size() < TgaHeader::SIZE) {
|
|
return false;
|
|
}
|
|
QDataStream stream(head);
|
|
stream.setByteOrder(QDataStream::LittleEndian);
|
|
stream >> header;
|
|
return true;
|
|
}
|
|
|
|
/*!
|
|
* \brief readTgaLine
|
|
* Read a scan line from the raw data.
|
|
* \param dev The current device.
|
|
* \param pixel_size The number of bytes per pixel.
|
|
* \param size The size of the uncompressed TGA raw line
|
|
* \param rle True if the stream is RLE compressed, otherwise false.
|
|
* \param cache The cache buffer used to store data (only used when the stream is RLE).
|
|
* \return The uncompressed raw data of a line or an empty array on error.
|
|
*/
|
|
static QByteArray readTgaLine(QIODevice *dev, qint32 pixel_size, qint32 size, bool rle, QByteArray &cache)
|
|
{
|
|
// uncompressed stream
|
|
if (!rle) {
|
|
auto ba = dev->read(size);
|
|
if (ba.size() != size)
|
|
ba.clear();
|
|
return ba;
|
|
}
|
|
|
|
// RLE compressed stream
|
|
if (cache.size() < qsizetype(size)) {
|
|
// Decode image.
|
|
qint64 num = size;
|
|
|
|
while (num > 0) {
|
|
if (dev->atEnd()) {
|
|
break;
|
|
}
|
|
|
|
// Get packet header.
|
|
char cc;
|
|
if (dev->read(&cc, 1) != 1) {
|
|
cache.clear();
|
|
break;
|
|
}
|
|
auto c = uchar(cc);
|
|
|
|
uint count = (c & 0x7f) + 1;
|
|
QByteArray tmp(count * pixel_size, char());
|
|
auto dst = tmp.data();
|
|
num -= count * pixel_size;
|
|
|
|
if (c & 0x80) { // RLE pixels.
|
|
assert(pixel_size <= 8);
|
|
char pixel[8];
|
|
const int dataRead = dev->read(pixel, pixel_size);
|
|
if (dataRead < (int)pixel_size) {
|
|
memset(&pixel[dataRead], 0, pixel_size - dataRead);
|
|
}
|
|
do {
|
|
memcpy(dst, pixel, pixel_size);
|
|
dst += pixel_size;
|
|
} while (--count);
|
|
} else { // Raw pixels.
|
|
count *= pixel_size;
|
|
const int dataRead = dev->read(dst, count);
|
|
if (dataRead < 0) {
|
|
cache.clear();
|
|
break;
|
|
}
|
|
|
|
if ((uint)dataRead < count) {
|
|
const size_t toCopy = count - dataRead;
|
|
memset(&dst[dataRead], 0, toCopy);
|
|
}
|
|
dst += count;
|
|
}
|
|
|
|
cache.append(tmp);
|
|
}
|
|
}
|
|
|
|
auto data = cache.left(size);
|
|
cache.remove(0, size);
|
|
if (data.size() != size)
|
|
data.clear();
|
|
return data;
|
|
}
|
|
|
|
inline QRgb rgb555ToRgb(char c0, char c1)
|
|
{
|
|
// c0 = GGGBBBBB
|
|
// c1 = IRRRRRGG (I = interrupt control of VDA(D) -> ignore it)
|
|
return qRgb(int((c1 >> 2) & 0x1F) * 255 / 31, int(((c1 & 3) << 3) | ((c0 >> 5) & 7)) * 255 / 31, int(c0 & 0x1F) * 255 / 31);
|
|
}
|
|
|
|
static bool LoadTGA(QIODevice *dev, const TgaHeader &tga, QImage &img)
|
|
{
|
|
img = imageAlloc(tga.width, tga.height, imageFormat(tga));
|
|
if (img.isNull()) {
|
|
qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Failed to allocate image, invalid dimensions?" << QSize(tga.width, tga.height);
|
|
return false;
|
|
}
|
|
|
|
TgaHeaderInfo info(tga);
|
|
|
|
const int numAlphaBits = qBound(0, tga.flags & 0xf, 8);
|
|
bool hasAlpha = img.hasAlphaChannel() && numAlphaBits > 0;
|
|
qint32 pixel_size = (tga.pixel_size == 15 ? 16 : tga.pixel_size) / 8;
|
|
qint32 line_size = qint32(tga.width) * pixel_size;
|
|
qint64 size = qint64(tga.height) * line_size;
|
|
if (size < 1) {
|
|
// qCDebug(LOG_TGAPLUGIN) << "This TGA file is broken with size " << size;
|
|
return false;
|
|
}
|
|
|
|
// Read palette.
|
|
if (info.pal) {
|
|
QList<QRgb> colorTable;
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
|
|
colorTable.resize(tga.colormap_length);
|
|
#else
|
|
colorTable.resizeForOverwrite(tga.colormap_length);
|
|
#endif
|
|
|
|
if (tga.colormap_size == 32) {
|
|
char data[4]; // BGRA
|
|
for (QRgb &rgb : colorTable) {
|
|
const auto dataRead = dev->read(data, 4);
|
|
if (dataRead < 4) {
|
|
return false;
|
|
}
|
|
// BGRA.
|
|
rgb = qRgba(data[2], data[1], data[0], data[3]);
|
|
}
|
|
} else if (tga.colormap_size == 24) {
|
|
char data[3]; // BGR
|
|
for (QRgb &rgb : colorTable) {
|
|
const auto dataRead = dev->read(data, 3);
|
|
if (dataRead < 3) {
|
|
return false;
|
|
}
|
|
// BGR.
|
|
rgb = qRgb(data[2], data[1], data[0]);
|
|
}
|
|
} else if (tga.colormap_size == 16 || tga.colormap_size == 15) {
|
|
char data[2];
|
|
for (QRgb &rgb : colorTable) {
|
|
const auto dataRead = dev->read(data, 2);
|
|
if (dataRead < 2) {
|
|
return false;
|
|
}
|
|
rgb = rgb555ToRgb(data[0], data[1]);
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
img.setColorTable(colorTable);
|
|
}
|
|
|
|
// Convert image to internal format.
|
|
bool valid = true;
|
|
int y_start = tga.height - 1;
|
|
int y_step = -1;
|
|
int y_end = -1;
|
|
if (tga.flags & TGA_ORIGIN_UPPER) {
|
|
y_start = 0;
|
|
y_step = 1;
|
|
y_end = tga.height;
|
|
}
|
|
int x_start = 0;
|
|
int x_step = 1;
|
|
int x_end = tga.width;
|
|
if (tga.flags & TGA_ORIGIN_RIGHT) {
|
|
x_start = tga.width - 1;
|
|
x_step = -1;
|
|
x_end = -1;
|
|
}
|
|
|
|
QByteArray cache;
|
|
for (int y = y_start; y != y_end; y += y_step) {
|
|
auto tgaLine = readTgaLine(dev, pixel_size, line_size, info.rle, cache);
|
|
if (tgaLine.size() != qsizetype(line_size)) {
|
|
qCWarning(LOG_TGAPLUGIN) << "LoadTGA: Error while decoding a TGA raw line";
|
|
valid = false;
|
|
break;
|
|
}
|
|
auto src = tgaLine.data();
|
|
if (info.pal && img.depth() == 8) {
|
|
// Paletted.
|
|
auto scanline = img.scanLine(y);
|
|
for (int x = x_start; x != x_end; x += x_step) {
|
|
uchar idx = *src++;
|
|
if (Q_UNLIKELY(idx >= tga.colormap_length)) {
|
|
valid = false;
|
|
break;
|
|
}
|
|
scanline[x] = idx;
|
|
}
|
|
} else if (info.grey) {
|
|
if (tga.pixel_size == 16 && img.depth() == 32) { // Greyscale with alpha.
|
|
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
|
|
for (int x = x_start; x != x_end; x += x_step) {
|
|
scanline[x] = qRgba(*src, *src, *src, *(src + 1));
|
|
src += 2;
|
|
}
|
|
} else if (tga.pixel_size == 8 && img.depth() == 8) { // Greyscale.
|
|
auto scanline = img.scanLine(y);
|
|
for (int x = x_start; x != x_end; x += x_step) {
|
|
scanline[x] = *src;
|
|
src++;
|
|
}
|
|
} else {
|
|
valid = false;
|
|
break;
|
|
}
|
|
} else {
|
|
// True Color.
|
|
if ((tga.pixel_size == 15 || tga.pixel_size == 16) && img.depth() == 16) {
|
|
auto scanline = reinterpret_cast<quint16 *>(img.scanLine(y));
|
|
for (int x = x_start; x != x_end; x += x_step) {
|
|
scanline[x] = ((quint16(src[1] & 0x7f) << 8) | quint8(src[0]));
|
|
src += 2;
|
|
}
|
|
} else if (tga.pixel_size == 24 && img.depth() == 32) {
|
|
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
|
|
for (int x = x_start; x != x_end; x += x_step) {
|
|
scanline[x] = qRgb(src[2], src[1], src[0]);
|
|
src += 3;
|
|
}
|
|
} else if (tga.pixel_size == 32 && img.depth() == 32) {
|
|
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
|
|
auto div = (1 << numAlphaBits) - 1;
|
|
if (div == 0)
|
|
hasAlpha = false;
|
|
for (int x = x_start; x != x_end; x += x_step) {
|
|
const int alpha = hasAlpha ? int((src[3]) << (8 - numAlphaBits)) * 255 / div : 255;
|
|
scanline[x] = qRgba(src[2], src[1], src[0], alpha);
|
|
src += 4;
|
|
}
|
|
} else {
|
|
valid = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!cache.isEmpty() && valid) {
|
|
qCDebug(LOG_TGAPLUGIN) << "LoadTGA: Found unused image data";
|
|
}
|
|
|
|
return valid;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class TGAHandlerPrivate
|
|
{
|
|
public:
|
|
TGAHandlerPrivate()
|
|
#ifdef TGA_V2E_AS_DEFAULT
|
|
: m_subType(subTypeTGA_V2E())
|
|
#else
|
|
: m_subType(subTypeTGA_V2S())
|
|
#endif
|
|
{
|
|
}
|
|
~TGAHandlerPrivate() {}
|
|
|
|
static QByteArray subTypeTGA_V1()
|
|
{
|
|
return QByteArrayLiteral("TGAv1");
|
|
}
|
|
static QByteArray subTypeTGA_V2S()
|
|
{
|
|
return QByteArrayLiteral("TGAv2");
|
|
}
|
|
static QByteArray subTypeTGA_V2E()
|
|
{
|
|
return QByteArrayLiteral("TGAv2E");
|
|
}
|
|
|
|
TgaHeader m_header;
|
|
|
|
QByteArray m_subType;
|
|
};
|
|
|
|
TGAHandler::TGAHandler()
|
|
: QImageIOHandler()
|
|
, d(new TGAHandlerPrivate)
|
|
{
|
|
}
|
|
|
|
bool TGAHandler::canRead() const
|
|
{
|
|
if (canRead(device())) {
|
|
setFormat("tga");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool TGAHandler::read(QImage *outImage)
|
|
{
|
|
// qCDebug(LOG_TGAPLUGIN) << "Loading TGA file!";
|
|
|
|
auto dev = device();
|
|
auto&& tga = d->m_header;
|
|
if (!peekHeader(dev, tga) || !IsSupported(tga)) {
|
|
// qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid.";
|
|
return false;
|
|
}
|
|
|
|
QByteArray imageId;
|
|
if (dev->isSequential()) {
|
|
auto tmp = dev->read(TgaHeader::SIZE);
|
|
if (tmp.size() != TgaHeader::SIZE)
|
|
return false;
|
|
} else {
|
|
if (!dev->seek(TgaHeader::SIZE))
|
|
return false;
|
|
}
|
|
if (tga.id_length > 0) {
|
|
imageId = dev->read(tga.id_length);
|
|
}
|
|
|
|
// Check image file format.
|
|
if (dev->atEnd()) {
|
|
// qCDebug(LOG_TGAPLUGIN) << "This TGA file is not valid.";
|
|
return false;
|
|
}
|
|
|
|
QImage img;
|
|
if (!LoadTGA(dev, tga, img)) {
|
|
// qCDebug(LOG_TGAPLUGIN) << "Error loading TGA file.";
|
|
return false;
|
|
} else if (!imageId.isEmpty()) {
|
|
img.setText(QStringLiteral(META_KEY_TITLE), QString::fromLatin1(imageId));
|
|
}
|
|
if (!readMetadata(img)) {
|
|
qCDebug(LOG_TGAPLUGIN) << "read: error while reading metadata";
|
|
}
|
|
|
|
*outImage = img;
|
|
return true;
|
|
}
|
|
|
|
bool TGAHandler::write(const QImage &image)
|
|
{
|
|
auto ok = false;
|
|
if (image.format() == QImage::Format_Indexed8) {
|
|
ok = writeIndexed(image);
|
|
} else if (image.format() == QImage::Format_Grayscale8 || image.format() == QImage::Format_Grayscale16) {
|
|
ok = writeGrayscale(image);
|
|
} else if (image.format() == QImage::Format_RGB555 || image.format() == QImage::Format_RGB16 || image.format() == QImage::Format_RGB444) {
|
|
ok = writeRGB555(image);
|
|
} else {
|
|
ok = writeRGBA(image);
|
|
}
|
|
return (ok && writeMetadata(image));
|
|
}
|
|
|
|
bool TGAHandler::writeIndexed(const QImage &image)
|
|
{
|
|
auto dev = device();
|
|
{ // write header and palette
|
|
QDataStream s(dev);
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
auto ct = image.colorTable();
|
|
auto iid = imageId(image);
|
|
s << quint8(iid.size()); // ID Length
|
|
s << quint8(1); // Color Map Type
|
|
s << quint8(TGA_TYPE_INDEXED); // Image Type
|
|
s << quint16(0); // First Entry Index
|
|
s << quint16(ct.size()); // Color Map Length
|
|
s << quint8(32); // Color map Entry Size
|
|
s << quint16(0); // X-origin of Image
|
|
s << quint16(0); // Y-origin of Image
|
|
|
|
s << quint16(image.width()); // Image Width
|
|
s << quint16(image.height()); // Image Height
|
|
s << quint8(8); // Pixel Depth
|
|
s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor
|
|
|
|
if (!iid.isEmpty())
|
|
s.writeRawData(iid.data(), iid.size());
|
|
|
|
for (auto &&rgb : ct) {
|
|
s << quint8(qBlue(rgb));
|
|
s << quint8(qGreen(rgb));
|
|
s << quint8(qRed(rgb));
|
|
s << quint8(qAlpha(rgb));
|
|
}
|
|
|
|
if (s.status() != QDataStream::Ok) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (int y = 0, h = image.height(), w = image.width(); y < h; y++) {
|
|
auto ptr = reinterpret_cast<const char *>(image.constScanLine(y));
|
|
if (dev->write(ptr, w) != w) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TGAHandler::writeGrayscale(const QImage &image)
|
|
{
|
|
auto dev = device();
|
|
{ // write header
|
|
QDataStream s(dev);
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
auto iid = imageId(image);
|
|
s << quint8(iid.size()); // ID Length
|
|
s << quint8(0); // Color Map Type
|
|
s << quint8(TGA_TYPE_GREY); // Image Type
|
|
s << quint16(0); // First Entry Index
|
|
s << quint16(0); // Color Map Length
|
|
s << quint8(0); // Color map Entry Size
|
|
s << quint16(0); // X-origin of Image
|
|
s << quint16(0); // Y-origin of Image
|
|
|
|
s << quint16(image.width()); // Image Width
|
|
s << quint16(image.height()); // Image Height
|
|
s << quint8(8); // Pixel Depth
|
|
s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor
|
|
|
|
if (!iid.isEmpty())
|
|
s.writeRawData(iid.data(), iid.size());
|
|
|
|
if (s.status() != QDataStream::Ok) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ScanLineConverter scl(QImage::Format_Grayscale8);
|
|
for (int y = 0, h = image.height(), w = image.width(); y < h; y++) {
|
|
auto ptr = reinterpret_cast<const char *>(scl.convertedScanLine(image, y));
|
|
if (dev->write(ptr, w) != w) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TGAHandler::writeRGB555(const QImage &image)
|
|
{
|
|
auto dev = device();
|
|
{ // write header
|
|
QDataStream s(dev);
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
auto iid = imageId(image);
|
|
for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
|
|
s << c;
|
|
}
|
|
s << quint16(image.width()); // width
|
|
s << quint16(image.height()); // height
|
|
s << quint8(16); // depth
|
|
s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT);
|
|
|
|
if (!iid.isEmpty())
|
|
s.writeRawData(iid.data(), iid.size());
|
|
|
|
if (s.status() != QDataStream::Ok) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ScanLineConverter scl(QImage::Format_RGB555);
|
|
QByteArray ba(image.width() * 2, char());
|
|
for (int y = 0, h = image.height(); y < h; y++) {
|
|
auto ptr = reinterpret_cast<const quint16 *>(scl.convertedScanLine(image, y));
|
|
for (int x = 0, w = image.width(); x < w; x++) {
|
|
auto color = *(ptr + x);
|
|
ba[x * 2] = char(color);
|
|
ba[x * 2 + 1] = char(color >> 8);
|
|
}
|
|
if (dev->write(ba.data(), ba.size()) != qint64(ba.size())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TGAHandler::writeRGBA(const QImage &image)
|
|
{
|
|
auto format = image.format();
|
|
const bool hasAlpha = image.hasAlphaChannel();
|
|
auto tcs = QColorSpace();
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
|
auto cs = image.colorSpace();
|
|
if (cs.isValid() && cs.colorModel() == QColorSpace::ColorModel::Cmyk && image.format() == QImage::Format_CMYK8888) {
|
|
format = QImage::Format_RGB32;
|
|
tcs = QColorSpace(QColorSpace::SRgb);
|
|
} else if (hasAlpha && image.format() != QImage::Format_ARGB32) {
|
|
#else
|
|
if (hasAlpha && image.format() != QImage::Format_ARGB32) {
|
|
#endif
|
|
format = QImage::Format_ARGB32;
|
|
} else if (!hasAlpha && image.format() != QImage::Format_RGB32) {
|
|
format = QImage::Format_RGB32;
|
|
}
|
|
|
|
auto dev = device();
|
|
{ // write header
|
|
QDataStream s(dev);
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
const quint8 originTopLeft = TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT; // 0x20
|
|
const quint8 alphaChannel8Bits = 0x08;
|
|
|
|
auto iid = imageId(image);
|
|
for (char c : {int(iid.size()), 0, int(TGA_TYPE_RGB), 0, 0, 0, 0, 0, 0, 0, 0, 0}) {
|
|
s << c;
|
|
}
|
|
s << quint16(image.width()); // width
|
|
s << quint16(image.height()); // height
|
|
s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
|
|
s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8)
|
|
|
|
if (!iid.isEmpty())
|
|
s.writeRawData(iid.data(), iid.size());
|
|
|
|
if (s.status() != QDataStream::Ok) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ScanLineConverter scl(format);
|
|
if (tcs.isValid()) {
|
|
scl.setTargetColorSpace(tcs);
|
|
}
|
|
auto mul = hasAlpha ? 4 : 3;
|
|
QByteArray ba(image.width() * mul, char());
|
|
for (int y = 0, h = image.height(); y < h; y++) {
|
|
auto ptr = reinterpret_cast<const QRgb *>(scl.convertedScanLine(image, y));
|
|
for (int x = 0, w = image.width(); x < w; x++) {
|
|
auto color = *(ptr + x);
|
|
auto xmul = x * mul;
|
|
ba[xmul] = char(qBlue(color));
|
|
ba[xmul + 1] = char(qGreen(color));
|
|
ba[xmul + 2] = char(qRed(color));
|
|
if (hasAlpha) {
|
|
ba[xmul + 3] = char(qAlpha(color));
|
|
}
|
|
}
|
|
if (dev->write(ba.data(), ba.size()) != qint64(ba.size())) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TGAHandler::writeMetadata(const QImage &image)
|
|
{
|
|
if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V1()) {
|
|
return true; // TGA V1 does not have these data
|
|
}
|
|
|
|
auto dev = device();
|
|
if (dev == nullptr) {
|
|
return false;
|
|
}
|
|
if (dev->isSequential()) {
|
|
qCInfo(LOG_TGAPLUGIN) << "writeMetadata: unable to save metadata on a sequential device";
|
|
return true;
|
|
}
|
|
|
|
QDataStream s(dev);
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
// TGA 2.0 footer
|
|
TgaFooter foot;
|
|
|
|
// 32-bit overflow check (rough check)
|
|
// I need at least 495 (extension) + 26 (footer) bytes -> 1024 bytes.
|
|
// for the development area I roughly estimate 4096 KiB (profile, exif and xmp) they should always be less.
|
|
auto reqBytes = qint64(d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E() ? 4096 * 1024 : 1024);
|
|
if (dev->pos() > std::numeric_limits<quint32>::max() - reqBytes) {
|
|
qCInfo(LOG_TGAPLUGIN) << "writeMetadata: there is no enough space for metadata";
|
|
return true;
|
|
}
|
|
|
|
// TGA 2.0 developer area
|
|
TgaDeveloperDirectory dir;
|
|
if (d->m_subType == TGAHandlerPrivate::subTypeTGA_V2E()) {
|
|
auto exif = MicroExif::fromImage(image);
|
|
if (!exif.isEmpty()) {
|
|
auto ba = QByteArray("eXif").append(exif.toByteArray(s.byteOrder()));
|
|
TgaDeveloperDirectory::Field f;
|
|
f.tagId = TGA_EXIF_TAGID;
|
|
f.offset = dev->pos();
|
|
f.size = ba.size();
|
|
if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
|
|
return false;
|
|
}
|
|
dir.fields << f;
|
|
}
|
|
auto icc = image.colorSpace().iccProfile();
|
|
if (!icc.isEmpty()) {
|
|
auto ba = QByteArray("iCCP").append(icc);
|
|
TgaDeveloperDirectory::Field f;
|
|
f.tagId = TGA_ICCP_TAGID;
|
|
f.offset = dev->pos();
|
|
f.size = ba.size();
|
|
if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
|
|
return false;
|
|
}
|
|
dir.fields << f;
|
|
}
|
|
auto xmp = image.text(QStringLiteral(META_KEY_XMP_ADOBE)).trimmed();
|
|
if (!xmp.isEmpty()) {
|
|
auto ba = QByteArray("xMPP").append(xmp.toUtf8());
|
|
TgaDeveloperDirectory::Field f;
|
|
f.tagId = TGA_XMPP_TAGID;
|
|
f.offset = dev->pos();
|
|
f.size = ba.size();
|
|
if (s.writeRawData(ba.data(), ba.size()) != ba.size()) {
|
|
return false;
|
|
}
|
|
dir.fields << f;
|
|
}
|
|
}
|
|
|
|
// TGA 2.0 extension area
|
|
TgaExtension ext;
|
|
ext.setDateTime(QDateTime::currentDateTimeUtc());
|
|
if (image.hasAlphaChannel()) {
|
|
ext.attributesType = TgaExtension::Alpha;
|
|
}
|
|
auto keys = image.textKeys();
|
|
for (auto &&key : keys) {
|
|
if (!key.compare(QStringLiteral(META_KEY_AUTHOR), Qt::CaseInsensitive)) {
|
|
ext.setAuthor(image.text(key));
|
|
continue;
|
|
}
|
|
if (!key.compare(QStringLiteral(META_KEY_COMMENT), Qt::CaseInsensitive)) {
|
|
ext.setComment(image.text(key));
|
|
continue;
|
|
}
|
|
if (!key.compare(QStringLiteral(META_KEY_DESCRIPTION), Qt::CaseInsensitive)) {
|
|
if (ext.comment().isEmpty())
|
|
ext.setComment(image.text(key));
|
|
continue;
|
|
}
|
|
if (!key.compare(QStringLiteral(META_KEY_SOFTWARE), Qt::CaseInsensitive)) {
|
|
ext.setSoftware(image.text(key));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// write developer area
|
|
if (!dir.isEmpty()) {
|
|
foot.developerOffset = dev->pos();
|
|
s << dir;
|
|
}
|
|
|
|
// write extension area (date time is always set)
|
|
foot.extensionOffset = dev->pos();
|
|
s << ext;
|
|
s << foot;
|
|
|
|
return s.status() == QDataStream::Ok;
|
|
}
|
|
|
|
bool TGAHandler::readMetadata(QImage &image)
|
|
{
|
|
auto dev = device();
|
|
if (dev == nullptr) {
|
|
return false;
|
|
}
|
|
if (dev->isSequential()) {
|
|
qCInfo(LOG_TGAPLUGIN) << "readMetadata: unable to load metadata on a sequential device";
|
|
return true;
|
|
}
|
|
|
|
// read TGA footer
|
|
if (!dev->seek(dev->size() - 26)) {
|
|
return false;
|
|
}
|
|
|
|
QDataStream s(dev);
|
|
s.setByteOrder(QDataStream::LittleEndian);
|
|
|
|
TgaFooter foot;
|
|
s >> foot;
|
|
if (s.status() != QDataStream::Ok) {
|
|
return false;
|
|
}
|
|
if (!foot.isValid()) {
|
|
return true; // not a TGA 2.0 -> no metadata are present
|
|
}
|
|
|
|
if (foot.extensionOffset > 0) {
|
|
// read the extension area
|
|
if (!dev->seek(foot.extensionOffset)) {
|
|
return false;
|
|
}
|
|
|
|
TgaExtension ext;
|
|
s >> ext;
|
|
if (s.status() != QDataStream::Ok || !ext.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
auto dt = ext.dateTime();
|
|
if (dt.isValid()) {
|
|
image.setText(QStringLiteral(META_KEY_MODIFICATIONDATE), dt.toString(Qt::ISODate));
|
|
}
|
|
auto au = ext.author();
|
|
if (!au.isEmpty()) {
|
|
image.setText(QStringLiteral(META_KEY_AUTHOR), au);
|
|
}
|
|
auto cm = ext.comment();
|
|
if (!cm.isEmpty()) {
|
|
image.setText(QStringLiteral(META_KEY_COMMENT), cm);
|
|
}
|
|
auto sw = ext.software();
|
|
if (!sw.isEmpty()) {
|
|
image.setText(QStringLiteral(META_KEY_SOFTWARE), sw);
|
|
}
|
|
}
|
|
|
|
if (foot.developerOffset > 0) {
|
|
// read developer area
|
|
if (!dev->seek(foot.developerOffset)) {
|
|
return false;
|
|
}
|
|
|
|
TgaDeveloperDirectory dir;
|
|
s >> dir;
|
|
if (s.status() != QDataStream::Ok) {
|
|
return false;
|
|
}
|
|
|
|
for (auto &&f : dir.fields) {
|
|
if (!dev->seek(f.offset)) {
|
|
return false;
|
|
}
|
|
if (f.tagId == TGA_EXIF_TAGID) {
|
|
auto ba = dev->read(f.size);
|
|
if (ba.startsWith(QByteArray("eXif"))) {
|
|
auto exif = MicroExif::fromByteArray(ba.mid(4));
|
|
exif.updateImageMetadata(image, true);
|
|
exif.updateImageResolution(image);
|
|
}
|
|
continue;
|
|
}
|
|
if (f.tagId == TGA_ICCP_TAGID) {
|
|
auto ba = dev->read(f.size);
|
|
if (ba.startsWith(QByteArray("iCCP"))) {
|
|
image.setColorSpace(QColorSpace::fromIccProfile(ba.mid(4)));
|
|
}
|
|
continue;
|
|
}
|
|
if (f.tagId == TGA_XMPP_TAGID) {
|
|
auto ba = dev->read(f.size);
|
|
if (ba.startsWith(QByteArray("xMPP"))) {
|
|
image.setText(QStringLiteral(META_KEY_XMP_ADOBE), QString::fromUtf8(ba.mid(4)));
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
return s.status() == QDataStream::Ok;
|
|
}
|
|
|
|
bool TGAHandler::supportsOption(ImageOption option) const
|
|
{
|
|
if (option == QImageIOHandler::Size) {
|
|
return true;
|
|
}
|
|
if (option == QImageIOHandler::ImageFormat) {
|
|
return true;
|
|
}
|
|
if (option == QImageIOHandler::SubType) {
|
|
return true;
|
|
}
|
|
if (option == QImageIOHandler::SupportedSubTypes) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void TGAHandler::setOption(ImageOption option, const QVariant &value)
|
|
{
|
|
if (option == QImageIOHandler::SubType) {
|
|
auto subType = value.toByteArray();
|
|
auto list = TGAHandler::option(QImageIOHandler::SupportedSubTypes).value<QList<QByteArray>>();
|
|
if (list.contains(subType)) {
|
|
d->m_subType = subType;
|
|
} else {
|
|
d->m_subType = TGAHandlerPrivate::subTypeTGA_V2S();
|
|
}
|
|
}
|
|
}
|
|
|
|
QVariant TGAHandler::option(ImageOption option) const
|
|
{
|
|
if (!supportsOption(option)) {
|
|
return {};
|
|
}
|
|
|
|
if (option == QImageIOHandler::SupportedSubTypes) {
|
|
return QVariant::fromValue(QList<QByteArray>()
|
|
<< TGAHandlerPrivate::subTypeTGA_V1() << TGAHandlerPrivate::subTypeTGA_V2S() << TGAHandlerPrivate::subTypeTGA_V2E());
|
|
}
|
|
|
|
if (option == QImageIOHandler::SubType) {
|
|
return QVariant::fromValue(d->m_subType);
|
|
}
|
|
|
|
auto &&header = d->m_header;
|
|
if (!IsSupported(header)) {
|
|
if (auto dev = device())
|
|
if (!peekHeader(dev, header) && IsSupported(header))
|
|
return {};
|
|
if (!IsSupported(header)) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
if (option == QImageIOHandler::Size) {
|
|
return QVariant::fromValue(QSize(header.width, header.height));
|
|
}
|
|
|
|
if (option == QImageIOHandler::ImageFormat) {
|
|
return QVariant::fromValue(imageFormat(header));
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
bool TGAHandler::canRead(QIODevice *device)
|
|
{
|
|
if (!device) {
|
|
qCWarning(LOG_TGAPLUGIN) << "TGAHandler::canRead() called with no device";
|
|
return false;
|
|
}
|
|
|
|
TgaHeader tga;
|
|
if (!peekHeader(device, tga)) {
|
|
qCWarning(LOG_TGAPLUGIN) << "TGAHandler::canRead() error while reading the header";
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#include "moc_tga_p.cpp"
|