282 lines
12 KiB
C++
282 lines
12 KiB
C++
#include "IO/PSD/PSDReader.h"
|
|
#include "Document/DocumentLimits.h"
|
|
#include "IO/PSD/PSDChannelCoder.h"
|
|
#include "IO/PSD/PSDReader+Records.h"
|
|
#include "IO/PSD/PSDVector.h"
|
|
#include "Logging.h"
|
|
#include <QColorSpace>
|
|
#include <QFile>
|
|
|
|
namespace {
|
|
using namespace PSDRecords;
|
|
|
|
PSDLayerKind kindOf(const RawLayer &layer, bool isGroup)
|
|
{
|
|
if (isGroup)
|
|
return PSDLayerKind::group;
|
|
if (layer.has({"TySh", "tySh", "txt2"}))
|
|
return PSDLayerKind::text;
|
|
if (layer.has({"vmsk", "vsms", "vogk"}))
|
|
return PSDLayerKind::vector;
|
|
if (layer.has({"SoLd", "SoLE"}))
|
|
return PSDLayerKind::smartObject;
|
|
if (layer.has({"lfx2", "lrFX", "lmfx"}))
|
|
return PSDLayerKind::effects;
|
|
for (const QString &key : PSDReader::adjustmentKeys)
|
|
if (layer.extra.contains(key))
|
|
return PSDLayerKind::adjustment;
|
|
return PSDLayerKind::raster;
|
|
}
|
|
|
|
std::vector<PSDRecord> assemble(const std::vector<RawLayer> &raw, QSizeF canvas, qint64 remainingPixels)
|
|
{
|
|
std::vector<PSDRecord> result;
|
|
std::vector<QUuid> groups;
|
|
qint64 remaining = std::max<qint64>(0, remainingPixels);
|
|
for (const RawLayer &layer : raw) {
|
|
// Groups run bottom to top: divider, children, then the folder.
|
|
if (layer.section == 3) {
|
|
groups.push_back(QUuid::createUuid());
|
|
continue;
|
|
}
|
|
const bool isGroup = layer.section == 1 || layer.section == 2;
|
|
QUuid id = QUuid::createUuid();
|
|
if (isGroup && !groups.empty()) {
|
|
id = groups.back();
|
|
groups.pop_back();
|
|
}
|
|
PSDRecord record;
|
|
record.id = id;
|
|
record.name = layer.name.isEmpty() ? QStringLiteral("Layer") : layer.name;
|
|
if (!groups.empty())
|
|
record.parentID = groups.back();
|
|
record.isGroup = isGroup;
|
|
record.isVisible = !layer.hidden;
|
|
const bool passes = layer.blendKey == QLatin1String("pass") || layer.blendKey == QLatin1String("norm");
|
|
record.blendKey = isGroup && passes ? QStringLiteral("pass") : layer.blendKey;
|
|
record.clipping = layer.clipping;
|
|
record.kind = kindOf(layer, isGroup);
|
|
record.croppedToCanvas = layer.cropped;
|
|
const bool hasEffects = record.kind == PSDLayerKind::effects || layer.has({"lfx2", "lrFX", "lmfx"});
|
|
record.opacity = hasEffects && layer.fill != 255 ? layer.opacity / 255.0 : (layer.opacity / 255.0) * (layer.fill / 255.0);
|
|
record.bounds = isGroup ? QRectF(QPointF(0, 0), canvas) : QRectF(layer.left, layer.top, std::max<qint64>(0, layer.right - layer.left), std::max<qint64>(0, layer.bottom - layer.top));
|
|
record.image = isGroup ? std::nullopt : layer.image;
|
|
if (record.kind == PSDLayerKind::text)
|
|
record.text = PSDText::parse(layer.extra);
|
|
std::optional<PSDVector::Live> live = isGroup || record.text ? std::nullopt : PSDVector::live(layer.extra, canvas, remaining);
|
|
if (live) {
|
|
record.image = live->image;
|
|
record.bounds = live->bounds;
|
|
record.shape = live->style;
|
|
record.shapeNotes = live->notes;
|
|
record.kind = PSDLayerKind::vector;
|
|
remaining = std::max<qint64>(0, remaining - qint64(live->image.width()) * live->image.height());
|
|
} else if (!record.text && !record.image && !isGroup) {
|
|
if (const std::optional<PSDVector::Raster> raster = PSDVector::raster(layer.extra, canvas, remaining)) {
|
|
record.image = raster->image;
|
|
record.bounds = raster->bounds;
|
|
record.kind = PSDLayerKind::vector;
|
|
remaining = std::max<qint64>(0, remaining - qint64(raster->image.width()) * raster->image.height());
|
|
}
|
|
}
|
|
record.mask = layer.maskFromRender ? std::nullopt : layer.maskImage;
|
|
record.maskBounds = QRectF(layer.maskLeft, layer.maskTop, layer.maskRight - layer.maskLeft, layer.maskBottom - layer.maskTop);
|
|
record.maskDefault = layer.maskDefault;
|
|
record.maskEnabled = !layer.maskDisabled;
|
|
record.maskLinked = layer.maskLinked;
|
|
if (!isGroup)
|
|
record.adjustment = PSDAdjustments::parse(layer.extra);
|
|
if (record.adjustment)
|
|
record.kind = PSDLayerKind::adjustment;
|
|
result.push_back(std::move(record));
|
|
}
|
|
if (!groups.empty())
|
|
throw PSDError(PSDError::Kind::truncated);
|
|
return result;
|
|
}
|
|
|
|
// What precedes the layers: the canvas, its channels, its profile.
|
|
struct Header {
|
|
bool isPSB;
|
|
int channels;
|
|
qint64 width;
|
|
qint64 height;
|
|
double resolution = 72;
|
|
QByteArray profile;
|
|
};
|
|
|
|
// A layered canvas stays one surface; merged, the budget.
|
|
Header readHeader(Cursor &cursor, bool surface)
|
|
{
|
|
if (cursor.string(4) != QLatin1String("8BPS"))
|
|
throw ImageImportError(ImageImportError::Kind::unreadable);
|
|
// Version 2 is a Large Document, some lengths wider.
|
|
const int version = cursor.u16();
|
|
if (version != 1 && version != 2)
|
|
throw PSDError(PSDError::Kind::unsupportedVersion);
|
|
cursor.skip(6);
|
|
Header header{version == 2, cursor.u16(), 0, 0, 72, {}};
|
|
header.height = cursor.u32();
|
|
header.width = cursor.u32();
|
|
const int depth = cursor.u16(), mode = cursor.u16();
|
|
if (header.width < 1 || header.width > DocumentLimits::maxSide || header.height < 1 || header.height > DocumentLimits::maxSide
|
|
|| (surface && header.width * header.height > DocumentLimits::maxSurfacePixels))
|
|
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
|
if (depth != 8)
|
|
throw PSDError(PSDError::Kind::unsupportedDepth);
|
|
if (mode != 3)
|
|
throw PSDError(PSDError::Kind::unsupportedColorMode);
|
|
cursor.skip(cursor.u32());
|
|
const qint64 resourcesLength = cursor.u32();
|
|
const qint64 resourcesEnd = cursor.offset + resourcesLength;
|
|
while (cursor.offset + 12 <= resourcesEnd) {
|
|
if (cursor.string(4) != QLatin1String("8BIM"))
|
|
break;
|
|
const int id = cursor.u16();
|
|
const int nameLength = cursor.u8();
|
|
cursor.skip(nameLength);
|
|
if ((nameLength + 1) % 2 == 1)
|
|
cursor.skip(1);
|
|
const qint64 length = cursor.u32();
|
|
const qint64 dataStart = cursor.offset;
|
|
if (id == 1005 && length >= 4) {
|
|
header.resolution = cursor.u32() / 65536.0;
|
|
// A fixed-point number is finite; below one reads as 72.
|
|
header.resolution = std::min(9600.0, header.resolution < 1 ? 72 : header.resolution);
|
|
}
|
|
// Only the merged image reads the profile, as Swift's.
|
|
if (id == 1039 && !surface)
|
|
header.profile = cursor.bytes(length);
|
|
cursor.offset = dataStart + length;
|
|
if (length % 2 == 1)
|
|
cursor.skip(1);
|
|
}
|
|
cursor.offset = resourcesEnd;
|
|
return header;
|
|
}
|
|
|
|
PSDDocument parse(const QByteArray &data, qint64 remainingPixels)
|
|
{
|
|
Cursor cursor{data};
|
|
const Header header = readHeader(cursor, true);
|
|
const qint64 layerSection = cursor.length(header.isPSB);
|
|
const PSDDocument empty{int(header.width), int(header.height), header.resolution, {}};
|
|
if (layerSection < 4)
|
|
return empty;
|
|
// The layer info length is skipped, as in Swift.
|
|
cursor.length(header.isPSB);
|
|
const qint64 count = std::abs(qint64(cursor.i16()));
|
|
if (count > 10'000)
|
|
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
|
std::vector<RawLayer> raw;
|
|
raw.reserve(size_t(count));
|
|
for (qint64 index = 0; index < count; ++index)
|
|
raw.push_back(readRecord(cursor, header.isPSB));
|
|
// Too large whole: cut to the canvas, judged again.
|
|
if (!fitsBudget(raw, remainingPixels)) {
|
|
for (RawLayer &layer : raw)
|
|
cropToCanvas(layer, header.width, header.height);
|
|
if (!fitsBudget(raw, remainingPixels))
|
|
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
|
}
|
|
qint64 usedPixels = 0;
|
|
for (RawLayer &layer : raw) {
|
|
decodeChannels(cursor, layer, remainingPixels - usedPixels, header.isPSB);
|
|
if (layer.image)
|
|
usedPixels += qint64(layer.image->width()) * layer.image->height();
|
|
}
|
|
return PSDDocument{int(header.width), int(header.height), header.resolution,
|
|
assemble(raw, QSizeF(header.width, header.height), remainingPixels - usedPixels)};
|
|
}
|
|
|
|
// The merged image, as ImageIO reads a layerless file.
|
|
QImage mergedImage(const QByteArray &data, qint64 remainingPixels)
|
|
{
|
|
Cursor cursor{data};
|
|
const Header header = readHeader(cursor, false);
|
|
if (header.width * header.height > remainingPixels)
|
|
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
|
if (header.channels < 3)
|
|
throw PSDError(PSDError::Kind::unsupportedColorMode);
|
|
cursor.skip(cursor.length(header.isPSB));
|
|
const int compression = cursor.u16();
|
|
const QByteArray planes = data.mid(cursor.offset);
|
|
// Opaque: a file of a background alone keeps no transparency.
|
|
const std::vector<std::vector<uchar>> decoded = PSDChannelCoder::mergedPlanes(compression, int(header.width), int(header.height), header.channels, 3, planes, header.isPSB);
|
|
QImage image = PSDChannelCoder::rgbaImage(int(header.width), int(header.height), decoded[0], decoded[1], decoded[2],
|
|
std::vector<uchar>(size_t(header.width * header.height), 255));
|
|
// Its own profile, else sRGB, as ImageIO reads it.
|
|
const QColorSpace profile = QColorSpace::fromIccProfile(header.profile);
|
|
image.setColorSpace(profile.isValid() ? profile : QColorSpace(QColorSpace::SRgb));
|
|
return image;
|
|
}
|
|
}
|
|
|
|
const std::set<QString> PSDReader::adjustmentKeys{
|
|
QStringLiteral("levl"), QStringLiteral("curv"), QStringLiteral("hue2"), QStringLiteral("hue "), QStringLiteral("expA"), QStringLiteral("grdm"),
|
|
QStringLiteral("brit"), QStringLiteral("blnc"), QStringLiteral("nvrt"), QStringLiteral("thrs"), QStringLiteral("post"), QStringLiteral("mixr"),
|
|
QStringLiteral("selc"), QStringLiteral("blwh"), QStringLiteral("phfl"), QStringLiteral("vibA"),
|
|
};
|
|
|
|
bool PSDReader::matches(const QString &path)
|
|
{
|
|
QFile file(path);
|
|
return file.open(QIODevice::ReadOnly) && file.read(4) == "8BPS";
|
|
}
|
|
|
|
bool PSDReader::matches(const QByteArray &data)
|
|
{
|
|
return data.startsWith("8BPS");
|
|
}
|
|
|
|
PSDDocument PSDReader::read(const QString &path, qint64 remainingPixels)
|
|
{
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
qCWarning(lcIO).noquote() << "cannot open the Photoshop file" << path << file.errorString();
|
|
throw ImageImportError(ImageImportError::Kind::unreadable);
|
|
}
|
|
// Swift's `mappedIfSafe`: mapped where it can be, else read.
|
|
if (uchar *mapped = file.map(0, file.size()))
|
|
return read(QByteArray::fromRawData(reinterpret_cast<const char *>(mapped), file.size()), remainingPixels);
|
|
const QByteArray bytes = file.readAll();
|
|
if (file.error() != QFileDevice::NoError) {
|
|
qCWarning(lcIO).noquote() << "cannot read the Photoshop file" << path << file.errorString();
|
|
throw ImageImportError(ImageImportError::Kind::unreadable);
|
|
}
|
|
return read(bytes, remainingPixels);
|
|
}
|
|
|
|
QImage PSDReader::merged(const QString &path, qint64 remainingPixels)
|
|
{
|
|
QFile file(path);
|
|
if (!file.open(QIODevice::ReadOnly)) {
|
|
qCWarning(lcIO).noquote() << "cannot open the Photoshop file" << path << file.errorString();
|
|
throw ImageImportError(ImageImportError::Kind::unreadable);
|
|
}
|
|
// Swift asks the size first: the header, before the file.
|
|
const QByteArray head = file.peek(26);
|
|
Cursor cursor{head};
|
|
cursor.skip(12);
|
|
cursor.u16();
|
|
const qint64 height = cursor.u32(), width = cursor.u32();
|
|
if (width > DocumentLimits::maxSide || height > DocumentLimits::maxSide || width * height > remainingPixels)
|
|
throw ImageImportError(ImageImportError::Kind::tooLarge);
|
|
const QByteArray bytes = file.readAll();
|
|
if (file.error() != QFileDevice::NoError) {
|
|
qCWarning(lcIO).noquote() << "cannot read the Photoshop file" << path << file.errorString();
|
|
throw ImageImportError(ImageImportError::Kind::unreadable);
|
|
}
|
|
return mergedImage(bytes, remainingPixels);
|
|
}
|
|
|
|
PSDDocument PSDReader::read(const QByteArray &data, qint64 remainingPixels)
|
|
{
|
|
try {
|
|
return parse(data, remainingPixels);
|
|
} catch (const std::runtime_error &error) {
|
|
qCWarning(lcIO) << "a Photoshop file was refused:" << error.what();
|
|
throw;
|
|
}
|
|
}
|