mirror of
https://invent.kde.org/frameworks/kimageformats.git
synced 2025-06-03 00:58:15 -04:00
360 lines
9.5 KiB
C++
360 lines
9.5 KiB
C++
/*
|
|
Photoshop File Format support for QImage.
|
|
|
|
SPDX-FileCopyrightText: 2003 Ignacio Castaño <castano@ludicon.com>
|
|
SPDX-FileCopyrightText: 2015 Alex Merry <alex.merry@kde.org>
|
|
|
|
SPDX-License-Identifier: LGPL-2.0-or-later
|
|
*/
|
|
|
|
/*
|
|
* This code is based on Thacher Ulrich PSD loading code released
|
|
* into the public domain. See: http://tulrich.com/geekstuff/
|
|
*/
|
|
|
|
/*
|
|
* Documentation on this file format is available at
|
|
* http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
|
|
*/
|
|
|
|
#include "psd_p.h"
|
|
|
|
#include "rle_p.h"
|
|
|
|
#include <QDataStream>
|
|
#include <QDebug>
|
|
#include <QImage>
|
|
|
|
typedef quint32 uint;
|
|
typedef quint16 ushort;
|
|
typedef quint8 uchar;
|
|
|
|
namespace // Private.
|
|
{
|
|
|
|
enum ColorMode {
|
|
CM_BITMAP = 0,
|
|
CM_GRAYSCALE = 1,
|
|
CM_INDEXED = 2,
|
|
CM_RGB = 3,
|
|
CM_CMYK = 4,
|
|
CM_MULTICHANNEL = 7,
|
|
CM_DUOTONE = 8,
|
|
CM_LABCOLOR = 9
|
|
};
|
|
|
|
struct PSDHeader {
|
|
uint signature;
|
|
ushort version;
|
|
uchar reserved[6];
|
|
ushort channel_count;
|
|
uint height;
|
|
uint width;
|
|
ushort depth;
|
|
ushort color_mode;
|
|
};
|
|
|
|
static QDataStream &operator>> (QDataStream &s, PSDHeader &header)
|
|
{
|
|
s >> header.signature;
|
|
s >> header.version;
|
|
for (int i = 0; i < 6; i++) {
|
|
s >> header.reserved[i];
|
|
}
|
|
s >> header.channel_count;
|
|
s >> header.height;
|
|
s >> header.width;
|
|
s >> header.depth;
|
|
s >> header.color_mode;
|
|
return s;
|
|
}
|
|
|
|
// Check that the header is a valid PSD.
|
|
static bool IsValid(const PSDHeader &header)
|
|
{
|
|
if (header.signature != 0x38425053) { // '8BPS'
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Check that the header is supported.
|
|
static bool IsSupported(const PSDHeader &header)
|
|
{
|
|
if (header.version != 1) {
|
|
return false;
|
|
}
|
|
if (header.channel_count > 16) {
|
|
return false;
|
|
}
|
|
if (header.depth != 8 && header.depth != 16) {
|
|
return false;
|
|
}
|
|
if (header.color_mode != CM_RGB) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void skip_section(QDataStream &s)
|
|
{
|
|
quint32 section_length;
|
|
// Skip mode data.
|
|
s >> section_length;
|
|
s.skipRawData(section_length);
|
|
}
|
|
|
|
template <class Trait>
|
|
static Trait readPixel(QDataStream &stream) {
|
|
Trait pixel;
|
|
stream >> pixel;
|
|
return pixel;
|
|
}
|
|
|
|
static QRgb updateRed(QRgb oldPixel, quint8 redPixel) {
|
|
return qRgba(redPixel, qGreen(oldPixel), qBlue(oldPixel), qAlpha(oldPixel));
|
|
}
|
|
static QRgb updateGreen(QRgb oldPixel, quint8 greenPixel) {
|
|
return qRgba(qRed(oldPixel), greenPixel, qBlue(oldPixel), qAlpha(oldPixel));
|
|
}
|
|
static QRgb updateBlue(QRgb oldPixel, quint8 bluePixel) {
|
|
return qRgba(qRed(oldPixel), qGreen(oldPixel), bluePixel, qAlpha(oldPixel));
|
|
}
|
|
static QRgb updateAlpha(QRgb oldPixel, quint8 alphaPixel) {
|
|
return qRgba(qRed(oldPixel), qGreen(oldPixel), qBlue(oldPixel), alphaPixel);
|
|
}
|
|
typedef QRgb(*channelUpdater)(QRgb,quint8);
|
|
|
|
// Load the PSD image.
|
|
static bool LoadPSD(QDataStream &stream, const PSDHeader &header, QImage &img)
|
|
{
|
|
// Mode data
|
|
skip_section(stream);
|
|
|
|
// Image resources
|
|
skip_section(stream);
|
|
|
|
// Reserved data
|
|
skip_section(stream);
|
|
|
|
// Find out if the data is compressed.
|
|
// Known values:
|
|
// 0: no compression
|
|
// 1: RLE compressed
|
|
quint16 compression;
|
|
stream >> compression;
|
|
|
|
if (compression > 1) {
|
|
qDebug() << "Unknown compression type";
|
|
return false;
|
|
}
|
|
|
|
quint32 channel_num = header.channel_count;
|
|
|
|
QImage::Format fmt = header.depth == 8 ? QImage::Format_RGB32
|
|
: QImage::Format_RGBX64;
|
|
// Clear the image.
|
|
if (channel_num >= 4) {
|
|
// Enable alpha.
|
|
fmt = header.depth == 8 ? QImage::Format_ARGB32
|
|
: QImage::Format_RGBA64;
|
|
|
|
// Ignore the other channels.
|
|
channel_num = 4;
|
|
}
|
|
|
|
img = QImage(header.width, header.height, fmt);
|
|
if (img.isNull()) {
|
|
qWarning() << "Failed to allocate image, invalid dimensions?" << QSize(header.width, header.height);
|
|
return false;
|
|
}
|
|
img.fill(qRgb(0,0,0));
|
|
|
|
const quint32 pixel_count = header.height * header.width;
|
|
const quint32 channel_size = pixel_count * header.depth / 8;
|
|
|
|
// Verify this, as this is used to write into the memory of the QImage
|
|
if (pixel_count > img.sizeInBytes() / (header.depth == 8 ? sizeof(QRgb) : sizeof(QRgba64))) {
|
|
qWarning() << "Invalid pixel count!" << pixel_count << "bytes available:" << img.sizeInBytes();
|
|
return false;
|
|
}
|
|
|
|
QRgb *image_data = reinterpret_cast<QRgb*>(img.bits());
|
|
|
|
if (!image_data) {
|
|
return false;
|
|
}
|
|
|
|
static const channelUpdater updaters[4] = {
|
|
updateRed,
|
|
updateGreen,
|
|
updateBlue,
|
|
updateAlpha
|
|
};
|
|
|
|
typedef QRgba64(*channelUpdater16)(QRgba64, quint16);
|
|
static const channelUpdater16 updaters64[4] = {
|
|
[](QRgba64 oldPixel, quint16 redPixel) {return qRgba64((oldPixel & ~(0xFFFFull << 0)) | (quint64( redPixel) << 0));},
|
|
[](QRgba64 oldPixel, quint16 greenPixel){return qRgba64((oldPixel & ~(0xFFFFull << 16)) | (quint64(greenPixel) << 16));},
|
|
[](QRgba64 oldPixel, quint16 bluePixel) {return qRgba64((oldPixel & ~(0xFFFFull << 32)) | (quint64( bluePixel) << 32));},
|
|
[](QRgba64 oldPixel, quint16 alphaPixel){return qRgba64((oldPixel & ~(0xFFFFull << 48)) | (quint64(alphaPixel) << 48));}
|
|
};
|
|
|
|
if (compression) {
|
|
// Skip row lengths.
|
|
int skip_count = header.height * header.channel_count * sizeof(quint16);
|
|
if (stream.skipRawData(skip_count) != skip_count) {
|
|
return false;
|
|
}
|
|
|
|
for (unsigned short channel = 0; channel < channel_num; channel++) {
|
|
bool success = false;
|
|
if (header.depth == 8) {
|
|
success = decodeRLEData(RLEVariant::PackBits, stream,
|
|
image_data, channel_size,
|
|
&readPixel<quint8>, updaters[channel]);
|
|
} else if (header.depth == 16) {
|
|
QRgba64 *image_data = reinterpret_cast<QRgba64*>(img.bits());
|
|
success = decodeRLEData(RLEVariant::PackBits16, stream,
|
|
image_data, channel_size,
|
|
&readPixel<quint8>, updaters64[channel]);
|
|
}
|
|
|
|
if (!success) {
|
|
qDebug() << "decodeRLEData on channel" << channel << "failed";
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
for (unsigned short channel = 0; channel < channel_num; channel++) {
|
|
if (header.depth == 8) {
|
|
for (unsigned i = 0; i < pixel_count; ++i) {
|
|
image_data[i] = updaters[channel](image_data[i], readPixel<quint8>(stream));
|
|
}
|
|
} else if (header.depth == 16) {
|
|
QRgba64 *image_data = reinterpret_cast<QRgba64*>(img.bits());
|
|
for (unsigned i = 0; i < pixel_count; ++i) {
|
|
image_data[i] = updaters64[channel](image_data[i], readPixel<quint16>(stream));
|
|
}
|
|
}
|
|
// make sure we didn't try to read past the end of the stream
|
|
if (stream.status() != QDataStream::Ok) {
|
|
qDebug() << "DataStream status was" << stream.status();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // Private
|
|
|
|
PSDHandler::PSDHandler()
|
|
{
|
|
}
|
|
|
|
bool PSDHandler::canRead() const
|
|
{
|
|
if (canRead(device())) {
|
|
setFormat("psd");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool PSDHandler::read(QImage *image)
|
|
{
|
|
QDataStream s(device());
|
|
s.setByteOrder(QDataStream::BigEndian);
|
|
|
|
PSDHeader header;
|
|
s >> header;
|
|
|
|
// Check image file format.
|
|
if (s.atEnd() || !IsValid(header)) {
|
|
// qDebug() << "This PSD file is not valid.";
|
|
return false;
|
|
}
|
|
|
|
// Check if it's a supported format.
|
|
if (!IsSupported(header)) {
|
|
// qDebug() << "This PSD file is not supported.";
|
|
return false;
|
|
}
|
|
|
|
QImage img;
|
|
if (!LoadPSD(s, header, img)) {
|
|
// qDebug() << "Error loading PSD file.";
|
|
return false;
|
|
}
|
|
|
|
*image = img;
|
|
return true;
|
|
}
|
|
|
|
bool PSDHandler::canRead(QIODevice *device)
|
|
{
|
|
if (!device) {
|
|
qWarning("PSDHandler::canRead() called with no device");
|
|
return false;
|
|
}
|
|
|
|
qint64 oldPos = device->pos();
|
|
|
|
char head[4];
|
|
qint64 readBytes = device->read(head, sizeof(head));
|
|
if (readBytes < 0) {
|
|
qWarning() << "Read failed" << device->errorString();
|
|
return false;
|
|
}
|
|
|
|
if (readBytes != sizeof(head)) {
|
|
if (device->isSequential()) {
|
|
while (readBytes > 0) {
|
|
device->ungetChar(head[readBytes-- - 1]);
|
|
}
|
|
} else {
|
|
device->seek(oldPos);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (device->isSequential()) {
|
|
while (readBytes > 0) {
|
|
device->ungetChar(head[readBytes-- - 1]);
|
|
}
|
|
} else {
|
|
device->seek(oldPos);
|
|
}
|
|
|
|
return qstrncmp(head, "8BPS", 4) == 0;
|
|
}
|
|
|
|
QImageIOPlugin::Capabilities PSDPlugin::capabilities(QIODevice *device, const QByteArray &format) const
|
|
{
|
|
if (format == "psd") {
|
|
return Capabilities(CanRead);
|
|
}
|
|
if (!format.isEmpty()) {
|
|
return {};
|
|
}
|
|
if (!device->isOpen()) {
|
|
return {};
|
|
}
|
|
|
|
Capabilities cap;
|
|
if (device->isReadable() && PSDHandler::canRead(device)) {
|
|
cap |= CanRead;
|
|
}
|
|
return cap;
|
|
}
|
|
|
|
QImageIOHandler *PSDPlugin::create(QIODevice *device, const QByteArray &format) const
|
|
{
|
|
QImageIOHandler *handler = new PSDHandler;
|
|
handler->setDevice(device);
|
|
handler->setFormat(format);
|
|
return handler;
|
|
}
|