Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
0a9f9fe106 | |||
21b3b890ec | |||
aef4bd7e1c | |||
ea1983a7d1 | |||
775b53f1f6 | |||
02cf1502c0 | |||
f6c718a789 | |||
4f2f2425d3 | |||
e6357c22f7 | |||
094177a01c |
@ -1,11 +1,11 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
set(KF_VERSION "6.15.0") # handled by release scripts
|
set(KF_VERSION "6.17.0") # handled by release scripts
|
||||||
set(KF_DEP_VERSION "6.15.0") # handled by release scripts
|
set(KF_DEP_VERSION "6.16.0") # handled by release scripts
|
||||||
project(KImageFormats VERSION ${KF_VERSION})
|
project(KImageFormats VERSION ${KF_VERSION})
|
||||||
|
|
||||||
include(FeatureSummary)
|
include(FeatureSummary)
|
||||||
find_package(ECM 6.15.0 NO_MODULE)
|
find_package(ECM 6.16.0 NO_MODULE)
|
||||||
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
|
set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules")
|
||||||
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES)
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ The following image formats have read-only support:
|
|||||||
- Animated Windows cursors (ani)
|
- Animated Windows cursors (ani)
|
||||||
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
|
- Camera RAW images (arw, cr2, cr3, dcs, dng, ...)
|
||||||
- Gimp (xcf)
|
- Gimp (xcf)
|
||||||
|
- Interchange Format Files (IFF)
|
||||||
- Krita (kra)
|
- Krita (kra)
|
||||||
- OpenRaster (ora)
|
- OpenRaster (ora)
|
||||||
- Pixar raster (pxr)
|
- Pixar raster (pxr)
|
||||||
@ -222,6 +223,7 @@ plugin ('n/a' means no limit, i.e. the limit depends on the format encoding).
|
|||||||
- EPS: n/a
|
- EPS: n/a
|
||||||
- HDR: n/a (large image)
|
- HDR: n/a (large image)
|
||||||
- HEIF: n/a
|
- HEIF: n/a
|
||||||
|
- IFF: 65,535 x 65,535 pixels
|
||||||
- JP2: 300,000 x 300,000 pixels, in any case no larger than 2 gigapixels
|
- JP2: 300,000 x 300,000 pixels, in any case no larger than 2 gigapixels
|
||||||
- JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels
|
- JXL: 262,144 x 262,144 pixels, in any case no larger than 256 megapixels
|
||||||
- JXR: n/a, in any case no larger than 4 GB
|
- JXR: n/a, in any case no larger than 4 GB
|
||||||
|
@ -77,6 +77,7 @@ endmacro()
|
|||||||
# result against the data read from the corresponding png file
|
# result against the data read from the corresponding png file
|
||||||
kimageformats_read_tests(
|
kimageformats_read_tests(
|
||||||
hdr
|
hdr
|
||||||
|
iff
|
||||||
pcx
|
pcx
|
||||||
pfm
|
pfm
|
||||||
psd
|
psd
|
||||||
|
BIN
autotests/read/iff/blue_noise_rgba16.iff
Normal file
BIN
autotests/read/iff/blue_noise_rgba16.png
Normal file
After Width: | Height: | Size: 103 KiB |
BIN
autotests/read/iff/blue_noise_rgba8.iff
Normal file
BIN
autotests/read/iff/blue_noise_rgba8.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
autotests/read/iff/flag_ham6.iff
Normal file
BIN
autotests/read/iff/flag_ham6.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
autotests/read/iff/meta_rgba.iff
Normal file
31
autotests/read/iff/meta_rgba.iff.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"fileName" : "metadata.png",
|
||||||
|
"metadata" : [
|
||||||
|
{
|
||||||
|
"key" : "Author",
|
||||||
|
"value" : "KDE Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Copyright",
|
||||||
|
"value" : "@2025 KDE Project"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "CreationDate",
|
||||||
|
"value" : "2025-01-14T10:34:51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Description",
|
||||||
|
"value" : "TV broadcast test image."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key" : "Title",
|
||||||
|
"value" : "Test Card"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resolution" : {
|
||||||
|
"dotsPerMeterX" : 2835,
|
||||||
|
"dotsPerMeterY" : 2835
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
BIN
autotests/read/iff/meta_rgba.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
autotests/read/iff/ps_testcard_bitmap_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_bitmap_amiga.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
autotests/read/iff/ps_testcard_gray_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_gray_amiga.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
autotests/read/iff/ps_testcard_indexed_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_indexed_amiga.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
autotests/read/iff/ps_testcard_rgb16_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgb16_maya.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
autotests/read/iff/ps_testcard_rgb_amiga.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgb_amiga.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
autotests/read/iff/ps_testcard_rgb_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgb_maya.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
autotests/read/iff/ps_testcard_rgba16_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgba16_maya.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
autotests/read/iff/ps_testcard_rgba_maya.iff
Normal file
BIN
autotests/read/iff/ps_testcard_rgba_maya.png
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
autotests/read/iff/testcard_indexed2_amiga.iff
Normal file
BIN
autotests/read/iff/testcard_indexed2_amiga.png
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
autotests/read/iff/testcard_indexed3_amiga.iff
Normal file
BIN
autotests/read/iff/testcard_indexed3_amiga.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
autotests/read/iff/testcard_indexed4_amiga.iff
Normal file
BIN
autotests/read/iff/testcard_indexed4_amiga.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
autotests/read/iff/testcard_pbm.iff
Normal file
BIN
autotests/read/iff/testcard_pbm.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
autotests/read/iff/testcard_rlepbm.iff
Normal file
BIN
autotests/read/iff/testcard_rlepbm.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
autotests/read/tga/colormapped.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
autotests/read/tga/colormapped.tga
Normal file
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 10 KiB |
@ -84,6 +84,10 @@ endif()
|
|||||||
|
|
||||||
##################################
|
##################################
|
||||||
|
|
||||||
|
kimageformats_add_plugin(kimg_iff SOURCES iff.cpp chunks.cpp microexif.cpp)
|
||||||
|
|
||||||
|
##################################
|
||||||
|
|
||||||
if (LibJXL_FOUND AND LibJXLThreads_FOUND AND LibJXLCMS_FOUND)
|
if (LibJXL_FOUND AND LibJXLThreads_FOUND AND LibJXLCMS_FOUND)
|
||||||
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp microexif.cpp)
|
kimageformats_add_plugin(kimg_jxl SOURCES jxl.cpp microexif.cpp)
|
||||||
target_link_libraries(kimg_jxl PRIVATE PkgConfig::LibJXL PkgConfig::LibJXLThreads PkgConfig::LibJXLCMS)
|
target_link_libraries(kimg_jxl PRIVATE PkgConfig::LibJXL PkgConfig::LibJXLThreads PkgConfig::LibJXLCMS)
|
||||||
|
1756
src/imageformats/chunks.cpp
Normal file
1063
src/imageformats/chunks_p.h
Normal file
412
src/imageformats/iff.cpp
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the KDE project
|
||||||
|
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "chunks_p.h"
|
||||||
|
#include "iff_p.h"
|
||||||
|
#include "util_p.h"
|
||||||
|
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
#include <QPainter>
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtInfoMsg)
|
||||||
|
#else
|
||||||
|
Q_LOGGING_CATEGORY(LOG_IFFPLUGIN, "kf.imageformats.plugins.iff", QtWarningMsg)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class IFFHandlerPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IFFHandlerPrivate() {}
|
||||||
|
~IFFHandlerPrivate() {}
|
||||||
|
|
||||||
|
bool readStructure(QIODevice *d) {
|
||||||
|
if (d == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_chunks.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ok = false;
|
||||||
|
auto chunks = IFFChunk::fromDevice(d, &ok);
|
||||||
|
if (ok) {
|
||||||
|
_chunks = chunks;
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
static QList<const T*> searchForms(const IFFChunk::ChunkList &chunks, bool supportedOnly = true) {
|
||||||
|
QList<const T*> list;
|
||||||
|
auto cid = T::defaultChunkId();
|
||||||
|
auto forms = IFFChunk::search(cid, chunks);
|
||||||
|
for (auto &&form : forms) {
|
||||||
|
if (auto f = dynamic_cast<const T*>(form.data()))
|
||||||
|
if (!supportedOnly || f->isSupported())
|
||||||
|
list << f;
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
QList<const T*> searchForms(bool supportedOnly = true) {
|
||||||
|
return searchForms<T>(_chunks, supportedOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFChunk::ChunkList _chunks;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
IFFHandler::IFFHandler()
|
||||||
|
: QImageIOHandler()
|
||||||
|
, d(new IFFHandlerPrivate)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IFFHandler::canRead() const
|
||||||
|
{
|
||||||
|
if (canRead(device())) {
|
||||||
|
setFormat("iff");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IFFHandler::canRead(QIODevice *device)
|
||||||
|
{
|
||||||
|
if (!device) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() called with no device";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device->isSequential()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ok = false;
|
||||||
|
auto pos = device->pos();
|
||||||
|
auto chunks = IFFChunk::fromDevice(device, &ok);
|
||||||
|
if (!device->seek(pos)) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::canRead() unable to reset device position";
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
auto forms = IFFHandlerPrivate::searchForms<FORMChunk>(chunks, true);
|
||||||
|
auto for4s = IFFHandlerPrivate::searchForms<FOR4Chunk>(chunks, true);
|
||||||
|
ok = !forms.isEmpty() || !for4s.isEmpty();
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addMetadata(QImage& img, const IFFChunk *form)
|
||||||
|
{
|
||||||
|
// standard IFF metadata
|
||||||
|
auto annos = IFFChunk::searchT<ANNOChunk>(form);
|
||||||
|
if (!annos.isEmpty()) {
|
||||||
|
auto anno = annos.first()->value();
|
||||||
|
if (!anno.isEmpty()) {
|
||||||
|
img.setText(QStringLiteral(META_KEY_DESCRIPTION), anno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto auths = IFFChunk::searchT<AUTHChunk>(form);
|
||||||
|
if (!auths.isEmpty()) {
|
||||||
|
auto auth = auths.first()->value();
|
||||||
|
if (!auth.isEmpty()) {
|
||||||
|
img.setText(QStringLiteral(META_KEY_AUTHOR), auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto dates = IFFChunk::searchT<DATEChunk>(form);
|
||||||
|
if (!dates.isEmpty()) {
|
||||||
|
auto dt = dates.first()->value();
|
||||||
|
if (dt.isValid()) {
|
||||||
|
img.setText(QStringLiteral(META_KEY_CREATIONDATE), dt.toString(Qt::ISODate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto copys = IFFChunk::searchT<COPYChunk>(form);
|
||||||
|
if (!copys.isEmpty()) {
|
||||||
|
auto cp = copys.first()->value();
|
||||||
|
if (!cp.isEmpty()) {
|
||||||
|
img.setText(QStringLiteral(META_KEY_COPYRIGHT), cp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto names = IFFChunk::searchT<NAMEChunk>(form);
|
||||||
|
if (!names.isEmpty()) {
|
||||||
|
auto name = names.first()->value();
|
||||||
|
if (!name.isEmpty()) {
|
||||||
|
img.setText(QStringLiteral(META_KEY_TITLE), name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// software info
|
||||||
|
auto vers = IFFChunk::searchT<VERSChunk>(form);
|
||||||
|
if (!vers.isEmpty()) {
|
||||||
|
auto ver = vers.first()->value();
|
||||||
|
if (!vers.isEmpty()) {
|
||||||
|
img.setText(QStringLiteral(META_KEY_SOFTWARE), ver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SView5 metadata
|
||||||
|
auto exifs = IFFChunk::searchT<EXIFChunk>(form);
|
||||||
|
if (!exifs.isEmpty()) {
|
||||||
|
auto exif = exifs.first()->value();
|
||||||
|
exif.updateImageMetadata(img, false);
|
||||||
|
exif.updateImageResolution(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto xmp0s = IFFChunk::searchT<XMP0Chunk>(form);
|
||||||
|
if (!xmp0s.isEmpty()) {
|
||||||
|
auto xmp = xmp0s.first()->value();
|
||||||
|
if (!xmp.isEmpty()) {
|
||||||
|
img.setText(QStringLiteral(META_KEY_XMP_ADOBE), xmp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto iccps = IFFChunk::searchT<ICCPChunk>(form);
|
||||||
|
if (!iccps.isEmpty()) {
|
||||||
|
auto cs = iccps.first()->value();
|
||||||
|
if (cs.isValid()) {
|
||||||
|
img.setColorSpace(cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolution -> leave after set of EXIF chunk
|
||||||
|
auto dpis = IFFChunk::searchT<DPIChunk>(form);
|
||||||
|
if (!dpis.isEmpty()) {
|
||||||
|
auto &&dpi = dpis.first();
|
||||||
|
if (dpi->isValid()) {
|
||||||
|
img.setDotsPerMeterX(dpi->dotsPerMeterX());
|
||||||
|
img.setDotsPerMeterY(dpi->dotsPerMeterY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IFFHandler::readStandardImage(QImage *image)
|
||||||
|
{
|
||||||
|
auto forms = d->searchForms<FORMChunk>();
|
||||||
|
if (forms.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto &&form = forms.first();
|
||||||
|
|
||||||
|
// show the first one (I don't have a sample with many images)
|
||||||
|
auto headers = IFFChunk::searchT<BMHDChunk>(form);
|
||||||
|
if (headers.isEmpty()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() no supported image found";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the image
|
||||||
|
auto &&header = headers.first();
|
||||||
|
auto img = imageAlloc(header->size(), form->format());
|
||||||
|
if (img.isNull()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while allocating the image";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set color table
|
||||||
|
auto cmaps = IFFChunk::searchT<CMAPChunk>(form);
|
||||||
|
if (img.format() == QImage::Format_Indexed8) {
|
||||||
|
if (!cmaps.isEmpty())
|
||||||
|
if (auto &&cmap = cmaps.first())
|
||||||
|
img.setColorTable(cmap->palette());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bodies = IFFChunk::searchT<BODYChunk>(form);
|
||||||
|
if (bodies.isEmpty()) {
|
||||||
|
img.fill(0);
|
||||||
|
} else {
|
||||||
|
const CAMGChunk *camg = nullptr;
|
||||||
|
auto camgs = IFFChunk::searchT<CAMGChunk>(form);
|
||||||
|
if (!camgs.isEmpty()) {
|
||||||
|
camg = camgs.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
const CMAPChunk *cmap = nullptr;
|
||||||
|
if (!cmaps.isEmpty())
|
||||||
|
cmap = cmaps.first();
|
||||||
|
|
||||||
|
auto &&body = bodies.first();
|
||||||
|
if (!body->resetStrideRead(device())) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image data";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto isPbm = form->formType() == PBM__FORM_TYPE;
|
||||||
|
for (auto y = 0, h = img.height(); y < h; ++y) {
|
||||||
|
auto line = reinterpret_cast<char*>(img.scanLine(y));
|
||||||
|
auto ba = body->strideRead(device(), header, camg, cmap, isPbm);
|
||||||
|
if (ba.isEmpty()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readStandardImage() error while reading image scanline";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(line, ba.constData(), std::min(img.bytesPerLine(), ba.size()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set metadata (including image resolution)
|
||||||
|
addMetadata(img, form);
|
||||||
|
|
||||||
|
*image = img;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IFFHandler::readMayaImage(QImage *image)
|
||||||
|
{
|
||||||
|
auto forms = d->searchForms<FOR4Chunk>();
|
||||||
|
if (forms.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto &&form = forms.first();
|
||||||
|
|
||||||
|
// show the first one (I don't have a sample with many images)
|
||||||
|
auto headers = IFFChunk::searchT<TBHDChunk>(form);
|
||||||
|
if (headers.isEmpty()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() no supported image found";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the image
|
||||||
|
auto &&header = headers.first();
|
||||||
|
auto img = imageAlloc(header->size(), form->format());
|
||||||
|
if (img.isNull()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while allocating the image";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &&tiles = IFFChunk::searchT<RGBAChunk>(form);
|
||||||
|
if ((tiles.size() & 0xFFFF) != header->tiles()) { // Photoshop, on large images saves more than 65535 tiles
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() tile number mismatch: found" << tiles.size() << "while expected" << header->tiles();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (auto &&tile : tiles) {
|
||||||
|
auto tp = tile->pos();
|
||||||
|
auto ts = tile->size();
|
||||||
|
if (tp.x() < 0 || tp.x() + ts.width() > img.width()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tp.y() < 0 || tp.y() + ts.height() > img.height()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() wrong tile position or size";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// For future releases: it might be a good idea not to use a QPainter
|
||||||
|
auto ti = tile->tile(device(), header);
|
||||||
|
if (ti.isNull()) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::readMayaImage() error while decoding the tile";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QPainter painter(&img);
|
||||||
|
painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||||
|
painter.drawImage(tp, ti);
|
||||||
|
}
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
|
||||||
|
img.mirror(false, true);
|
||||||
|
#else
|
||||||
|
img.flip(Qt::Orientation::Vertical);
|
||||||
|
#endif
|
||||||
|
addMetadata(img, form);
|
||||||
|
|
||||||
|
*image = img;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IFFHandler::read(QImage *image)
|
||||||
|
{
|
||||||
|
if (!d->readStructure(device())) {
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() invalid IFF structure";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readStandardImage(image)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readMayaImage(image)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCWarning(LOG_IFFPLUGIN) << "IFFHandler::read() no supported image found";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IFFHandler::supportsOption(ImageOption option) const
|
||||||
|
{
|
||||||
|
if (option == QImageIOHandler::Size) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (option == QImageIOHandler::ImageFormat) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant IFFHandler::option(ImageOption option) const
|
||||||
|
{
|
||||||
|
QVariant v;
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::Size) {
|
||||||
|
if (d->readStructure(device())) {
|
||||||
|
auto forms = d->searchForms<FORMChunk>();
|
||||||
|
if (!forms.isEmpty())
|
||||||
|
if (auto &&form = forms.first())
|
||||||
|
v = QVariant::fromValue(form->size());
|
||||||
|
|
||||||
|
auto for4s = d->searchForms<FOR4Chunk>();
|
||||||
|
if (!for4s.isEmpty())
|
||||||
|
if (auto &&form = for4s.first())
|
||||||
|
v = QVariant::fromValue(form->size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option == QImageIOHandler::ImageFormat) {
|
||||||
|
if (d->readStructure(device())) {
|
||||||
|
auto forms = d->searchForms<FORMChunk>();
|
||||||
|
if (!forms.isEmpty())
|
||||||
|
if (auto &&form = forms.first())
|
||||||
|
v = QVariant::fromValue(form->format());
|
||||||
|
|
||||||
|
auto for4s = d->searchForms<FOR4Chunk>();
|
||||||
|
if (!for4s.isEmpty())
|
||||||
|
if (auto &&form = for4s.first())
|
||||||
|
v = QVariant::fromValue(form->format());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImageIOPlugin::Capabilities IFFPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
||||||
|
{
|
||||||
|
if (format == "iff") {
|
||||||
|
return Capabilities(CanRead);
|
||||||
|
}
|
||||||
|
if (!format.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!device->isOpen()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Capabilities cap;
|
||||||
|
if (device->isReadable() && IFFHandler::canRead(device)) {
|
||||||
|
cap |= CanRead;
|
||||||
|
}
|
||||||
|
return cap;
|
||||||
|
}
|
||||||
|
|
||||||
|
QImageIOHandler *IFFPlugin::create(QIODevice *device, const QByteArray &format) const
|
||||||
|
{
|
||||||
|
QImageIOHandler *handler = new IFFHandler;
|
||||||
|
handler->setDevice(device);
|
||||||
|
handler->setFormat(format);
|
||||||
|
return handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "moc_iff_p.cpp"
|
4
src/imageformats/iff.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Keys": [ "iff" ],
|
||||||
|
"MimeTypes": [ "application/x-iff" ]
|
||||||
|
}
|
47
src/imageformats/iff_p.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
This file is part of the KDE project
|
||||||
|
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef KIMG_IFF_P_H
|
||||||
|
#define KIMG_IFF_P_H
|
||||||
|
|
||||||
|
#include <QImageIOPlugin>
|
||||||
|
#include <QScopedPointer>
|
||||||
|
|
||||||
|
class IFFHandlerPrivate;
|
||||||
|
class IFFHandler : public QImageIOHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
IFFHandler();
|
||||||
|
|
||||||
|
bool canRead() const override;
|
||||||
|
bool read(QImage *image) override;
|
||||||
|
|
||||||
|
bool supportsOption(QImageIOHandler::ImageOption option) const override;
|
||||||
|
QVariant option(QImageIOHandler::ImageOption option) const override;
|
||||||
|
|
||||||
|
static bool canRead(QIODevice *device);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool readStandardImage(QImage *image);
|
||||||
|
|
||||||
|
bool readMayaImage(QImage *image);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QScopedPointer<IFFHandlerPrivate> d;
|
||||||
|
};
|
||||||
|
|
||||||
|
class IFFPlugin : public QImageIOPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface" FILE "iff.json")
|
||||||
|
|
||||||
|
public:
|
||||||
|
Capabilities capabilities(QIODevice *device, const QByteArray &format) const override;
|
||||||
|
QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const override;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // KIMG_IFF_P_H
|
@ -1072,7 +1072,7 @@ QByteArray MicroExif::toByteArray(const QDataStream::ByteOrder &byteOrder, const
|
|||||||
QByteArray ba;
|
QByteArray ba;
|
||||||
{
|
{
|
||||||
QBuffer buf(&ba);
|
QBuffer buf(&ba);
|
||||||
if (!write(&buf, byteOrder))
|
if (!write(&buf, byteOrder, version))
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return ba;
|
return ba;
|
||||||
|
111
src/imageformats/packbits_p.h
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
Packbits compression used on many legacy formats (IFF, PSD, TIFF).
|
||||||
|
|
||||||
|
SPDX-FileCopyrightText: 2025 Mirco Miranda <mircomir@outlook.com>
|
||||||
|
|
||||||
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||||
|
*/
|
||||||
|
#ifndef PACKBITS_P_H
|
||||||
|
#define PACKBITS_P_H
|
||||||
|
|
||||||
|
#include <QIODevice>
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief packbitsDecompress
|
||||||
|
* Fast PackBits decompression.
|
||||||
|
* \param input The compressed input buffer.
|
||||||
|
* \param ilen The input buffer size.
|
||||||
|
* \param output The uncompressed target buffer.
|
||||||
|
* \param olen The target buffer size.
|
||||||
|
* \param allowN128 If true, -128 is a valid run length size (false for PSD / TIFF, true for IFF) .
|
||||||
|
* \return The number of valid bytes in the target buffer.
|
||||||
|
*/
|
||||||
|
inline qint64 packbitsDecompress(const char *input, qint64 ilen, char *output, qint64 olen, bool allowN128 = false)
|
||||||
|
{
|
||||||
|
qint64 j = 0;
|
||||||
|
for (qint64 ip = 0, rr = 0, available = olen; j < olen && ip < ilen; available = olen - j) {
|
||||||
|
signed char n = static_cast<signed char>(input[ip++]);
|
||||||
|
if (n == -128 && !allowN128)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (n >= 0) {
|
||||||
|
rr = qint64(n) + 1;
|
||||||
|
if (available < rr) {
|
||||||
|
--ip;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip + rr > ilen)
|
||||||
|
return -1;
|
||||||
|
memcpy(output + j, input + ip, size_t(rr));
|
||||||
|
ip += rr;
|
||||||
|
} else if (ip < ilen) {
|
||||||
|
rr = qint64(1-n);
|
||||||
|
if (available < rr) {
|
||||||
|
--ip;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
memset(output + j, input[ip++], size_t(rr));
|
||||||
|
}
|
||||||
|
|
||||||
|
j += rr;
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief packbitsDecompress
|
||||||
|
* PackBits decompression.
|
||||||
|
* \param input The input device.
|
||||||
|
* \param output The uncompressed target buffer.
|
||||||
|
* \param olen The target buffer size.
|
||||||
|
* \param allowN128 If true, -128 is a valid run length size (false for PSD / TIFF, true for IFF) .
|
||||||
|
* \return The number of valid bytes in the target buffer.
|
||||||
|
*/
|
||||||
|
inline qint64 packbitsDecompress(QIODevice *input, char *output, qint64 olen, bool allowN128 = false)
|
||||||
|
{
|
||||||
|
qint64 j = 0;
|
||||||
|
for (qint64 rr = 0, available = olen; j < olen; available = olen - j) {
|
||||||
|
char n;
|
||||||
|
|
||||||
|
// check the output buffer space for the next run
|
||||||
|
if (available < 129) {
|
||||||
|
if (input->peek(&n, 1) != 1) { // end of data (or error)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (static_cast<signed char>(n) != -128 || allowN128)
|
||||||
|
if ((static_cast<signed char>(n) >= 0 ? qint64(n) + 1 : qint64(1 - n)) > available)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// decompress
|
||||||
|
if (input->read(&n, 1) != 1) { // end of data (or error)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<signed char>(n) == -128 && !allowN128) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (static_cast<signed char>(n) >= 0) {
|
||||||
|
rr = input->read(output + j, qint64(n) + 1);
|
||||||
|
if (rr == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
char b;
|
||||||
|
if (input->read(&b, 1) != 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rr = qint64(1 - static_cast<signed char>(n));
|
||||||
|
std::memset(output + j, b, size_t(rr));
|
||||||
|
}
|
||||||
|
|
||||||
|
j += rr;
|
||||||
|
}
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endif // PACKBITS_P_H
|
@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
#include "fastmath_p.h"
|
#include "fastmath_p.h"
|
||||||
#include "microexif_p.h"
|
#include "microexif_p.h"
|
||||||
|
#include "packbits_p.h"
|
||||||
#include "psd_p.h"
|
#include "psd_p.h"
|
||||||
#include "scanlineconverter_p.h"
|
#include "scanlineconverter_p.h"
|
||||||
#include "util_p.h"
|
#include "util_p.h"
|
||||||
@ -712,48 +713,6 @@ static bool IsSupported(const PSDHeader &header)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* \brief decompress
|
|
||||||
* Fast PackBits decompression.
|
|
||||||
* \param input The compressed input buffer.
|
|
||||||
* \param ilen The input buffer size.
|
|
||||||
* \param output The uncompressed target buffer.
|
|
||||||
* \param olen The target buffer size.
|
|
||||||
* \return The number of valid bytes in the target buffer.
|
|
||||||
*/
|
|
||||||
qint64 decompress(const char *input, qint64 ilen, char *output, qint64 olen)
|
|
||||||
{
|
|
||||||
qint64 j = 0;
|
|
||||||
for (qint64 ip = 0, rr = 0, available = olen; j < olen && ip < ilen; available = olen - j) {
|
|
||||||
signed char n = static_cast<signed char>(input[ip++]);
|
|
||||||
if (n == -128)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (n >= 0) {
|
|
||||||
rr = qint64(n) + 1;
|
|
||||||
if (available < rr) {
|
|
||||||
--ip;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ip + rr > ilen)
|
|
||||||
return -1;
|
|
||||||
memcpy(output + j, input + ip, size_t(rr));
|
|
||||||
ip += rr;
|
|
||||||
} else if (ip < ilen) {
|
|
||||||
rr = qint64(1-n);
|
|
||||||
if (available < rr) {
|
|
||||||
--ip;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
memset(output + j, input[ip++], size_t(rr));
|
|
||||||
}
|
|
||||||
|
|
||||||
j += rr;
|
|
||||||
}
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief imageFormat
|
* \brief imageFormat
|
||||||
* \param header The PSD header.
|
* \param header The PSD header.
|
||||||
@ -1102,7 +1061,7 @@ bool readChannel(QByteArray &target, QDataStream &stream, quint32 compressedSize
|
|||||||
if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) {
|
if (stream.readRawData(tmp.data(), tmp.size()) != tmp.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (decompress(tmp.data(), tmp.size(), target.data(), target.size()) < 0) {
|
if (packbitsDecompress(tmp.data(), tmp.size(), target.data(), target.size()) < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (stream.readRawData(target.data(), target.size()) != target.size()) {
|
} else if (stream.readRawData(target.data(), target.size()) != target.size()) {
|
||||||
|
@ -99,7 +99,12 @@ static bool IsSupported(const TgaHeader &head)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
|
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) {
|
// 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;
|
||||||
|
}
|
||||||
|
// colormap_size == 16 would be ARRRRRGG GGGBBBBB but we don't support that.
|
||||||
|
if (head.colormap_size != 24 && head.colormap_size != 32) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,10 +119,11 @@ static bool IsSupported(const TgaHeader &head)
|
|||||||
if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
|
if (head.pixel_size != 8 && head.pixel_size != 16 && head.pixel_size != 24 && head.pixel_size != 32) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 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.
|
// 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_size != 0 || head.colormap_index != 0 || head.colormap_length != 0)) {
|
if (head.colormap_type == 0 && (head.colormap_index != 0 || head.colormap_length != 0)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +195,8 @@ static QImage::Format imageFormat(const TgaHeader &head)
|
|||||||
if (numAlphaBits == 8) {
|
if (numAlphaBits == 8) {
|
||||||
format = QImage::Format_ARGB32;
|
format = QImage::Format_ARGB32;
|
||||||
}
|
}
|
||||||
|
} else if (head.image_type == TGA_TYPE_INDEXED || head.image_type == TGA_TYPE_RLE_INDEXED) {
|
||||||
|
format = QImage::Format_Indexed8;
|
||||||
} else {
|
} else {
|
||||||
format = QImage::Format_RGB32;
|
format = QImage::Format_RGB32;
|
||||||
}
|
}
|
||||||
@ -232,21 +240,40 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read palette.
|
// Read palette.
|
||||||
static const int max_palette_size = 768;
|
|
||||||
char palette[max_palette_size];
|
|
||||||
if (info.pal) {
|
if (info.pal) {
|
||||||
// @todo Support palettes in other formats!
|
QList<QRgb> colorTable;
|
||||||
const int palette_size = 3 * tga.colormap_length;
|
#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0)
|
||||||
if (palette_size > max_palette_size) {
|
colorTable.resize(tga.colormap_length);
|
||||||
|
#else
|
||||||
|
colorTable.resizeForOverwrite(tga.colormap_length);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (tga.colormap_size == 32) { // BGRA.
|
||||||
|
char data[4];
|
||||||
|
for (QRgb &rgb : colorTable) {
|
||||||
|
const auto dataRead = s.readRawData(data, 4);
|
||||||
|
if (dataRead < 4) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// BGRA.
|
||||||
|
rgb = qRgba(data[2], data[1], data[0], data[3]);
|
||||||
|
}
|
||||||
|
} else if (tga.colormap_size == 24) { // BGR.
|
||||||
|
char data[3];
|
||||||
|
for (QRgb &rgb : colorTable) {
|
||||||
|
const auto dataRead = s.readRawData(data, 3);
|
||||||
|
if (dataRead < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// BGR.
|
||||||
|
rgb = qRgb(data[2], data[1], data[0]);
|
||||||
|
}
|
||||||
|
// TODO tga.colormap_size == 16 ARRRRRGG GGGBBBBB
|
||||||
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int dataRead = s.readRawData(palette, palette_size);
|
|
||||||
if (dataRead < 0) {
|
img.setColorTable(colorTable);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (dataRead < max_palette_size) {
|
|
||||||
memset(&palette[dataRead], 0, max_palette_size - dataRead);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate image.
|
// Allocate image.
|
||||||
@ -355,14 +382,19 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
|||||||
uchar *src = image;
|
uchar *src = image;
|
||||||
|
|
||||||
for (int y = y_start; y != y_end; y += y_step) {
|
for (int y = y_start; y != y_end; y += y_step) {
|
||||||
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
|
|
||||||
if (info.pal) {
|
if (info.pal) {
|
||||||
// Paletted.
|
// Paletted.
|
||||||
|
auto scanline = img.scanLine(y);
|
||||||
for (int x = 0; x < tga.width; x++) {
|
for (int x = 0; x < tga.width; x++) {
|
||||||
uchar idx = *src++;
|
uchar idx = *src++;
|
||||||
scanline[x] = qRgb(palette[3 * idx + 2], palette[3 * idx + 1], palette[3 * idx + 0]);
|
if (Q_UNLIKELY(idx >= tga.colormap_length)) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
scanline[x] = idx;
|
||||||
}
|
}
|
||||||
} else if (info.grey) {
|
} else if (info.grey) {
|
||||||
|
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
|
||||||
// Greyscale.
|
// Greyscale.
|
||||||
for (int x = 0; x < tga.width; x++) {
|
for (int x = 0; x < tga.width; x++) {
|
||||||
if (tga.pixel_size == 16) {
|
if (tga.pixel_size == 16) {
|
||||||
@ -375,6 +407,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
auto scanline = reinterpret_cast<QRgb *>(img.scanLine(y));
|
||||||
// True Color.
|
// True Color.
|
||||||
if (tga.pixel_size == 16) {
|
if (tga.pixel_size == 16) {
|
||||||
for (int x = 0; x < tga.width; x++) {
|
for (int x = 0; x < tga.width; x++) {
|
||||||
@ -401,7 +434,7 @@ static bool LoadTGA(QDataStream &s, const TgaHeader &tga, QImage &img)
|
|||||||
// Free image.
|
// Free image.
|
||||||
free(image);
|
free(image);
|
||||||
|
|
||||||
return true;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@ -469,6 +502,59 @@ bool TGAHandler::read(QImage *outImage)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool TGAHandler::write(const QImage &image)
|
bool TGAHandler::write(const QImage &image)
|
||||||
|
{
|
||||||
|
if (image.format() == QImage::Format_Indexed8)
|
||||||
|
return writeIndexed(image);
|
||||||
|
return writeRGBA(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TGAHandler::writeIndexed(const QImage &image)
|
||||||
|
{
|
||||||
|
QDataStream s(device());
|
||||||
|
s.setByteOrder(QDataStream::LittleEndian);
|
||||||
|
|
||||||
|
QImage img(image);
|
||||||
|
auto ct = img.colorTable();
|
||||||
|
|
||||||
|
s << quint8(0); // 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(img.width()); // Image Width
|
||||||
|
s << quint16(img.height()); // Image Height
|
||||||
|
s << quint8(8); // Pixe Depth
|
||||||
|
s << quint8(TGA_ORIGIN_UPPER + TGA_ORIGIN_LEFT); // Image Descriptor
|
||||||
|
|
||||||
|
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; y < img.height(); y++) {
|
||||||
|
auto ptr = img.constScanLine(y);
|
||||||
|
for (int x = 0; x < img.width(); x++) {
|
||||||
|
s << *(ptr + x);
|
||||||
|
}
|
||||||
|
if (s.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TGAHandler::writeRGBA(const QImage &image)
|
||||||
{
|
{
|
||||||
QDataStream s(device());
|
QDataStream s(device());
|
||||||
s.setByteOrder(QDataStream::LittleEndian);
|
s.setByteOrder(QDataStream::LittleEndian);
|
||||||
@ -504,6 +590,10 @@ bool TGAHandler::write(const QImage &image)
|
|||||||
s << quint8(hasAlpha ? 32 : 24); // depth (24 bit RGB + 8 bit alpha)
|
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)
|
s << quint8(hasAlpha ? originTopLeft + alphaChannel8Bits : originTopLeft); // top left image (0x20) + 8 bit alpha (0x8)
|
||||||
|
|
||||||
|
if (s.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
for (int y = 0; y < img.height(); y++) {
|
for (int y = 0; y < img.height(); y++) {
|
||||||
auto ptr = reinterpret_cast<const QRgb *>(img.constScanLine(y));
|
auto ptr = reinterpret_cast<const QRgb *>(img.constScanLine(y));
|
||||||
for (int x = 0; x < img.width(); x++) {
|
for (int x = 0; x < img.width(); x++) {
|
||||||
@ -515,6 +605,9 @@ bool TGAHandler::write(const QImage &image)
|
|||||||
s << quint8(qAlpha(color));
|
s << quint8(qAlpha(color));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (s.status() != QDataStream::Ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -27,6 +27,10 @@ public:
|
|||||||
static bool canRead(QIODevice *device);
|
static bool canRead(QIODevice *device);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool writeIndexed(const QImage &image);
|
||||||
|
|
||||||
|
bool writeRGBA(const QImage &image);
|
||||||
|
|
||||||
const QScopedPointer<TGAHandlerPrivate> d;
|
const QScopedPointer<TGAHandlerPrivate> d;
|
||||||
};
|
};
|
||||||
|
|
||||||
|